feat: add anti-detect functionality

This commit is contained in:
zhom
2025-07-07 06:19:43 +04:00
parent 198046fca9
commit 703ca2c50b
30 changed files with 5844 additions and 759 deletions
+44 -2
View File
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { FaDownload } from "react-icons/fa";
import { FiWifi } from "react-icons/fi";
import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go";
import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
import { ChangeVersionDialog } from "@/components/change-version-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog";
import { ImportProfileDialog } from "@/components/import-profile-dialog";
@@ -36,7 +37,7 @@ import { useUpdateNotifications } from "@/hooks/use-update-notifications";
import { useVersionUpdater } from "@/hooks/use-version-updater";
import { showErrorToast } from "@/lib/toast-utils";
import { sleep } from "@/lib/utils";
import type { BrowserProfile } from "@/types";
import type { BrowserProfile, CamoufoxConfig } from "@/types";
type BrowserTypeString =
| "mullvad-browser"
@@ -45,7 +46,8 @@ type BrowserTypeString =
| "chromium"
| "brave"
| "zen"
| "tor-browser";
| "tor-browser"
| "camoufox";
interface PendingUrl {
id: string;
@@ -62,11 +64,15 @@ export default function Home() {
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
const [proxyManagementDialogOpen, setProxyManagementDialogOpen] =
useState(false);
const [camoufoxConfigDialogOpen, setCamoufoxConfigDialogOpen] =
useState(false);
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
const [currentProfileForProxy, setCurrentProfileForProxy] =
useState<BrowserProfile | null>(null);
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
useState<BrowserProfile | null>(null);
const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] =
useState<BrowserProfile | null>(null);
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [currentPermissionType, setCurrentPermissionType] =
@@ -326,6 +332,30 @@ export default function Home() {
setChangeVersionDialogOpen(true);
}, []);
const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => {
setCurrentProfileForCamoufoxConfig(profile);
setCamoufoxConfigDialogOpen(true);
}, []);
const handleSaveCamoufoxConfig = useCallback(
async (profile: BrowserProfile, config: CamoufoxConfig) => {
setError(null);
try {
await invoke("update_camoufox_config", {
profileName: profile.name,
config,
});
await loadProfiles();
setCamoufoxConfigDialogOpen(false);
} catch (err: unknown) {
console.error("Failed to update camoufox config:", err);
setError(`Failed to update camoufox config: ${JSON.stringify(err)}`);
throw err;
}
},
[loadProfiles],
);
const handleSaveProxy = useCallback(
async (proxyId: string | null) => {
setProxyDialogOpen(false);
@@ -356,6 +386,7 @@ export default function Home() {
version: string;
releaseType: string;
proxyId?: string;
camoufoxConfig?: CamoufoxConfig;
}) => {
setError(null);
@@ -368,6 +399,7 @@ export default function Home() {
version: profileData.version,
releaseType: profileData.releaseType,
proxyId: profileData.proxyId,
camoufoxConfig: profileData.camoufoxConfig,
},
);
@@ -658,6 +690,7 @@ export default function Home() {
onDeleteProfile={handleDeleteProfile}
onRenameProfile={handleRenameProfile}
onChangeVersion={openChangeVersionDialog}
onConfigureCamoufox={handleConfigureCamoufox}
runningProfiles={runningProfiles}
isUpdating={isUpdating}
onReloadProxyData={
@@ -739,6 +772,15 @@ export default function Home() {
permissionType={currentPermissionType}
onPermissionGranted={checkNextPermission}
/>
<CamoufoxConfigDialog
isOpen={camoufoxConfigDialogOpen}
onClose={() => {
setCamoufoxConfigDialogOpen(false);
}}
profile={currentProfileForCamoufoxConfig}
onSave={handleSaveCamoufoxConfig}
/>
</div>
);
}
+502
View File
@@ -0,0 +1,502 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { BrowserProfile, CamoufoxConfig } from "@/types";
interface CamoufoxConfigDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise<void>;
}
const osOptions = [
{ value: "windows", label: "Windows" },
{ value: "macos", label: "macOS" },
{ value: "linux", label: "Linux" },
];
const timezoneOptions = [
{ value: "America/New_York", label: "America/New_York" },
{ value: "America/Los_Angeles", label: "America/Los_Angeles" },
{ value: "Europe/London", label: "Europe/London" },
{ value: "Europe/Paris", label: "Europe/Paris" },
{ value: "Asia/Tokyo", label: "Asia/Tokyo" },
{ value: "Asia/Shanghai", label: "Asia/Shanghai" },
{ value: "Australia/Sydney", label: "Australia/Sydney" },
];
const localeOptions = [
{ value: "en-US", label: "English (US)" },
{ value: "en-GB", label: "English (UK)" },
{ value: "fr-FR", label: "French" },
{ value: "de-DE", label: "German" },
{ value: "es-ES", label: "Spanish" },
{ value: "it-IT", label: "Italian" },
{ value: "ja-JP", label: "Japanese" },
{ value: "zh-CN", label: "Chinese (Simplified)" },
];
const getCurrentOS = () => {
if (typeof window !== "undefined") {
const userAgent = window.navigator.userAgent;
if (userAgent.includes("Win")) return "windows";
if (userAgent.includes("Mac")) return "macos";
if (userAgent.includes("Linux")) return "linux";
}
return "unknown";
};
export function CamoufoxConfigDialog({
isOpen,
onClose,
profile,
onSave,
}: CamoufoxConfigDialogProps) {
const [config, setConfig] = useState<CamoufoxConfig>({
enable_cache: true,
os: [getCurrentOS()],
});
const [isSaving, setIsSaving] = useState(false);
// Initialize config when profile changes
useEffect(() => {
if (profile && profile.browser === "camoufox") {
setConfig(
profile.camoufox_config || {
enable_cache: true,
os: [getCurrentOS()],
},
);
}
}, [profile]);
const updateConfig = (key: keyof CamoufoxConfig, value: unknown) => {
setConfig((prev) => ({ ...prev, [key]: value }));
};
const handleSave = async () => {
if (!profile) return;
setIsSaving(true);
try {
await onSave(profile, config);
onClose();
} catch (error) {
console.error("Failed to save camoufox config:", error);
} finally {
setIsSaving(false);
}
};
const handleClose = () => {
// Reset config to original when closing without saving
if (profile && profile.browser === "camoufox") {
setConfig(
profile.camoufox_config || {
enable_cache: true,
os: [getCurrentOS()],
},
);
}
onClose();
};
if (!profile || profile.browser !== "camoufox") {
return null;
}
// Get the selected OS for warning
const selectedOS = config.os?.[0];
const currentOS = getCurrentOS();
const showOSWarning =
selectedOS && selectedOS !== currentOS && currentOS !== "unknown";
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>
Configure Camoufox Settings - {profile.name}
</DialogTitle>
</DialogHeader>
<ScrollArea className="flex-1 pr-6 h-[350px]">
<div className="py-4 space-y-6">
{/* Operating System */}
<div className="space-y-3">
<Label>Operating System Fingerprint</Label>
<Select
value={selectedOS || ""}
onValueChange={(value: string) => updateConfig("os", [value])}
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
{osOptions.map((os) => (
<SelectItem key={os.value} value={os.value}>
{os.label}
</SelectItem>
))}
</SelectContent>
</Select>
{showOSWarning && (
<div className="p-3 bg-amber-50 rounded-md border border-amber-200">
<p className="text-sm text-amber-800">
Warning: Spoofing OS features is detectable by advanced
anti-bot systems. Some platform-specific APIs and behaviors
cannot be fully replicated.
</p>
</div>
)}
</div>
{/* Blocking Options */}
<div className="space-y-3">
<Label>Privacy & Blocking</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="block-images"
checked={config.block_images || false}
onCheckedChange={(checked) =>
updateConfig("block_images", checked)
}
/>
<Label htmlFor="block-images">Block Images</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webrtc"
checked={config.block_webrtc || false}
onCheckedChange={(checked) =>
updateConfig("block_webrtc", checked)
}
/>
<Label htmlFor="block-webrtc">Block WebRTC</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webgl"
checked={config.block_webgl || false}
onCheckedChange={(checked) =>
updateConfig("block_webgl", checked)
}
/>
<Label htmlFor="block-webgl">Block WebGL</Label>
</div>
</div>
</div>
{/* Geolocation */}
<div className="space-y-3">
<Label>Geolocation</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="country">Country</Label>
<Input
id="country"
value={config.country || ""}
onChange={(e) =>
updateConfig("country", e.target.value || undefined)
}
placeholder="e.g., US, GB, DE"
/>
</div>
<div className="space-y-2">
<Label>Timezone</Label>
<Select
value={config.timezone || "auto"}
onValueChange={(value) =>
updateConfig(
"timezone",
value === "auto" ? undefined : value,
)
}
>
<SelectTrigger>
<SelectValue placeholder="Select timezone" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
{timezoneOptions.map((tz) => (
<SelectItem key={tz.value} value={tz.value}>
{tz.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="latitude">Latitude</Label>
<Input
id="latitude"
type="number"
step="any"
value={config.latitude || ""}
onChange={(e) =>
updateConfig(
"latitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
placeholder="e.g., 40.7128"
/>
</div>
<div className="space-y-2">
<Label htmlFor="longitude">Longitude</Label>
<Input
id="longitude"
type="number"
step="any"
value={config.longitude || ""}
onChange={(e) =>
updateConfig(
"longitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
placeholder="e.g., -74.0060"
/>
</div>
</div>
</div>
{/* Localization */}
<div className="space-y-3">
<Label>Locale</Label>
<Select
value={config.locale?.[0] || ""}
onValueChange={(value) =>
updateConfig("locale", value ? [value] : undefined)
}
>
<SelectTrigger>
<SelectValue placeholder="Select locale" />
</SelectTrigger>
<SelectContent>
{localeOptions.map((locale) => (
<SelectItem key={locale.value} value={locale.value}>
{locale.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Screen Resolution */}
<div className="space-y-3">
<Label>Screen Resolution</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="screen-min-width">Min Width</Label>
<Input
id="screen-min-width"
type="number"
value={config.screen_min_width || ""}
onChange={(e) =>
updateConfig(
"screen_min_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1024"
/>
</div>
<div className="space-y-2">
<Label htmlFor="screen-max-width">Max Width</Label>
<Input
id="screen-max-width"
type="number"
value={config.screen_max_width || ""}
onChange={(e) =>
updateConfig(
"screen_max_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1920"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="screen-min-height">Min Height</Label>
<Input
id="screen-min-height"
type="number"
value={config.screen_min_height || ""}
onChange={(e) =>
updateConfig(
"screen_min_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 768"
/>
</div>
<div className="space-y-2">
<Label htmlFor="screen-max-height">Max Height</Label>
<Input
id="screen-max-height"
type="number"
value={config.screen_max_height || ""}
onChange={(e) =>
updateConfig(
"screen_max_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1080"
/>
</div>
</div>
</div>
{/* Window Size */}
<div className="space-y-3">
<Label>Window Size</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="window-width">Width</Label>
<Input
id="window-width"
type="number"
value={config.window_width || ""}
onChange={(e) =>
updateConfig(
"window_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1366"
/>
</div>
<div className="space-y-2">
<Label htmlFor="window-height">Height</Label>
<Input
id="window-height"
type="number"
value={config.window_height || ""}
onChange={(e) =>
updateConfig(
"window_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 768"
/>
</div>
</div>
</div>
{/* Advanced Options */}
<div className="space-y-3">
<Label>Advanced Options</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="enable-cache"
checked={config.enable_cache || false}
onCheckedChange={(checked) =>
updateConfig("enable_cache", checked)
}
/>
<Label htmlFor="enable-cache">Enable Browser Cache</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="main-world-eval"
checked={config.main_world_eval || false}
onCheckedChange={(checked) =>
updateConfig("main_world_eval", checked)
}
/>
<Label htmlFor="main-world-eval">
Enable Main World Evaluation
</Label>
</div>
</div>
</div>
{/* WebGL Settings */}
<div className="space-y-3">
<Label>WebGL Settings</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="webgl-vendor">WebGL Vendor</Label>
<Input
id="webgl-vendor"
value={config.webgl_vendor || ""}
onChange={(e) =>
updateConfig("webgl_vendor", e.target.value || undefined)
}
placeholder="e.g., Intel Inc."
/>
</div>
<div className="space-y-2">
<Label htmlFor="webgl-renderer">WebGL Renderer</Label>
<Input
id="webgl-renderer"
value={config.webgl_renderer || ""}
onChange={(e) =>
updateConfig(
"webgl_renderer",
e.target.value || undefined,
)
}
placeholder="e.g., Intel Iris OpenGL Engine"
/>
</div>
</div>
</div>
{/* Debug Options */}
<div className="space-y-3">
<Label>Debug Options</Label>
<div className="flex items-center space-x-2">
<Checkbox
id="debug"
checked={config.debug || false}
onCheckedChange={(checked) => updateConfig("debug", checked)}
/>
<Label htmlFor="debug">Enable Debug Mode</Label>
</div>
</div>
</div>
</ScrollArea>
<DialogFooter className="flex-shrink-0 pt-4 border-t">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? "Saving..." : "Save Configuration"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+338 -416
View File
@@ -2,12 +2,10 @@
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
import { FiPlus } from "react-icons/fi";
import { toast } from "sonner";
import { LoadingButton } from "@/components/loading-button";
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
import { ReleaseTypeSelector } from "@/components/release-type-selector";
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import {
Dialog,
DialogContent,
@@ -17,6 +15,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
@@ -24,16 +23,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useBrowserDownload } from "@/hooks/use-browser-download";
import { useBrowserSupport } from "@/hooks/use-browser-support";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import type { BrowserProfile, BrowserReleaseTypes, StoredProxy } from "@/types";
import { Alert, AlertDescription } from "./ui/alert";
import { getBrowserIcon } from "@/lib/browser-utils";
import type { BrowserReleaseTypes, CamoufoxConfig, StoredProxy } from "@/types";
type BrowserTypeString =
| "mullvad-browser"
@@ -42,7 +35,8 @@ type BrowserTypeString =
| "chromium"
| "brave"
| "zen"
| "tor-browser";
| "tor-browser"
| "camoufox";
interface CreateProfileDialogProps {
isOpen: boolean;
@@ -53,503 +47,431 @@ interface CreateProfileDialogProps {
version: string;
releaseType: string;
proxyId?: string;
camoufoxConfig?: CamoufoxConfig;
}) => Promise<void>;
}
interface BrowserOption {
value: BrowserTypeString;
label: string;
description: string;
}
const browserOptions: BrowserOption[] = [
{
value: "firefox",
label: "Firefox",
description: "Mozilla's main web browser",
},
{
value: "firefox-developer",
label: "Firefox Developer Edition",
description: "Browser for developers with cutting-edge features",
},
{
value: "chromium",
label: "Chromium",
description: "Open-source version of Chrome",
},
{
value: "brave",
label: "Brave",
description: "Privacy-focused browser with ad blocking",
},
{
value: "zen",
label: "Zen Browser",
description: "Beautiful, customizable Firefox-based browser",
},
{
value: "mullvad-browser",
label: "Mullvad Browser",
description: "Privacy browser by Mullvad VPN",
},
{
value: "tor-browser",
label: "Tor Browser",
description: "Browse anonymously through the Tor network",
},
];
const getCurrentOS = () => {
if (typeof window !== "undefined") {
const userAgent = window.navigator.userAgent;
if (userAgent.includes("Win")) return "windows";
if (userAgent.includes("Mac")) return "macos";
if (userAgent.includes("Linux")) return "linux";
}
return "unknown";
};
export function CreateProfileDialog({
isOpen,
onClose,
onCreateProfile,
}: CreateProfileDialogProps) {
const [profileName, setProfileName] = useState("");
const [selectedBrowser, setSelectedBrowser] =
useState<BrowserTypeString | null>("mullvad-browser");
const [selectedReleaseType, setSelectedReleaseType] = useState<
"stable" | "nightly" | null
>(null);
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>({
stable: undefined,
nightly: undefined,
const [activeTab, setActiveTab] = useState("regular");
// Regular browser states
const [selectedBrowser, setSelectedBrowser] = useState<BrowserTypeString>();
const [selectedProxyId, setSelectedProxyId] = useState<string>();
// Camoufox anti-detect states
const [camoufoxConfig, setCamoufoxConfig] = useState<CamoufoxConfig>({
enable_cache: true, // Cache enabled by default
os: [getCurrentOS()], // Default to current OS
});
const [isCreating, setIsCreating] = useState(false);
const [existingProfiles, setExistingProfiles] = useState<BrowserProfile[]>(
[],
);
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
// Proxy settings - now using stored proxy selection
const [selectedProxyId, setSelectedProxyId] = useState<string | null>(null);
// Common states
const [availableReleaseTypes, setAvailableReleaseTypes] =
useState<BrowserReleaseTypes>({});
const [camoufoxReleaseTypes, setCamoufoxReleaseTypes] =
useState<BrowserReleaseTypes>({});
const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]);
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
const [isLoadingProxies, setIsLoadingProxies] = useState(false);
const [showProxyForm, setShowProxyForm] = useState(false);
const [isCreating, setIsCreating] = useState(false);
// Use the browser download hook
const {
isBrowserDownloading,
downloadBrowser,
isDownloading,
downloadedVersions,
loadDownloadedVersions,
isVersionDownloaded,
} = useBrowserDownload();
const {
supportedBrowsers,
isLoading: isLoadingSupport,
isBrowserSupported,
} = useBrowserSupport();
useEffect(() => {
if (supportedBrowsers.length > 0) {
// Set default browser to first supported browser
if (supportedBrowsers.includes("mullvad-browser")) {
setSelectedBrowser("mullvad-browser");
} else if (supportedBrowsers.length > 0) {
setSelectedBrowser(supportedBrowsers[0] as BrowserTypeString);
}
}
}, [supportedBrowsers]);
// Set default release type when release types are loaded
useEffect(() => {
if (!selectedReleaseType && Object.keys(releaseTypes).length > 0) {
// First try to set stable if it exists
if (releaseTypes.stable) {
setSelectedReleaseType("stable");
}
// If stable doesn't exist but nightly does, set nightly as default
else if (releaseTypes.nightly && selectedBrowser !== "chromium") {
setSelectedReleaseType("nightly");
}
}
}, [releaseTypes, selectedReleaseType, selectedBrowser]);
const loadExistingProfiles = useCallback(async () => {
const loadSupportedBrowsers = useCallback(async () => {
try {
const profiles = await invoke<BrowserProfile[]>("list_browser_profiles");
setExistingProfiles(profiles);
const browsers = await invoke<string[]>("get_supported_browsers");
setSupportedBrowsers(browsers);
} catch (error) {
console.error("Failed to load existing profiles:", error);
console.error("Failed to load supported browsers:", error);
}
}, []);
const loadStoredProxies = useCallback(async () => {
try {
setIsLoadingProxies(true);
const proxies = await invoke<StoredProxy[]>("get_stored_proxies");
setStoredProxies(proxies);
} catch (error) {
console.error("Failed to load stored proxies:", error);
toast.error("Failed to load available proxies");
} finally {
setIsLoadingProxies(false);
}
}, []);
const loadReleaseTypes = useCallback(async (browser: string) => {
try {
setIsLoadingReleaseTypes(true);
const types = await invoke<BrowserReleaseTypes>(
"get_browser_release_types",
{
browserStr: browser,
},
);
setReleaseTypes(types);
} catch (error) {
console.error("Failed to load release types:", error);
toast.error("Failed to load available versions");
} finally {
setIsLoadingReleaseTypes(false);
}
}, []);
const loadReleaseTypes = useCallback(
async (browser: string) => {
try {
const releaseTypes = await invoke<BrowserReleaseTypes>(
"get_browser_release_types",
{ browserStr: browser },
);
const handleDownload = useCallback(async () => {
if (!selectedBrowser || !selectedReleaseType) return;
if (browser === "camoufox") {
setCamoufoxReleaseTypes(releaseTypes);
} else {
setAvailableReleaseTypes(releaseTypes);
}
const version =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
if (!version) return;
await downloadBrowser(selectedBrowser, version);
}, [selectedBrowser, selectedReleaseType, downloadBrowser, releaseTypes]);
const validateProfileName = useCallback(
(name: string): string | null => {
const trimmedName = name.trim();
if (!trimmedName) {
return "Profile name cannot be empty";
// Load downloaded versions for this browser
await loadDownloadedVersions(browser);
} catch (error) {
console.error(`Failed to load release types for ${browser}:`, error);
}
// Check for duplicate names (case insensitive)
const isDuplicate = existingProfiles.some(
(profile) => profile.name.toLowerCase() === trimmedName.toLowerCase(),
);
if (isDuplicate) {
return "A profile with this name already exists";
}
return null;
},
[existingProfiles],
[loadDownloadedVersions],
);
// Helper to determine if proxy should be disabled for the selected browser
const isProxyDisabled = selectedBrowser === "tor-browser";
// Update proxy selection when browser changes to tor-browser
// Load data when dialog opens
useEffect(() => {
if (selectedBrowser === "tor-browser" && selectedProxyId) {
setSelectedProxyId(null);
if (isOpen) {
void loadSupportedBrowsers();
void loadStoredProxies();
// Load camoufox release types when dialog opens
void loadReleaseTypes("camoufox");
}
}, [selectedBrowser, selectedProxyId]);
}, [isOpen, loadSupportedBrowsers, loadStoredProxies, loadReleaseTypes]);
const handleCreateProxy = useCallback(() => {
setShowProxyForm(true);
}, []);
// Load release types when browser selection changes
useEffect(() => {
if (selectedBrowser) {
void loadReleaseTypes(selectedBrowser);
}
}, [selectedBrowser, loadReleaseTypes]);
const handleProxySaved = useCallback((savedProxy: StoredProxy) => {
setStoredProxies((prev) => {
const existingIndex = prev.findIndex((p) => p.id === savedProxy.id);
if (existingIndex >= 0) {
// Update existing proxy
const updated = [...prev];
updated[existingIndex] = savedProxy;
return updated;
} else {
// Add new proxy
return [...prev, savedProxy];
}
});
setSelectedProxyId(savedProxy.id);
setShowProxyForm(false);
}, []);
const handleDownload = async (browserStr: string) => {
const releaseTypes =
browserStr === "camoufox" ? camoufoxReleaseTypes : availableReleaseTypes;
const latestStableVersion = releaseTypes.stable;
const handleProxyFormClose = useCallback(() => {
setShowProxyForm(false);
}, []);
const handleCreate = useCallback(async () => {
if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return;
// Validate profile name
const nameError = validateProfileName(profileName);
if (nameError) {
toast.error(nameError);
if (!latestStableVersion) {
console.error("No stable version available for download");
return;
}
const version =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
if (!version) {
toast.error("Selected release type is not available");
return;
try {
await downloadBrowser(browserStr, latestStableVersion);
} catch (error) {
console.error("Failed to download browser:", error);
}
};
const handleCreate = async () => {
if (!profileName.trim()) return;
setIsCreating(true);
try {
await onCreateProfile({
name: profileName.trim(),
browserStr: selectedBrowser,
version,
releaseType: selectedReleaseType,
proxyId: isProxyDisabled ? undefined : (selectedProxyId ?? undefined),
});
if (activeTab === "regular") {
if (!selectedBrowser) {
console.error("Missing required browser selection");
return;
}
// Reset form
setProfileName("");
setSelectedReleaseType(null);
setSelectedProxyId(null);
onClose();
// Use the latest stable version by default
const latestStableVersion = availableReleaseTypes.stable;
if (!latestStableVersion) {
console.error("No stable version available");
return;
}
await onCreateProfile({
name: profileName.trim(),
browserStr: selectedBrowser,
version: latestStableVersion,
releaseType: "stable",
proxyId: selectedProxyId,
});
} else {
// Anti-detect tab - always use Camoufox with latest version
const latestCamoufoxVersion = camoufoxReleaseTypes.stable;
if (!latestCamoufoxVersion) {
console.error("No Camoufox version available");
return;
}
await onCreateProfile({
name: profileName.trim(),
browserStr: "camoufox" as BrowserTypeString,
version: latestCamoufoxVersion,
releaseType: "stable",
proxyId: selectedProxyId,
camoufoxConfig,
});
}
handleClose();
} catch (error) {
console.error("Failed to create profile:", error);
} finally {
setIsCreating(false);
}
}, [
profileName,
selectedBrowser,
selectedReleaseType,
onCreateProfile,
isProxyDisabled,
selectedProxyId,
onClose,
releaseTypes.nightly,
releaseTypes.stable,
validateProfileName,
]);
};
const nameError = profileName.trim()
? validateProfileName(profileName)
: null;
const handleClose = () => {
// Reset all states
setProfileName("");
setSelectedBrowser(undefined);
setSelectedProxyId(undefined);
setCamoufoxConfig({
enable_cache: true,
os: [getCurrentOS()], // Reset to current OS
});
setActiveTab("regular");
onClose();
};
const selectedVersion =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
const isCreateDisabled = () => {
if (!profileName.trim()) return true;
const canCreate =
profileName.trim() &&
selectedBrowser &&
selectedReleaseType &&
selectedVersion &&
isVersionDownloaded(selectedVersion) &&
!nameError;
useEffect(() => {
if (isOpen) {
void loadExistingProfiles();
void loadStoredProxies();
if (activeTab === "regular") {
return !selectedBrowser || !availableReleaseTypes.stable;
} else {
// For anti-detect, we need camoufox to be available
return !camoufoxReleaseTypes.stable;
}
}, [isOpen, loadExistingProfiles, loadStoredProxies]);
};
useEffect(() => {
if (isOpen && selectedBrowser) {
// Reset selected release type when browser changes
setSelectedReleaseType(null);
void loadReleaseTypes(selectedBrowser);
void loadDownloadedVersions(selectedBrowser);
}
}, [isOpen, selectedBrowser, loadDownloadedVersions, loadReleaseTypes]);
const updateCamoufoxConfig = (key: keyof CamoufoxConfig, value: unknown) => {
setCamoufoxConfig((prev) => ({ ...prev, [key]: value }));
};
// Check if browser version is downloaded and available
const isBrowserVersionAvailable = (browserStr: string) => {
const releaseTypes =
browserStr === "camoufox" ? camoufoxReleaseTypes : availableReleaseTypes;
const latestStableVersion = releaseTypes.stable;
return latestStableVersion && isVersionDownloaded(latestStableVersion);
};
// Get the selected OS for warning
const selectedOS = camoufoxConfig.os?.[0];
const currentOS = getCurrentOS();
const _showOSWarning =
selectedOS && selectedOS !== currentOS && currentOS !== "unknown";
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>Create New Profile</DialogTitle>
</DialogHeader>
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>Create New Profile</DialogTitle>
</DialogHeader>
<div className="grid overflow-y-scroll flex-1 gap-6 py-4 min-h-0">
{/* Profile Name */}
<div className="grid gap-2">
<Label htmlFor="profile-name">Profile Name</Label>
<Input
id="profile-name"
value={profileName}
onChange={(e) => {
setProfileName(e.target.value);
}}
placeholder="Enter profile name"
className={nameError ? "border-red-500" : ""}
/>
{nameError && <p className="text-sm text-red-600">{nameError}</p>}
</div>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex flex-col flex-1 w-full min-h-0"
>
<TabsList className="grid flex-shrink-0 grid-cols-2 w-full">
<TabsTrigger value="regular">Regular Browsers</TabsTrigger>
<TabsTrigger value="anti-detect">Anti-Detect</TabsTrigger>
</TabsList>
{/* Browser Selection */}
<div className="grid gap-2">
<Label>Browser</Label>
<Select
value={selectedBrowser ?? undefined}
onValueChange={(value) => {
setSelectedBrowser(value as BrowserTypeString);
}}
disabled={isLoadingSupport}
>
<SelectTrigger>
<SelectValue
placeholder={
isLoadingSupport
? "Loading browsers..."
: "Select browser"
}
/>
</SelectTrigger>
<SelectContent>
{(
[
"mullvad-browser",
"firefox",
"firefox-developer",
"chromium",
"brave",
"zen",
"tor-browser",
] as BrowserTypeString[]
).map((browser) => {
const isSupported = isBrowserSupported(browser);
const displayName = getBrowserDisplayName(browser);
<ScrollArea className="flex-1 pr-6 h-[350px]">
<div className="py-4 space-y-6">
{/* Profile Name - Common to both tabs */}
<div className="space-y-2">
<Label htmlFor="profile-name">Profile Name</Label>
<Input
id="profile-name"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
placeholder="Enter profile name"
/>
</div>
if (!isSupported) {
return (
<Tooltip key={browser}>
<TooltipTrigger asChild>
<SelectItem
value={browser}
disabled={true}
className="opacity-50"
>
{displayName} (Not supported)
</SelectItem>
</TooltipTrigger>
<TooltipContent>
<p>
{displayName} is not supported on your current
platform or architecture.
</p>
</TooltipContent>
</Tooltip>
);
}
return (
<SelectItem key={browser} value={browser}>
{displayName}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
{selectedBrowser ? (
<div className="grid gap-2">
<Label>Release Type</Label>
{isLoadingReleaseTypes ? (
<div className="text-sm text-muted-foreground">
Loading release types...
</div>
) : Object.keys(releaseTypes).length === 0 ? (
<Alert>
<AlertDescription>
No releases are available for{" "}
{getBrowserDisplayName(selectedBrowser)}.
</AlertDescription>
</Alert>
) : (
<div className="space-y-4">
{(!releaseTypes.stable || !releaseTypes.nightly) && (
<Alert>
<AlertDescription>
Only {(releaseTypes.stable && "Stable") ?? "Nightly"}{" "}
releases are available for{" "}
{getBrowserDisplayName(selectedBrowser)}.
</AlertDescription>
</Alert>
)}
<ReleaseTypeSelector
selectedReleaseType={selectedReleaseType}
onReleaseTypeSelect={setSelectedReleaseType}
availableReleaseTypes={releaseTypes}
browser={selectedBrowser}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select release type..."
downloadedVersions={downloadedVersions}
<TabsContent value="regular" className="mt-0 space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<Label>Browser</Label>
<Combobox
options={browserOptions
.filter((browser) =>
supportedBrowsers.includes(browser.value),
)
.map((browser) => {
const IconComponent = getBrowserIcon(browser.value);
return {
value: browser.value,
label: browser.label,
icon: IconComponent,
};
})}
value={selectedBrowser || ""}
onValueChange={(value) =>
setSelectedBrowser(value as BrowserTypeString)
}
placeholder="Select a browser..."
searchPlaceholder="Search browsers..."
/>
</div>
)}
</div>
) : null}
{/* Proxy Settings */}
<div className="grid gap-4 pt-4 border-t">
<div className="grid gap-2">
<div className="flex justify-between items-center">
<Label>Proxy Settings</Label>
{!isProxyDisabled && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={handleCreateProxy}
className="flex gap-2 items-center"
>
<FiPlus className="w-4 h-4" />
Create Proxy
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Create a new proxy configuration</p>
</TooltipContent>
</Tooltip>
{selectedBrowser && (
<div className="space-y-3">
{!isBrowserVersionAvailable(selectedBrowser) &&
availableReleaseTypes.stable && (
<div className="flex gap-3 items-center">
<p className="text-sm text-muted-foreground">
Latest stable version (
{availableReleaseTypes.stable}) needs to be
downloaded
</p>
<LoadingButton
onClick={() => handleDownload(selectedBrowser)}
isLoading={isBrowserDownloading(selectedBrowser)}
size="sm"
disabled={isBrowserDownloading(selectedBrowser)}
>
Download
</LoadingButton>
</div>
)}
{isBrowserVersionAvailable(selectedBrowser) && (
<div className="text-sm text-green-600">
Latest stable version (
{availableReleaseTypes.stable}) is available
</div>
)}
</div>
)}
</div>
</TabsContent>
{isProxyDisabled ? (
<Tooltip>
<TooltipTrigger asChild>
<div className="p-3 bg-yellow-50 rounded-md border border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
Tor Browser has its own built-in proxy system and
doesn&apos;t support additional proxy configuration.
<TabsContent value="anti-detect" className="mt-0 space-y-6">
<div className="space-y-6">
{/* Camoufox Download Status */}
{!isBrowserVersionAvailable("camoufox") &&
camoufoxReleaseTypes.stable && (
<div className="flex gap-3 items-center p-3 bg-amber-50 rounded-md border border-amber-200">
<p className="text-sm text-amber-800">
Camoufox version ({camoufoxReleaseTypes.stable}) needs
to be downloaded
</p>
<LoadingButton
onClick={() => handleDownload("camoufox")}
isLoading={isBrowserDownloading("camoufox")}
size="sm"
disabled={isBrowserDownloading("camoufox")}
>
Download
</LoadingButton>
</div>
</TooltipTrigger>
<TooltipContent>
<p>
Tor Browser manages its own proxy routing automatically
</p>
</TooltipContent>
</Tooltip>
) : (
)}
{isBrowserVersionAvailable("camoufox") && (
<div className="p-3 text-sm text-green-600 bg-green-50 rounded-md border border-green-200">
Camoufox version ({camoufoxReleaseTypes.stable}) is
available
</div>
)}
<SharedCamoufoxConfigForm
config={camoufoxConfig}
onConfigChange={updateCamoufoxConfig}
/>
</div>
</TabsContent>
{/* Proxy Selection - Common to both tabs - Compact without card */}
{storedProxies.length > 0 && (
<div className="space-y-3">
<Label>Proxy</Label>
<Select
value={selectedProxyId ?? "none"}
onValueChange={(value) => {
setSelectedProxyId(value === "none" ? null : value);
}}
disabled={isLoadingProxies}
value={selectedProxyId || "none"}
onValueChange={(value) =>
setSelectedProxyId(value === "none" ? undefined : value)
}
>
<SelectTrigger>
<SelectValue
placeholder={
isLoadingProxies
? "Loading proxies..."
: "Select proxy (optional)"
}
/>
<SelectValue placeholder="No proxy" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No Proxy</SelectItem>
<SelectItem value="none">No proxy</SelectItem>
{storedProxies.map((proxy) => (
<SelectItem key={proxy.id} value={proxy.id}>
{proxy.name}
{proxy.name} ({proxy.proxy_settings.proxy_type}://
{proxy.proxy_settings.host}:
{proxy.proxy_settings.port})
</SelectItem>
))}
</SelectContent>
</Select>
)}
{!isProxyDisabled &&
storedProxies.length === 0 &&
!isLoadingProxies && (
<p className="text-sm text-muted-foreground">
No saved proxies available. Use the "Create Proxy" button
above to create proxy configurations.
</p>
)}
</div>
</div>
)}
</div>
</div>
</ScrollArea>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
<DialogFooter className="flex-shrink-0 pt-4 border-t">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<LoadingButton
onClick={handleCreate}
isLoading={isCreating}
onClick={() => void handleCreate()}
disabled={!canCreate}
disabled={isCreateDisabled()}
>
Create Profile
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
<ProxyFormDialog
isOpen={showProxyForm}
onClose={handleProxyFormClose}
onSave={handleProxySaved}
/>
</>
</Tabs>
</DialogContent>
</Dialog>
);
}
+16 -1
View File
@@ -58,6 +58,7 @@ interface ProfilesDataTableProps {
onDeleteProfile: (profile: BrowserProfile) => void | Promise<void>;
onRenameProfile: (oldName: string, newName: string) => Promise<void>;
onChangeVersion: (profile: BrowserProfile) => void;
onConfigureCamoufox?: (profile: BrowserProfile) => void;
runningProfiles: Set<string>;
isUpdating?: (browser: string) => boolean;
onReloadProxyData?: () => void | Promise<void>;
@@ -71,6 +72,7 @@ export function ProfilesDataTable({
onDeleteProfile,
onRenameProfile,
onChangeVersion,
onConfigureCamoufox,
runningProfiles,
isUpdating = () => false,
onReloadProxyData,
@@ -447,7 +449,19 @@ export function ProfilesDataTable({
>
Configure Proxy
</DropdownMenuItem>
{!["chromium", "zen"].includes(profile.browser) && (
{profile.browser === "camoufox" && onConfigureCamoufox && (
<DropdownMenuItem
onClick={() => {
onConfigureCamoufox(profile);
}}
disabled={!isClient || isBrowserUpdating}
>
Configure Camoufox
</DropdownMenuItem>
)}
{!["chromium", "zen", "camoufox"].includes(
profile.browser,
) && (
<DropdownMenuItem
onClick={() => {
onChangeVersion(profile);
@@ -492,6 +506,7 @@ export function ProfilesDataTable({
onKillProfile,
onProxySettings,
onChangeVersion,
onConfigureCamoufox,
getProxyInfo,
hasProxy,
getProxyDisplayName,
@@ -0,0 +1,568 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { CamoufoxConfig } from "@/types";
const osOptions = [
{ value: "windows", label: "Windows" },
{ value: "macos", label: "macOS" },
{ value: "linux", label: "Linux" },
];
const timezoneOptions = [
{ value: "America/New_York", label: "America/New_York" },
{ value: "America/Los_Angeles", label: "America/Los_Angeles" },
{ value: "America/Chicago", label: "America/Chicago" },
{ value: "America/Denver", label: "America/Denver" },
{ value: "America/Phoenix", label: "America/Phoenix" },
{ value: "America/Toronto", label: "America/Toronto" },
{ value: "America/Vancouver", label: "America/Vancouver" },
{ value: "Europe/London", label: "Europe/London" },
{ value: "Europe/Paris", label: "Europe/Paris" },
{ value: "Europe/Berlin", label: "Europe/Berlin" },
{ value: "Europe/Rome", label: "Europe/Rome" },
{ value: "Europe/Madrid", label: "Europe/Madrid" },
{ value: "Europe/Amsterdam", label: "Europe/Amsterdam" },
{ value: "Europe/Zurich", label: "Europe/Zurich" },
{ value: "Europe/Vienna", label: "Europe/Vienna" },
{ value: "Europe/Warsaw", label: "Europe/Warsaw" },
{ value: "Europe/Prague", label: "Europe/Prague" },
{ value: "Europe/Stockholm", label: "Europe/Stockholm" },
{ value: "Europe/Copenhagen", label: "Europe/Copenhagen" },
{ value: "Europe/Helsinki", label: "Europe/Helsinki" },
{ value: "Europe/Oslo", label: "Europe/Oslo" },
{ value: "Europe/Brussels", label: "Europe/Brussels" },
{ value: "Europe/Dublin", label: "Europe/Dublin" },
{ value: "Europe/Lisbon", label: "Europe/Lisbon" },
{ value: "Europe/Athens", label: "Europe/Athens" },
{ value: "Europe/Budapest", label: "Europe/Budapest" },
{ value: "Europe/Bucharest", label: "Europe/Bucharest" },
{ value: "Europe/Sofia", label: "Europe/Sofia" },
{ value: "Europe/Kiev", label: "Europe/Kiev" },
{ value: "Europe/Moscow", label: "Europe/Moscow" },
{ value: "Asia/Tokyo", label: "Asia/Tokyo" },
{ value: "Asia/Seoul", label: "Asia/Seoul" },
{ value: "Asia/Shanghai", label: "Asia/Shanghai" },
{ value: "Asia/Hong_Kong", label: "Asia/Hong_Kong" },
{ value: "Asia/Singapore", label: "Asia/Singapore" },
{ value: "Asia/Bangkok", label: "Asia/Bangkok" },
{ value: "Asia/Jakarta", label: "Asia/Jakarta" },
{ value: "Asia/Manila", label: "Asia/Manila" },
{ value: "Asia/Kolkata", label: "Asia/Kolkata" },
{ value: "Asia/Dubai", label: "Asia/Dubai" },
{ value: "Asia/Riyadh", label: "Asia/Riyadh" },
{ value: "Asia/Tehran", label: "Asia/Tehran" },
{ value: "Asia/Jerusalem", label: "Asia/Jerusalem" },
{ value: "Asia/Istanbul", label: "Asia/Istanbul" },
{ value: "Australia/Sydney", label: "Australia/Sydney" },
{ value: "Australia/Melbourne", label: "Australia/Melbourne" },
{ value: "Australia/Brisbane", label: "Australia/Brisbane" },
{ value: "Australia/Perth", label: "Australia/Perth" },
{ value: "Australia/Adelaide", label: "Australia/Adelaide" },
{ value: "Pacific/Auckland", label: "Pacific/Auckland" },
{ value: "Pacific/Honolulu", label: "Pacific/Honolulu" },
{ value: "Africa/Cairo", label: "Africa/Cairo" },
{ value: "Africa/Johannesburg", label: "Africa/Johannesburg" },
{ value: "Africa/Lagos", label: "Africa/Lagos" },
{ value: "Africa/Nairobi", label: "Africa/Nairobi" },
{ value: "America/Sao_Paulo", label: "America/Sao_Paulo" },
{ value: "America/Buenos_Aires", label: "America/Buenos_Aires" },
{ value: "America/Lima", label: "America/Lima" },
{ value: "America/Bogota", label: "America/Bogota" },
{ value: "America/Santiago", label: "America/Santiago" },
{ value: "America/Caracas", label: "America/Caracas" },
{ value: "America/Mexico_City", label: "America/Mexico_City" },
];
const localeOptions = [
{ value: "en-US", label: "English (US)" },
{ value: "en-GB", label: "English (UK)" },
{ value: "en-CA", label: "English (Canada)" },
{ value: "en-AU", label: "English (Australia)" },
{ value: "fr-FR", label: "French (France)" },
{ value: "fr-CA", label: "French (Canada)" },
{ value: "de-DE", label: "German (Germany)" },
{ value: "de-AT", label: "German (Austria)" },
{ value: "de-CH", label: "German (Switzerland)" },
{ value: "es-ES", label: "Spanish (Spain)" },
{ value: "es-MX", label: "Spanish (Mexico)" },
{ value: "es-AR", label: "Spanish (Argentina)" },
{ value: "it-IT", label: "Italian (Italy)" },
{ value: "it-CH", label: "Italian (Switzerland)" },
{ value: "pt-BR", label: "Portuguese (Brazil)" },
{ value: "pt-PT", label: "Portuguese (Portugal)" },
{ value: "ru-RU", label: "Russian (Russia)" },
{ value: "zh-CN", label: "Chinese (Simplified)" },
{ value: "zh-TW", label: "Chinese (Traditional)" },
{ value: "ja-JP", label: "Japanese (Japan)" },
{ value: "ko-KR", label: "Korean (Korea)" },
{ value: "ar-SA", label: "Arabic (Saudi Arabia)" },
{ value: "ar-EG", label: "Arabic (Egypt)" },
{ value: "hi-IN", label: "Hindi (India)" },
{ value: "tr-TR", label: "Turkish (Turkey)" },
{ value: "pl-PL", label: "Polish (Poland)" },
{ value: "nl-NL", label: "Dutch (Netherlands)" },
{ value: "nl-BE", label: "Dutch (Belgium)" },
{ value: "sv-SE", label: "Swedish (Sweden)" },
{ value: "da-DK", label: "Danish (Denmark)" },
{ value: "no-NO", label: "Norwegian (Norway)" },
{ value: "fi-FI", label: "Finnish (Finland)" },
{ value: "he-IL", label: "Hebrew (Israel)" },
{ value: "th-TH", label: "Thai (Thailand)" },
{ value: "vi-VN", label: "Vietnamese (Vietnam)" },
{ value: "id-ID", label: "Indonesian (Indonesia)" },
{ value: "ms-MY", label: "Malay (Malaysia)" },
{ value: "uk-UA", label: "Ukrainian (Ukraine)" },
{ value: "cs-CZ", label: "Czech (Czech Republic)" },
{ value: "sk-SK", label: "Slovak (Slovakia)" },
{ value: "hu-HU", label: "Hungarian (Hungary)" },
{ value: "ro-RO", label: "Romanian (Romania)" },
{ value: "bg-BG", label: "Bulgarian (Bulgaria)" },
{ value: "hr-HR", label: "Croatian (Croatia)" },
{ value: "sr-RS", label: "Serbian (Serbia)" },
{ value: "sl-SI", label: "Slovenian (Slovenia)" },
{ value: "lt-LT", label: "Lithuanian (Lithuania)" },
{ value: "lv-LV", label: "Latvian (Latvia)" },
{ value: "et-EE", label: "Estonian (Estonia)" },
{ value: "el-GR", label: "Greek (Greece)" },
{ value: "ca-ES", label: "Catalan (Spain)" },
{ value: "eu-ES", label: "Basque (Spain)" },
{ value: "gl-ES", label: "Galician (Spain)" },
{ value: "is-IS", label: "Icelandic (Iceland)" },
{ value: "mt-MT", label: "Maltese (Malta)" },
];
const getCurrentOS = () => {
if (typeof window !== "undefined") {
const userAgent = window.navigator.userAgent;
if (userAgent.includes("Win")) return "windows";
if (userAgent.includes("Mac")) return "macos";
if (userAgent.includes("Linux")) return "linux";
}
return "unknown";
};
interface SystemLocale {
locale: string;
language: string;
country: string;
}
interface SystemTimezone {
timezone: string;
offset: string;
}
interface SharedCamoufoxConfigFormProps {
config: CamoufoxConfig;
onConfigChange: (key: keyof CamoufoxConfig, value: unknown) => void;
className?: string;
}
export function SharedCamoufoxConfigForm({
config,
onConfigChange,
className = "",
}: SharedCamoufoxConfigFormProps) {
const [systemLocale, setSystemLocale] = useState<SystemLocale | null>(null);
const [systemTimezone, setSystemTimezone] = useState<SystemTimezone | null>(
null,
);
const [isLoadingSystemDefaults, setIsLoadingSystemDefaults] = useState(true);
// Load system defaults on component mount
useEffect(() => {
const loadSystemDefaults = async () => {
try {
const [locale, timezone] = await Promise.all([
invoke<SystemLocale>("get_system_locale"),
invoke<SystemTimezone>("get_system_timezone"),
]);
setSystemLocale(locale);
setSystemTimezone(timezone);
} catch (error) {
console.error("Failed to load system defaults:", error);
// Set fallback defaults
setSystemLocale({
locale: "en-US",
language: "en",
country: "US",
});
setSystemTimezone({
timezone: "America/New_York",
offset: "-05:00",
});
} finally {
setIsLoadingSystemDefaults(false);
}
};
loadSystemDefaults();
}, []);
// Get the selected OS for warning
const selectedOS = config.os?.[0];
const currentOS = getCurrentOS();
const showOSWarning =
selectedOS && selectedOS !== currentOS && currentOS !== "unknown";
return (
<div className={`space-y-6 ${className}`}>
{/* OS Selection */}
<div className="space-y-3">
<Label>Operating System</Label>
<Select
value={config.os?.[0] || getCurrentOS()}
onValueChange={(value) => onConfigChange("os", [value])}
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
{osOptions.map((os) => (
<SelectItem key={os.value} value={os.value}>
{os.label}
</SelectItem>
))}
</SelectContent>
</Select>
{showOSWarning && (
<p className="text-sm text-yellow-600 dark:text-yellow-400">
Selected OS ({selectedOS}) differs from your current OS (
{currentOS}). This may affect fingerprinting effectiveness.
</p>
)}
</div>
{/* Privacy & Blocking */}
<div className="space-y-3">
<Label>Privacy & Blocking</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="block-images"
checked={config.block_images || false}
onCheckedChange={(checked) =>
onConfigChange("block_images", checked)
}
/>
<Label htmlFor="block-images">Block Images</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webrtc"
checked={config.block_webrtc || false}
onCheckedChange={(checked) =>
onConfigChange("block_webrtc", checked)
}
/>
<Label htmlFor="block-webrtc">Block WebRTC</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webgl"
checked={config.block_webgl || false}
onCheckedChange={(checked) =>
onConfigChange("block_webgl", checked)
}
/>
<Label htmlFor="block-webgl">Block WebGL</Label>
</div>
</div>
</div>
{/* Geolocation */}
<div className="space-y-3">
<Label>Geolocation</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="country">Country</Label>
<Input
id="country"
value={config.country || ""}
onChange={(e) =>
onConfigChange("country", e.target.value || undefined)
}
placeholder={
systemLocale
? `e.g., ${systemLocale.country}`
: "e.g., US, GB, DE"
}
/>
</div>
<div className="space-y-2">
<Label>Timezone</Label>
<Select
value={config.timezone || "auto"}
onValueChange={(value) =>
onConfigChange("timezone", value === "auto" ? undefined : value)
}
>
<SelectTrigger>
<SelectValue
placeholder={
isLoadingSystemDefaults ? "Loading..." : "Select timezone"
}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">
{isLoadingSystemDefaults
? "Auto (loading...)"
: `Auto (${systemTimezone?.timezone || "UTC"})`}
</SelectItem>
{timezoneOptions.map((tz) => (
<SelectItem key={tz.value} value={tz.value}>
{tz.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="latitude">Latitude</Label>
<Input
id="latitude"
type="number"
step="any"
value={config.latitude || ""}
onChange={(e) =>
onConfigChange(
"latitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
placeholder="e.g., 40.7128"
/>
</div>
<div className="space-y-2">
<Label htmlFor="longitude">Longitude</Label>
<Input
id="longitude"
type="number"
step="any"
value={config.longitude || ""}
onChange={(e) =>
onConfigChange(
"longitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
placeholder="e.g., -74.0060"
/>
</div>
</div>
</div>
{/* Localization */}
<div className="space-y-3">
<Label>Locale</Label>
<Select
value={config.locale?.[0] || ""}
onValueChange={(value) =>
onConfigChange("locale", value ? [value] : undefined)
}
>
<SelectTrigger>
<SelectValue
placeholder={
isLoadingSystemDefaults
? "Loading..."
: `Select locale (system: ${systemLocale?.locale || "unknown"})`
}
/>
</SelectTrigger>
<SelectContent>
{!isLoadingSystemDefaults && systemLocale && (
<SelectItem value={systemLocale.locale}>
{systemLocale.locale} (System Default)
</SelectItem>
)}
{localeOptions.map((locale) => (
<SelectItem key={locale.value} value={locale.value}>
{locale.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Screen Resolution */}
<div className="space-y-3">
<Label>Screen Resolution</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="screen-min-width">Min Width</Label>
<Input
id="screen-min-width"
type="number"
value={config.screen_min_width || ""}
onChange={(e) =>
onConfigChange(
"screen_min_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1024"
/>
</div>
<div className="space-y-2">
<Label htmlFor="screen-max-width">Max Width</Label>
<Input
id="screen-max-width"
type="number"
value={config.screen_max_width || ""}
onChange={(e) =>
onConfigChange(
"screen_max_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1920"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="screen-min-height">Min Height</Label>
<Input
id="screen-min-height"
type="number"
value={config.screen_min_height || ""}
onChange={(e) =>
onConfigChange(
"screen_min_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 768"
/>
</div>
<div className="space-y-2">
<Label htmlFor="screen-max-height">Max Height</Label>
<Input
id="screen-max-height"
type="number"
value={config.screen_max_height || ""}
onChange={(e) =>
onConfigChange(
"screen_max_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1080"
/>
</div>
</div>
</div>
{/* Window Size */}
<div className="space-y-3">
<Label>Window Size</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="window-width">Width</Label>
<Input
id="window-width"
type="number"
value={config.window_width || ""}
onChange={(e) =>
onConfigChange(
"window_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1366"
/>
</div>
<div className="space-y-2">
<Label htmlFor="window-height">Height</Label>
<Input
id="window-height"
type="number"
value={config.window_height || ""}
onChange={(e) =>
onConfigChange(
"window_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 768"
/>
</div>
</div>
</div>
{/* Advanced Options */}
<div className="space-y-3">
<Label>Advanced Options</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="enable-cache"
checked={config.enable_cache || false}
onCheckedChange={(checked) =>
onConfigChange("enable_cache", checked)
}
/>
<Label htmlFor="enable-cache">Enable Browser Cache</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="main-world-eval"
checked={config.main_world_eval || false}
onCheckedChange={(checked) =>
onConfigChange("main_world_eval", checked)
}
/>
<Label htmlFor="main-world-eval">
Enable Main World Evaluation
</Label>
</div>
</div>
</div>
{/* WebGL Settings */}
<div className="space-y-3">
<Label>WebGL Settings</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="webgl-vendor">WebGL Vendor</Label>
<Input
id="webgl-vendor"
value={config.webgl_vendor || ""}
onChange={(e) =>
onConfigChange("webgl_vendor", e.target.value || undefined)
}
placeholder="e.g., Intel Inc."
/>
</div>
<div className="space-y-2">
<Label htmlFor="webgl-renderer">WebGL Renderer</Label>
<Input
id="webgl-renderer"
value={config.webgl_renderer || ""}
onChange={(e) =>
onConfigChange("webgl_renderer", e.target.value || undefined)
}
placeholder="e.g., Intel HD Graphics"
/>
</div>
</div>
</div>
</div>
);
}
+79
View File
@@ -19,6 +19,85 @@ import {
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
interface ComboboxOption {
value: string;
label: string;
description?: string;
}
interface ComboboxProps {
options: ComboboxOption[];
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
searchPlaceholder?: string;
className?: string;
}
export function Combobox({
options,
value,
onValueChange,
placeholder = "Select option...",
searchPlaceholder = "Search...",
className,
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-full justify-between", className)}
>
{value
? options.find((option) => option.value === value)?.label
: placeholder}
<LuChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
<CommandEmpty>No option found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
onValueChange(currentValue === value ? "" : currentValue);
setOpen(false);
}}
>
<LuCheck
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span>{option.label}</span>
{option.description && (
<span className="text-sm text-muted-foreground">
{option.description}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
const frameworks = [
{
value: "next.js",
+55
View File
@@ -0,0 +1,55 @@
"use client";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
+46 -29
View File
@@ -247,41 +247,58 @@ export function useBrowserDownload() {
// Listen for download progress events
useEffect(() => {
const unlisten = listen<DownloadProgress>("download-progress", (event) => {
const progress = event.payload;
setDownloadProgress(progress);
let unlistenFn: (() => void) | null = null;
const browserName = getBrowserDisplayName(progress.browser);
const setupListener = async () => {
try {
unlistenFn = await listen<DownloadProgress>(
"download-progress",
(event) => {
const progress = event.payload;
setDownloadProgress(progress);
// Show toast with progress
if (progress.stage === "downloading") {
const speedMBps = (
progress.speed_bytes_per_sec /
(1024 * 1024)
).toFixed(1);
const etaText = progress.eta_seconds
? formatTime(progress.eta_seconds)
: "calculating...";
const browserName = getBrowserDisplayName(progress.browser);
showDownloadToast(browserName, progress.version, "downloading", {
percentage: progress.percentage,
speed: speedMBps,
eta: etaText,
});
} else if (progress.stage === "extracting") {
showDownloadToast(browserName, progress.version, "extracting");
} else if (progress.stage === "verifying") {
showDownloadToast(browserName, progress.version, "verifying");
} else if (progress.stage === "completed") {
showDownloadToast(browserName, progress.version, "completed");
setDownloadProgress(null);
// Show toast with progress
if (progress.stage === "downloading") {
const speedMBps = (
progress.speed_bytes_per_sec /
(1024 * 1024)
).toFixed(1);
const etaText = progress.eta_seconds
? formatTime(progress.eta_seconds)
: "calculating...";
showDownloadToast(browserName, progress.version, "downloading", {
percentage: progress.percentage,
speed: speedMBps,
eta: etaText,
});
} else if (progress.stage === "extracting") {
showDownloadToast(browserName, progress.version, "extracting");
} else if (progress.stage === "verifying") {
showDownloadToast(browserName, progress.version, "verifying");
} else if (progress.stage === "completed") {
showDownloadToast(browserName, progress.version, "completed");
setDownloadProgress(null);
}
},
);
} catch (error) {
console.error("Failed to setup download progress listener:", error);
}
});
};
setupListener();
return () => {
void unlisten.then((fn) => {
fn();
});
if (unlistenFn) {
try {
unlistenFn();
} catch (error) {
console.error("Failed to cleanup download progress listener:", error);
}
}
};
}, [formatTime]);
+176 -136
View File
@@ -61,167 +61,207 @@ export function useVersionUpdater() {
// Listen for version update progress events
useEffect(() => {
const unlisten = listen<VersionUpdateProgress>(
"version-update-progress",
(event) => {
const progress = event.payload;
setUpdateProgress(progress);
let unlistenFn: (() => void) | null = null;
if (progress.status === "updating") {
setIsUpdating(true);
const setupListener = async () => {
try {
unlistenFn = await listen<VersionUpdateProgress>(
"version-update-progress",
(event) => {
const progress = event.payload;
setUpdateProgress(progress);
// Show unified progress toast
const currentBrowserName = progress.current_browser
? getBrowserDisplayName(progress.current_browser)
: undefined;
if (progress.status === "updating") {
setIsUpdating(true);
showUnifiedVersionUpdateToast("Checking for browser updates...", {
description: currentBrowserName
? `Fetching ${currentBrowserName} release information...`
: "Initializing version check...",
progress: {
current: progress.completed_browsers,
total: progress.total_browsers,
found: progress.new_versions_found,
current_browser: currentBrowserName,
},
});
} else if (progress.status === "completed") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
// Show unified progress toast
const currentBrowserName = progress.current_browser
? getBrowserDisplayName(progress.current_browser)
: undefined;
if (progress.new_versions_found > 0) {
showSuccessToast("Browser versions updated successfully", {
duration: 5000,
description:
"Auto-downloads will start shortly for available updates.",
});
} else {
showSuccessToast("No new browser versions found", {
duration: 3000,
description: "All browser versions are up to date",
});
}
showUnifiedVersionUpdateToast("Checking for browser updates...", {
description: currentBrowserName
? `Fetching ${currentBrowserName} release information...`
: "Initializing version check...",
progress: {
current: progress.completed_browsers,
total: progress.total_browsers,
found: progress.new_versions_found,
current_browser: currentBrowserName,
},
});
} else if (progress.status === "completed") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
// Refresh status
void loadUpdateStatus();
} else if (progress.status === "error") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
if (progress.new_versions_found > 0) {
showSuccessToast("Browser versions updated successfully", {
duration: 5000,
description:
"Auto-downloads will start shortly for available updates.",
});
} else {
showSuccessToast("No new browser versions found", {
duration: 3000,
description: "All browser versions are up to date",
});
}
showErrorToast("Failed to update browser versions", {
duration: 6000,
description: "Check your internet connection and try again",
});
}
},
);
// Refresh status
void loadUpdateStatus();
} else if (progress.status === "error") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
showErrorToast("Failed to update browser versions", {
duration: 6000,
description: "Check your internet connection and try again",
});
}
},
);
} catch (error) {
console.error(
"Failed to setup version update progress listener:",
error,
);
}
};
setupListener();
return () => {
void unlisten.then((fn) => {
fn();
});
if (unlistenFn) {
try {
unlistenFn();
} catch (error) {
console.error(
"Failed to cleanup version update progress listener:",
error,
);
}
}
};
}, [loadUpdateStatus]);
// Listen for browser auto-update events
useEffect(() => {
const unlisten = listen<AutoUpdateEvent>(
"browser-auto-update-available",
(event) => {
const handleAutoUpdate = async () => {
const { browser, new_version, notification_id } = event.payload;
console.log("Browser auto-update event received:", event.payload);
let unlistenFn: (() => void) | null = null;
const browserDisplayName = getBrowserDisplayName(browser);
const setupListener = async () => {
try {
unlistenFn = await listen<AutoUpdateEvent>(
"browser-auto-update-available",
(event) => {
const handleAutoUpdate = async () => {
const { browser, new_version, notification_id } = event.payload;
console.log("Browser auto-update event received:", event.payload);
try {
// Show auto-update start notification
showAutoUpdateToast(browserDisplayName, new_version, {
description: `Downloading ${browserDisplayName} ${new_version} automatically. Progress will be shown below.`,
});
const browserDisplayName = getBrowserDisplayName(browser);
// Dismiss the update notification in the backend
await invoke("dismiss_update_notification", {
notificationId: notification_id,
});
try {
// Show auto-update start notification
showAutoUpdateToast(browserDisplayName, new_version, {
description: `Downloading ${browserDisplayName} ${new_version} automatically. Progress will be shown below.`,
});
// Check if browser already exists before downloading
const isDownloaded = await invoke<boolean>("check_browser_exists", {
browserStr: browser,
version: new_version,
});
// Dismiss the update notification in the backend
await invoke("dismiss_update_notification", {
notificationId: notification_id,
});
if (isDownloaded) {
// Browser already exists, skip download and go straight to profile update
console.log(
`${browserDisplayName} ${new_version} already exists, skipping download`,
);
// Check if browser already exists before downloading
const isDownloaded = await invoke<boolean>(
"check_browser_exists",
{
browserStr: browser,
version: new_version,
},
);
showSuccessToast(
`${browserDisplayName} ${new_version} already available`,
{
description: "Updating profile configurations...",
duration: 3000,
},
);
} else {
// Download the browser - this will trigger download progress events automatically
await invoke("download_browser", {
browserStr: browser,
version: new_version,
});
}
if (isDownloaded) {
// Browser already exists, skip download and go straight to profile update
console.log(
`${browserDisplayName} ${new_version} already exists, skipping download`,
);
// Complete the update with auto-update of profile versions
const updatedProfiles = await invoke<string[]>(
"complete_browser_update_with_auto_update",
{
browser,
newVersion: new_version,
},
);
showSuccessToast(
`${browserDisplayName} ${new_version} already available`,
{
description: "Updating profile configurations...",
duration: 3000,
},
);
} else {
// Download the browser - this will trigger download progress events automatically
await invoke("download_browser", {
browserStr: browser,
version: new_version,
});
}
// Show success message based on whether profiles were updated
if (updatedProfiles.length > 0) {
const profileText =
updatedProfiles.length === 1
? `Profile "${updatedProfiles[0]}" has been updated`
: `${updatedProfiles.length} profiles have been updated`;
// Complete the update with auto-update of profile versions
const updatedProfiles = await invoke<string[]>(
"complete_browser_update_with_auto_update",
{
browser,
newVersion: new_version,
},
);
showSuccessToast(`${browserDisplayName} update completed`, {
description: `${profileText} to version ${new_version}. You can now launch your browsers with the latest version.`,
duration: 6000,
});
} else {
showSuccessToast(`${browserDisplayName} update completed`, {
description: `Version ${new_version} is now available. Running profiles will use the new version when restarted.`,
duration: 6000,
});
}
} catch (error) {
console.error("Failed to handle browser auto-update:", error);
showErrorToast(`Failed to auto-update ${browserDisplayName}`, {
description:
error instanceof Error
? error.message
: "Unknown error occurred",
duration: 8000,
});
}
};
// Show success message based on whether profiles were updated
if (updatedProfiles.length > 0) {
const profileText =
updatedProfiles.length === 1
? `Profile "${updatedProfiles[0]}" has been updated`
: `${updatedProfiles.length} profiles have been updated`;
// Call the async handler
void handleAutoUpdate();
},
);
showSuccessToast(`${browserDisplayName} update completed`, {
description: `${profileText} to version ${new_version}. You can now launch your browsers with the latest version.`,
duration: 6000,
});
} else {
showSuccessToast(`${browserDisplayName} update completed`, {
description: `Version ${new_version} is now available. Running profiles will use the new version when restarted.`,
duration: 6000,
});
}
} catch (error) {
console.error("Failed to handle browser auto-update:", error);
showErrorToast(`Failed to auto-update ${browserDisplayName}`, {
description:
error instanceof Error
? error.message
: "Unknown error occurred",
duration: 8000,
});
}
};
// Call the async handler
void handleAutoUpdate();
},
);
} catch (error) {
console.error("Failed to setup browser auto-update listener:", error);
}
};
setupListener();
return () => {
void unlisten.then((fn) => {
fn();
});
if (unlistenFn) {
try {
unlistenFn();
} catch (error) {
console.error(
"Failed to cleanup browser auto-update listener:",
error,
);
}
}
};
}, []);
+4 -1
View File
@@ -3,7 +3,7 @@
* Centralized helpers for browser name mapping, icons, etc.
*/
import { FaChrome, FaFirefox } from "react-icons/fa";
import { FaChrome, FaFirefox, FaShieldAlt } from "react-icons/fa";
import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si";
import { ZenBrowser } from "@/components/icons/zen-browser";
@@ -19,6 +19,7 @@ export function getBrowserDisplayName(browserType: string): string {
brave: "Brave",
chromium: "Chromium",
"tor-browser": "Tor Browser",
camoufox: "Anti-Detect",
};
return browserNames[browserType] || browserType;
@@ -42,6 +43,8 @@ export function getBrowserIcon(browserType: string) {
return ZenBrowser;
case "tor-browser":
return SiTorbrowser;
case "camoufox":
return FaShieldAlt;
default:
return null;
}
+47
View File
@@ -20,6 +20,7 @@ export interface BrowserProfile {
process_id?: number;
last_launch?: number;
release_type: string; // "stable" or "nightly"
camoufox_config?: CamoufoxConfig; // Camoufox configuration
}
export interface StoredProxy {
@@ -56,3 +57,49 @@ export interface AppUpdateProgress {
eta?: string; // estimated time remaining
message: string;
}
export interface CamoufoxConfig {
os?: string[];
block_images?: boolean;
block_webrtc?: boolean;
block_webgl?: boolean;
disable_coop?: boolean;
geoip?: string | boolean;
country?: string;
timezone?: string;
latitude?: number;
longitude?: number;
humanize?: boolean;
humanize_duration?: number;
headless?: boolean;
locale?: string[];
addons?: string[];
fonts?: string[];
custom_fonts_only?: boolean;
exclude_addons?: string[];
screen_min_width?: number;
screen_max_width?: number;
screen_min_height?: number;
screen_max_height?: number;
window_width?: number;
window_height?: number;
ff_version?: number;
main_world_eval?: boolean;
webgl_vendor?: string;
webgl_renderer?: string;
proxy?: string;
enable_cache?: boolean;
virtual_display?: string;
debug?: boolean;
additional_args?: string[];
env_vars?: Record<string, string>;
firefox_prefs?: Record<string, unknown>;
}
export interface CamoufoxLaunchResult {
id: string;
pid?: number;
executable_path: string;
profile_path: string;
url?: string;
}