mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-02 13:21:36 +02:00
feat: add anti-detect functionality
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user