"use client"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { FaApple, FaLinux, FaWindows } from "react-icons/fa"; import { LuChevronRight, LuClipboard, LuClipboardCheck, LuCookie, LuCopy, LuFingerprint, LuGlobe, LuGroup, LuKey, LuLink, LuLockOpen, LuPlus, LuPuzzle, LuRefreshCw, LuSettings, LuShield, LuShieldCheck, LuTrash2, LuUsers, LuX, } from "react-icons/lu"; import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { WayfernConfigForm } from "@/components/wayfern-config-form"; import { translateBackendError } from "@/lib/backend-errors"; import { getProfileIcon } from "@/lib/browser-utils"; import { formatRelativeTime } from "@/lib/flag-utils"; import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; import { cn } from "@/lib/utils"; import type { BrowserProfile, CamoufoxConfig, ProfileGroup, StoredProxy, VpnConfig, WayfernConfig, } from "@/types"; interface ProfileInfoDialogProps { isOpen: boolean; onClose: () => void; profile: BrowserProfile | null; storedProxies: StoredProxy[]; vpnConfigs: VpnConfig[]; onOpenTrafficDialog?: (profileId: string) => void; onOpenProfileSyncDialog?: (profile: BrowserProfile) => void; onAssignProfilesToGroup?: (profileIds: string[]) => void; onConfigureCamoufox?: (profile: BrowserProfile) => void; onCopyCookiesToProfile?: (profile: BrowserProfile) => void; onOpenCookieManagement?: (profile: BrowserProfile) => void; onAssignExtensionGroup?: (profileIds: string[]) => void; onOpenBypassRules?: (profile: BrowserProfile) => void; onOpenDnsBlocklist?: (profile: BrowserProfile) => void; onOpenLaunchHook?: (profile: BrowserProfile) => void; onCloneProfile?: (profile: BrowserProfile) => void; onDeleteProfile?: (profile: BrowserProfile) => void; onLaunchWithSync?: (profile: BrowserProfile) => void; onSetPassword?: (profile: BrowserProfile) => void; onChangePassword?: (profile: BrowserProfile) => void; onRemovePassword?: (profile: BrowserProfile) => void; crossOsUnlocked?: boolean; isRunning?: boolean; isDisabled?: boolean; isCrossOs?: boolean; syncStatuses: Record; } function _OSIcon({ os }: { os: string }) { switch (os) { case "macos": return ; case "windows": return ; case "linux": return ; default: return null; } } function InfoCard({ label, value }: { label: string; value: string }) { return (

{label}

{value}

); } function formatBytes(bytes: number): string { if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } /** * Shows the total bytes routed through Donut's local proxy worker for this * profile. Only counts traffic flowing through the donut-proxy binary — not * the browser's full network usage, hence the "Local" qualifier. */ function LocalDataTransferCard({ profileId, t, }: { profileId: string; t: (key: string, options?: Record) => string; }) { type Snapshot = { total_bytes_sent: number; total_bytes_received: number; }; const [value, setValue] = React.useState("—"); React.useEffect(() => { let mounted = true; const fetchSnapshot = async () => { try { const snap = await invoke( "get_profile_traffic_snapshot", { profileId }, ); if (!mounted) return; if (!snap) { setValue("0 B"); return; } setValue( formatBytes(snap.total_bytes_sent + snap.total_bytes_received), ); } catch { if (mounted) setValue("—"); } }; void fetchSnapshot(); const interval = window.setInterval(fetchSnapshot, 5000); return () => { mounted = false; window.clearInterval(interval); }; }, [profileId]); return ( ); } export function ProfileInfoDialog({ isOpen, onClose, profile, storedProxies, vpnConfigs, onOpenTrafficDialog, onOpenProfileSyncDialog, onAssignProfilesToGroup, onConfigureCamoufox, onCopyCookiesToProfile, onOpenCookieManagement, onAssignExtensionGroup, onOpenBypassRules, onOpenDnsBlocklist, onOpenLaunchHook, onCloneProfile, onDeleteProfile, onLaunchWithSync, onSetPassword, onChangePassword, onRemovePassword, crossOsUnlocked = false, isRunning = false, isDisabled = false, isCrossOs = false, syncStatuses, }: ProfileInfoDialogProps) { const { t } = useTranslation(); const [copied, setCopied] = React.useState(false); const [groupName, setGroupName] = React.useState(null); const [extensionGroupName, setExtensionGroupName] = React.useState< string | null >(null); React.useEffect(() => { if (!isOpen || !profile?.group_id) { setGroupName(null); return; } void (async () => { try { const groups = await invoke("get_groups"); const group = groups.find((g) => g.id === profile.group_id); setGroupName(group?.name ?? null); } catch { setGroupName(null); } })(); }, [isOpen, profile?.group_id]); React.useEffect(() => { if (!isOpen || !profile?.extension_group_id) { setExtensionGroupName(null); return; } void (async () => { try { const group = await invoke<{ name: string } | null>( "get_extension_group_for_profile", { profileId: profile.id }, ); setExtensionGroupName(group?.name ?? null); } catch { setExtensionGroupName(null); } })(); }, [isOpen, profile?.extension_group_id, profile?.id]); React.useEffect(() => { if (!isOpen) { setCopied(false); } }, [isOpen]); if (!profile) return null; const ProfileIcon = getProfileIcon(profile); const isCamoufoxOrWayfern = profile.browser === "camoufox" || profile.browser === "wayfern"; const isDeleteDisabled = isRunning; const proxyName = profile.proxy_id ? storedProxies.find((p) => p.id === profile.proxy_id)?.name : null; const vpnName = profile.vpn_id ? vpnConfigs.find((v) => v.id === profile.vpn_id)?.name : null; const networkLabel = vpnName ? `VPN: ${vpnName}` : proxyName ? `Proxy: ${proxyName}` : t("profileInfo.values.none"); const syncStatus = syncStatuses[profile.id]; const syncMode = profile.sync_mode ?? "Disabled"; const handleCopyId = async () => { try { await navigator.clipboard.writeText(profile.id); setCopied(true); setTimeout(() => { setCopied(false); }, 2000); } catch { // ignore } }; const handleAction = (action: () => void) => { onClose(); action(); }; const hasTags = profile.tags && profile.tags.length > 0; const hasNote = !!profile.note; // Items in the settings tab `actions` list MUST only open another dialog // (or trigger a navigation/action that closes this one). Do NOT put inline // settings UI — inputs, toggles, save buttons — directly in this dialog's // settings tab. Each setting belongs in its own focused dialog (see // `ProfileLaunchHookDialog`, `ProfileBypassRulesDialog`, // `ProfileDnsBlocklistDialog` for the pattern). The settings tab is purely // a navigation hub. interface ActionItem { icon: React.ReactNode; label: string; onClick: () => void; disabled?: boolean; destructive?: boolean; proBadge?: boolean; runningBadge?: boolean; hidden?: boolean; } const actions: ActionItem[] = [ { icon: , label: t("profiles.actions.viewNetwork"), onClick: () => { handleAction(() => onOpenTrafficDialog?.(profile.id)); }, disabled: isCrossOs, }, { icon: , label: t("profiles.actions.syncSettings"), onClick: () => { handleAction(() => onOpenProfileSyncDialog?.(profile)); }, disabled: isCrossOs, hidden: profile.ephemeral === true, }, { icon: , label: t("profiles.actions.assignToGroup"), onClick: () => { handleAction(() => onAssignProfilesToGroup?.([profile.id])); }, disabled: isDisabled, runningBadge: isRunning, }, { icon: , label: t("profiles.actions.changeFingerprint"), onClick: () => { handleAction(() => onConfigureCamoufox?.(profile)); }, disabled: isDisabled, runningBadge: isRunning, hidden: !isCamoufoxOrWayfern || !onConfigureCamoufox, }, { icon: , label: t("profiles.synchronizer.launchWithSync"), onClick: () => { handleAction(() => onLaunchWithSync?.(profile)); }, disabled: isDisabled || isRunning || !crossOsUnlocked, proBadge: !crossOsUnlocked, hidden: profile.browser !== "wayfern" || !onLaunchWithSync, }, { icon: , label: t("profiles.actions.copyCookiesToProfile"), onClick: () => { handleAction(() => onCopyCookiesToProfile?.(profile)); }, disabled: isDisabled, runningBadge: isRunning, hidden: !isCamoufoxOrWayfern || profile.ephemeral === true || !onCopyCookiesToProfile, }, { icon: , label: t("profileInfo.actions.manageCookies"), onClick: () => { handleAction(() => onOpenCookieManagement?.(profile)); }, disabled: isDisabled, runningBadge: isRunning, hidden: !isCamoufoxOrWayfern || profile.ephemeral === true || !onOpenCookieManagement, }, { icon: , label: t("profiles.actions.clone"), onClick: () => { handleAction(() => onCloneProfile?.(profile)); }, disabled: isDisabled, runningBadge: isRunning, hidden: profile.ephemeral === true, }, { icon: , label: t("profileInfo.actions.assignExtensionGroup"), onClick: () => { handleAction(() => onAssignExtensionGroup?.([profile.id])); }, disabled: isDisabled, runningBadge: isRunning, hidden: profile.ephemeral === true, }, { icon: , label: t("profileInfo.network.bypassRulesTitle"), onClick: () => { handleAction(() => onOpenBypassRules?.(profile)); }, }, { icon: , label: t("dnsBlocklist.title"), onClick: () => { handleAction(() => onOpenDnsBlocklist?.(profile)); }, }, { icon: , label: t("profiles.actions.launchHook"), onClick: () => { handleAction(() => onOpenLaunchHook?.(profile)); }, hidden: !onOpenLaunchHook, }, { icon: , label: t("profiles.actions.setPassword"), onClick: () => { handleAction(() => onSetPassword?.(profile)); }, disabled: isDisabled || isRunning, runningBadge: isRunning, hidden: profile.password_protected === true || profile.ephemeral === true || !onSetPassword, }, { icon: , label: t("profiles.actions.changePassword"), onClick: () => { handleAction(() => onChangePassword?.(profile)); }, disabled: isDisabled || isRunning, runningBadge: isRunning, hidden: profile.password_protected !== true || !onChangePassword, }, { icon: , label: t("profiles.actions.removePassword"), onClick: () => { handleAction(() => onRemovePassword?.(profile)); }, disabled: isDisabled || isRunning, runningBadge: isRunning, hidden: profile.password_protected !== true || !onRemovePassword, destructive: true, }, { icon: , label: t("profiles.actions.delete"), onClick: () => { handleAction(() => onDeleteProfile?.(profile)); }, disabled: isDeleteDisabled, destructive: true, }, ]; const visibleActions = actions.filter((a) => !a.hidden); return ( { if (!open) onClose(); }} > ); } interface ProfileInfoLayoutProps { profile: BrowserProfile; ProfileIcon: React.ComponentType<{ className?: string }>; isRunning: boolean; isDisabled: boolean; networkLabel: string; groupName: string | null; extensionGroupName: string | null; syncMode: string; syncStatus: { status: string; error?: string } | undefined; hasTags: boolean | undefined; hasNote: boolean; copied: boolean; storedProxies: StoredProxy[]; vpnConfigs: VpnConfig[]; handleCopyId: () => Promise; onClose: () => void; onCloneProfile?: (profile: BrowserProfile) => void; onKillProfile?: (profile: BrowserProfile) => void; visibleActions: { icon: React.ReactNode; label: string; onClick: () => void; disabled?: boolean; destructive?: boolean; proBadge?: boolean; runningBadge?: boolean; }[]; t: (key: string, options?: Record) => string; } type ProfileSection = | "overview" | "fingerprint" | "network" | "cookies" | "extensions" | "sync" | "automation" | "security" | "delete"; function ProfileInfoLayout({ profile, ProfileIcon, isRunning, isDisabled, networkLabel, groupName, extensionGroupName, syncMode, syncStatus, storedProxies, vpnConfigs, hasTags, hasNote, copied, handleCopyId, onClose, onCloneProfile, visibleActions, t, }: ProfileInfoLayoutProps) { const [section, setSection] = React.useState("overview"); // Map sidebar items to existing action labels, so clicking a section // simply triggers the existing dialog handler. const findAction = React.useCallback( (substr: string) => visibleActions.find((a) => a.label.toLowerCase().includes(substr)), [visibleActions], ); const deleteAction = findAction("delete"); const fingerprintAction = findAction("fingerprint"); const cookiesAction = findAction("manage cookies") ?? findAction("copy cookies"); const extensionAction = findAction("extension"); const syncAction = findAction("sync"); const _launchHookAction = findAction("hook") ?? findAction("launch hook"); const _networkAction = findAction("network"); // Password actions are no longer routed via the legacy action handlers — // SecuritySectionInline writes directly to the backend instead. // Cookie count is fetched at the layout level so the sidebar badge can // surface it without waiting for the user to open the Cookies section. // The effect deps must be primitive — `cookiesAction` is a new object // every render and using it directly here caused an infinite re-render // loop that froze the entire app when the dialog opened. // Skipped while running: the cookie DB is held by the browser and we // don't want to compete for its lock from the badge fetch. const cookiesSupported = !!cookiesAction; const [cookieCount, setCookieCount] = React.useState(null); React.useEffect(() => { if (!cookiesSupported || isRunning) { setCookieCount(null); return; } let mounted = true; void (async () => { try { const data = await invoke<{ total_count: number }>( "get_profile_cookie_stats", { profileId: profile.id }, ); if (mounted) setCookieCount(data.total_count); } catch { if (mounted) setCookieCount(null); } })(); return () => { mounted = false; }; }, [profile.id, cookiesSupported, isRunning]); const sidebarItems: { id: ProfileSection; icon: React.ReactNode; label: string; badge?: string; destructive?: boolean; hidden?: boolean; }[] = [ { id: "overview", icon: , label: t("profileInfo.sections.overview"), }, { id: "fingerprint", icon: , label: t("profileInfo.sections.fingerprint"), badge: profile.password_protected ? t("profileInfo.badges.locked") : undefined, hidden: !fingerprintAction, }, { id: "network", icon: , label: t("profileInfo.sections.network"), badge: profile.proxy_id || profile.vpn_id ? networkLabel : undefined, }, { id: "cookies", icon: , label: t("profileInfo.sections.cookies"), badge: cookieCount !== null && cookieCount > 0 ? cookieCount.toLocaleString() : undefined, hidden: !cookiesAction, }, { id: "extensions", icon: , label: t("profileInfo.sections.extensions"), badge: extensionGroupName ?? undefined, hidden: !extensionAction, }, { id: "sync", icon: , label: t("profileInfo.sections.sync"), hidden: !syncAction, }, { id: "automation", icon: , label: t("profileInfo.sections.launchHook"), badge: profile.launch_hook ? t("profileInfo.badges.active") : undefined, }, { id: "security", icon: , label: t("profileInfo.sections.security"), }, ]; return ( <> {/* Top bar */}
{t("profileInfo.breadcrumbRoot")} / {profile.name}
{onCloneProfile && ( )}
{/* Body */}
{/* Sidebar */} {/* Main */}
{section === "overview" && (
{/* Hero */}

{profile.name}

{profile.version}
{/* ID */}
ID {profile.id}
{/* 2x2 cards */}
{/* Activity */}
{t("profileInfo.sections.activity")}
{profile.created_by_email && (

{t("sync.team.title")}

{t("sync.team.createdBy", { email: profile.created_by_email, })}

)}
)} {section === "fingerprint" && ( )} {section === "network" && ( )} {section === "cookies" && ( )} {section === "extensions" && ( )} {section === "sync" && ( )} {section === "automation" && ( )} {section === "security" && ( )}
); } function _SectionPlaceholder({ icon, title, description, actionLabel, onAction, disabled, hint, }: { icon: React.ReactNode; title: string; description: string; actionLabel: string; onAction: () => void; disabled?: boolean; hint?: string; }) { return (
{icon} {title}

{description}

{hint && (
{hint}
)}
); } function _SectionAction({ icon, label, onClick, disabled, destructive, }: { icon: React.ReactNode; label: string; onClick: () => void; disabled?: boolean; destructive?: boolean; }) { return ( ); } function isValidHttpUrl(value: string): boolean { const trimmed = value.trim(); if (!trimmed) return false; try { const u = new URL(trimmed); return u.protocol === "http:" || u.protocol === "https:"; } catch { return false; } } function LaunchHookEditor({ profile, t, }: { profile: BrowserProfile; t: (key: string, options?: Record) => string; }) { const { t: tFn } = useTranslation(); const [value, setValue] = React.useState(profile.launch_hook ?? ""); const [isSaving, setIsSaving] = React.useState(false); const [error, setError] = React.useState(null); const initial = profile.launch_hook ?? ""; const dirty = value !== initial; const trimmed = value.trim(); const showInvalidHint = trimmed.length > 0 && !isValidHttpUrl(trimmed); const onSave = async () => { setIsSaving(true); setError(null); try { await invoke("update_profile_launch_hook", { profileId: profile.id, launchHook: trimmed ? trimmed : null, }); } catch (e) { setError(translateBackendError(tFn, e)); } finally { setIsSaving(false); } }; return (
{t("profileInfo.sections.launchHook")}

{t("profileInfo.sectionDesc.launchHook")}

{ setValue(e.target.value); }} placeholder={t("profiles.launchHook.placeholder")} className="text-xs font-mono" /> {showInvalidHint && (

{t("profileInfo.launchHook.invalidUrlHint")}

)} {error &&

{error}

}
{dirty && ( )}
); } function SyncSectionInline({ profile, syncMode, syncStatus, isDisabled, t, }: { profile: BrowserProfile; syncMode: string; syncStatus: { status: string; error?: string } | undefined; isDisabled: boolean; t: (key: string, options?: Record) => string; }) { const [isSaving, setIsSaving] = React.useState(false); const [error, setError] = React.useState(null); const onChangeMode = async (mode: string) => { setIsSaving(true); setError(null); try { await invoke("set_profile_sync_mode", { profileId: profile.id, syncMode: mode, }); } catch (e) { setError(String(e)); } finally { setIsSaving(false); } }; return (
{t("profileInfo.sections.sync")}

{t("profileInfo.sectionDesc.sync")}

{t("profileInfo.fields.syncMode")}
{syncStatus && (

{t("profileInfo.fields.syncStatus")}

{syncStatus.status}

{syncStatus.error && (

{syncStatus.error}

)}
)} {error &&

{error}

}
); } function NetworkSectionInline({ profile, storedProxies, vpnConfigs, isDisabled, t, }: { profile: BrowserProfile; storedProxies: StoredProxy[]; vpnConfigs: VpnConfig[]; isDisabled: boolean; t: (key: string, options?: Record) => string; }) { const [isSaving, setIsSaving] = React.useState(false); const [error, setError] = React.useState(null); // Track the effective selection so the dropdown reflects the last save // even before the parent `profile` prop refreshes from a backend event. const [proxyId, setProxyId] = React.useState( profile.proxy_id ?? null, ); const [vpnId, setVpnId] = React.useState( profile.vpn_id ?? null, ); React.useEffect(() => { setProxyId(profile.proxy_id ?? null); setVpnId(profile.vpn_id ?? null); }, [profile.proxy_id, profile.vpn_id]); const onProxyChange = async (value: string) => { const nextId = value === "__none__" ? null : value; setIsSaving(true); setError(null); try { await invoke("update_profile_proxy", { profileId: profile.id, proxyId: nextId, }); // Clearing the proxy implicitly clears any VPN binding too on the // backend, but we mirror it locally for an immediate visual. setProxyId(nextId); if (nextId !== null) setVpnId(null); } catch (e) { setError(String(e)); } finally { setIsSaving(false); } }; const onVpnChange = async (value: string) => { const nextId = value === "__none__" ? null : value; setIsSaving(true); setError(null); try { await invoke("update_profile_vpn", { profileId: profile.id, vpnId: nextId, }); setVpnId(nextId); if (nextId !== null) setProxyId(null); } catch (e) { setError(String(e)); } finally { setIsSaving(false); } }; return (
{t("profileInfo.sections.network")}

{t("profileInfo.sectionDesc.network")}

{t("profileInfo.fields.proxy")}
{t("profileInfo.fields.vpn")}
{error &&

{error}

}
); } function ExtensionsSectionInline({ profile, isDisabled, t, }: { profile: BrowserProfile; isDisabled: boolean; t: (key: string, options?: Record) => string; }) { type ExtensionGroupOption = { id: string; name: string }; const [groups, setGroups] = React.useState([]); const [groupId, setGroupId] = React.useState( profile.extension_group_id ?? null, ); const [isSaving, setIsSaving] = React.useState(false); const [error, setError] = React.useState(null); React.useEffect(() => { setGroupId(profile.extension_group_id ?? null); }, [profile.extension_group_id]); React.useEffect(() => { let mounted = true; let unlisten: (() => void) | undefined; const load = async () => { try { const data = await invoke( "list_extension_groups", ); if (mounted) setGroups(data); } catch (e) { if (mounted) setError(String(e)); } }; void load(); void listen("extensions-changed", () => { void load(); }).then((u) => { if (mounted) unlisten = u; else u(); }); return () => { mounted = false; unlisten?.(); }; }, []); const onChange = async (value: string) => { const next = value === "__none__" ? null : value; setIsSaving(true); setError(null); try { await invoke("assign_extension_group_to_profile", { profileId: profile.id, extensionGroupId: next, }); setGroupId(next); } catch (e) { setError(String(e)); } finally { setIsSaving(false); } }; return (
{t("profileInfo.sections.extensions")}

{t("profileInfo.sectionDesc.extensions")}

{t("profileInfo.fields.extensionGroup")}
{error &&

{error}

}
); } function CookiesSectionInline({ profile, isRunning, t, }: { profile: BrowserProfile; isRunning: boolean; isDisabled: boolean; t: (key: string, options?: Record) => string; }) { type CookieStats = { profile_id: string; browser_type: string; total_count: number; domains: { domain: string; count: number }[]; }; const [stats, setStats] = React.useState(null); const [isLoading, setIsLoading] = React.useState(!isRunning); const [error, setError] = React.useState(null); React.useEffect(() => { if (isRunning) { setStats(null); setIsLoading(false); setError(null); return; } let mounted = true; setIsLoading(true); setError(null); void (async () => { try { const data = await invoke("get_profile_cookie_stats", { profileId: profile.id, }); if (mounted) setStats(data); } catch (e) { if (mounted) setError(translateBackendError(t as never, e)); } finally { if (mounted) setIsLoading(false); } })(); return () => { mounted = false; }; }, [profile.id, isRunning, t]); const domains = stats?.domains ?? []; return (
{t("profileInfo.sections.cookies")}

{t("profileInfo.sectionDesc.cookies")}

{isRunning ? (

{t("profileInfo.cookies.runningNotice")}

) : ( <>

{t("profileInfo.fields.cookieCount")}

{isLoading ? t("profileInfo.values.loading") : stats ? stats.total_count.toLocaleString() : "—"}

{domains.length > 0 && (

{t("profileInfo.cookies.domainsHeader", { count: domains.length, })}

    {domains.map((d) => (
  • {d.domain} {d.count}
  • ))}
)} {error &&

{error}

} )}
); } // Inline password set / change / remove form. Replaces three separate // nested modal dialogs with one in-page form that branches on the current // `password_protected` state of the profile. // Inline fingerprint editor. Reuses SharedCamoufoxConfigForm (Camoufox/Firefox // engine) and WayfernConfigForm (Chromium engine) so the same field set as // the standalone dialog is available without opening a nested modal. function FingerprintSectionInline({ profile, isDisabled, crossOsUnlocked, t, }: { profile: BrowserProfile; isDisabled: boolean; crossOsUnlocked: boolean; t: (key: string, options?: Record) => string; }) { const [camoufoxConfig, setCamoufoxConfig] = React.useState( () => profile.camoufox_config ?? {}, ); const [wayfernConfig, setWayfernConfig] = React.useState( () => profile.wayfern_config ?? {}, ); const [isSaving, setIsSaving] = React.useState(false); const [error, setError] = React.useState(null); const [success, setSuccess] = React.useState(null); // When the underlying profile changes (e.g. a different profile is opened // in the dialog) reset the local form state to match. React.useEffect(() => { setCamoufoxConfig(profile.camoufox_config ?? {}); setWayfernConfig(profile.wayfern_config ?? {}); setError(null); setSuccess(null); }, [profile.camoufox_config, profile.wayfern_config]); const isCamoufox = profile.browser === "camoufox"; const isWayfern = profile.browser === "wayfern"; if (!isCamoufox && !isWayfern) { return (
{t("profileInfo.sections.fingerprint")}

{t("profileInfo.fingerprint.notSupported")}

); } const onCamoufoxChange = (key: keyof CamoufoxConfig, value: unknown) => { setCamoufoxConfig((prev) => ({ ...prev, [key]: value })); setSuccess(null); }; const onWayfernChange = (key: keyof WayfernConfig, value: unknown) => { setWayfernConfig((prev) => ({ ...prev, [key]: value })); setSuccess(null); }; const onSave = async () => { setIsSaving(true); setError(null); setSuccess(null); try { if (isCamoufox) { await invoke("update_camoufox_config", { profileId: profile.id, config: camoufoxConfig, }); } else { await invoke("update_wayfern_config", { profileId: profile.id, config: wayfernConfig, }); } setSuccess(t("common.buttons.saved")); } catch (e) { setError(String(e)); } finally { setIsSaving(false); } }; const initial = isCamoufox ? JSON.stringify(profile.camoufox_config ?? {}) : JSON.stringify(profile.wayfern_config ?? {}); const current = isCamoufox ? JSON.stringify(camoufoxConfig) : JSON.stringify(wayfernConfig); const dirty = current !== initial; return (
{t("profileInfo.sections.fingerprint")}

{t("profileInfo.sectionDesc.fingerprint")}

{isCamoufox && ( )} {isWayfern && ( )} {error &&

{error}

} {success && !error &&

{success}

}
{dirty && ( )}
); } function SecuritySectionInline({ profile, isRunning, t, }: { profile: BrowserProfile; isRunning: boolean; t: (key: string, options?: Record) => string; }) { // Mode is implied by current state: unprotected → "set"; protected → // "change" by default, with a "remove" alternative. type Mode = "set" | "change" | "remove"; const initialMode: Mode = profile.password_protected ? "change" : "set"; const [mode, setMode] = React.useState(initialMode); const [oldPassword, setOldPassword] = React.useState(""); const [password, setPassword] = React.useState(""); const [confirm, setConfirm] = React.useState(""); const [isSubmitting, setIsSubmitting] = React.useState(false); const [error, setError] = React.useState(null); const [success, setSuccess] = React.useState(null); const [isVerifyOpen, setIsVerifyOpen] = React.useState(false); const [verifyPassword, setVerifyPassword] = React.useState(""); const [isVerifying, setIsVerifying] = React.useState(false); const onVerify = async () => { setIsVerifying(true); try { await invoke("verify_profile_password", { profileId: profile.id, password: verifyPassword, }); showSuccessToast(t("profilePassword.verifyDialog.matchToast")); setIsVerifyOpen(false); setVerifyPassword(""); } catch (e) { const message = translateBackendError( t as unknown as Parameters[0], e, ); showErrorToast(message); } finally { setIsVerifying(false); } }; // Reset the form whenever the underlying profile state changes (e.g. the // user just set a password — flip to "change" mode and clear fields). React.useEffect(() => { setMode(profile.password_protected ? "change" : "set"); setOldPassword(""); setPassword(""); setConfirm(""); setError(null); setSuccess(null); }, [profile.password_protected]); const reset = () => { setOldPassword(""); setPassword(""); setConfirm(""); setError(null); }; const validate = (): string | null => { if (mode === "change" || mode === "remove") { if (!oldPassword) return t("profilePassword.errors.passwordRequired"); } if (mode === "set" || mode === "change") { if (password.length < 8) return t("profilePassword.errors.tooShort"); if (password !== confirm) return t("profilePassword.errors.passwordMismatch"); } return null; }; const onSubmit = async () => { if (isRunning) return; const v = validate(); if (v) { setError(v); return; } setIsSubmitting(true); setError(null); setSuccess(null); try { if (mode === "set") { await invoke("set_profile_password", { profileId: profile.id, password, }); showSuccessToast(t("profilePassword.toasts.set")); } else if (mode === "change") { await invoke("change_profile_password", { profileId: profile.id, oldPassword, newPassword: password, }); showSuccessToast(t("profilePassword.toasts.changed")); } else { await invoke("remove_profile_password", { profileId: profile.id, password: oldPassword, }); showSuccessToast(t("profilePassword.toasts.removed")); } reset(); } catch (e) { const message = translateBackendError( t as unknown as Parameters[0], e, ); setError(message); showErrorToast(message); } finally { setIsSubmitting(false); } }; return (
{t("profileInfo.sections.security")}

{profile.password_protected ? t("profileInfo.security.protected") : t("profileInfo.security.unprotected")}

{profile.password_protected && (
)}
{(mode === "change" || mode === "remove") && ( { setOldPassword(e.target.value); setError(null); }} placeholder={t("profilePassword.fields.currentPassword")} disabled={isRunning || isSubmitting} className="h-8 text-xs" /> )} {(mode === "set" || mode === "change") && ( <> { setPassword(e.target.value); setError(null); }} placeholder={t("profilePassword.fields.newPassword")} disabled={isRunning || isSubmitting} className="h-8 text-xs" /> { setConfirm(e.target.value); setError(null); }} placeholder={t("profilePassword.fields.confirmPassword")} disabled={isRunning || isSubmitting} className="h-8 text-xs" /> )}
{error &&

{error}

} {success && !error &&

{success}

} {isRunning && (

{t("profileInfo.security.cannotWhileRunning")}

)} { if (!isVerifying) { setIsVerifyOpen(open); if (!open) setVerifyPassword(""); } }} > {t("profilePassword.verifyDialog.title")} {t("profilePassword.verifyDialog.description")} setVerifyPassword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && verifyPassword.length > 0) { e.preventDefault(); void onVerify(); } }} />
); } interface ProfileLaunchHookDialogProps { isOpen: boolean; onClose: () => void; profileId: string | null; currentLaunchHook: string | null; } export function ProfileLaunchHookDialog({ isOpen, onClose, profileId, currentLaunchHook, }: ProfileLaunchHookDialogProps) { const { t } = useTranslation(); const [value, setValue] = React.useState(currentLaunchHook ?? ""); const [isSaving, setIsSaving] = React.useState(false); React.useEffect(() => { if (isOpen) { setValue(currentLaunchHook ?? ""); } }, [isOpen, currentLaunchHook]); const trimmed = value.trim(); const saved = currentLaunchHook ?? ""; const isDirty = trimmed !== saved; const handleSave = async () => { if (!profileId) return; setIsSaving(true); try { await invoke("update_profile_launch_hook", { profileId, launchHook: trimmed || null, }); onClose(); } catch (err) { console.error("Failed to update launch hook:", err); } finally { setIsSaving(false); } }; return ( !open && onClose()}> {t("profileInfo.launchHook.title")}

{t("profileInfo.launchHook.description")}

{ setValue(e.target.value); }} placeholder={t("profileInfo.launchHook.placeholder")} disabled={isSaving} />
); } interface ProfileDnsBlocklistDialogProps { isOpen: boolean; onClose: () => void; profileId: string | null; currentLevel: string | null; } export function ProfileDnsBlocklistDialog({ isOpen, onClose, profileId, currentLevel, }: ProfileDnsBlocklistDialogProps) { const { t } = useTranslation(); const [level, setLevel] = React.useState(currentLevel ?? ""); const [isSaving, setIsSaving] = React.useState(false); React.useEffect(() => { if (isOpen) { setLevel(currentLevel ?? ""); } }, [isOpen, currentLevel]); const handleSave = async () => { if (!profileId) return; setIsSaving(true); try { await invoke("update_profile_dns_blocklist", { profileId, dnsBlocklist: level || null, }); onClose(); } catch (err) { console.error("Failed to update DNS blocklist:", err); } finally { setIsSaving(false); } }; const options = [ { value: "", label: t("dnsBlocklist.none") }, { value: "light", label: t("dnsBlocklist.light") }, { value: "normal", label: t("dnsBlocklist.normal") }, { value: "pro", label: t("dnsBlocklist.pro") }, { value: "pro_plus", label: t("dnsBlocklist.proPlus") }, { value: "ultimate", label: t("dnsBlocklist.ultimate") }, ]; return ( !open && onClose()}> {t("dnsBlocklist.title")}

{t("dnsBlocklist.settingsDescription")}{" "} {t("common.buttons.moreInfo")}

{options.map((option) => ( ))}
); } interface ProfileBypassRulesDialogProps { isOpen: boolean; onClose: () => void; profileId: string | null; initialRules?: string[]; } export function ProfileBypassRulesDialog({ isOpen, onClose, profileId, initialRules, }: ProfileBypassRulesDialogProps) { const { t } = useTranslation(); const [bypassRules, setBypassRules] = React.useState([]); const [newRule, setNewRule] = React.useState(""); React.useEffect(() => { if (isOpen) { setBypassRules(initialRules ?? []); setNewRule(""); } }, [isOpen, initialRules]); const updateBypassRules = async (rules: string[]) => { if (!profileId) return; try { await invoke("update_profile_proxy_bypass_rules", { profileId, rules, }); setBypassRules(rules); } catch { // ignore } }; const handleAddRule = () => { const trimmed = newRule.trim(); if (!trimmed || bypassRules.includes(trimmed)) return; const updated = [...bypassRules, trimmed]; setNewRule(""); void updateBypassRules(updated); }; const handleRemoveRule = (rule: string) => { void updateBypassRules(bypassRules.filter((r) => r !== rule)); }; return ( { if (!open) onClose(); }} > {t("profileInfo.network.bypassRulesTitle")}

{t("profileInfo.network.bypassRulesDescription")}

{ setNewRule(e.target.value); }} onKeyDown={(e) => { if (e.key === "Enter") handleAddRule(); }} placeholder={t("profileInfo.network.rulePlaceholder")} className="flex-1 text-sm" />
{bypassRules.length === 0 ? (

{t("profileInfo.network.noRules")}

) : (
{bypassRules.map((rule) => (
{rule}
))}
)}

{t("profileInfo.network.ruleTypes")}

); }