"use client"; import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuChevronDown, LuChevronRight, LuCookie, LuSearch, } from "react-icons/lu"; import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, 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 { getBrowserIcon } from "@/lib/browser-utils"; import type { BrowserProfile, CookieCopyRequest, CookieCopyResult, CookieReadResult, DomainCookies, SelectedCookie, UnifiedCookie, } from "@/types"; import { RippleButton } from "./ui/ripple"; interface CookieCopyDialogProps { isOpen: boolean; onClose: () => void; selectedProfiles: string[]; profiles: BrowserProfile[]; runningProfiles: Set; onCopyComplete?: () => void; } type SelectionState = Record< string, { allSelected: boolean; cookies: Set; } >; export function CookieCopyDialog({ isOpen, onClose, selectedProfiles, profiles, runningProfiles, onCopyComplete, }: CookieCopyDialogProps) { const { t } = useTranslation(); const [sourceProfileId, setSourceProfileId] = useState(null); const [cookieData, setCookieData] = useState(null); const [isLoadingCookies, setIsLoadingCookies] = useState(false); const [isCopying, setIsCopying] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [selection, setSelection] = useState({}); const [expandedDomains, setExpandedDomains] = useState>( new Set(), ); const [error, setError] = useState(null); // Never offer a selected profile as a source — you can't copy a profile's // cookies onto itself, and including it here would leave the user in a // dead-end state (source picked = target list empty = copy button disabled). const eligibleSourceProfiles = useMemo(() => { return profiles.filter( (p) => !selectedProfiles.includes(p.id) && (p.browser === "wayfern" || p.browser === "camoufox"), ); }, [profiles, selectedProfiles]); const targetProfiles = useMemo(() => { return profiles.filter( (p) => selectedProfiles.includes(p.id) && p.id !== sourceProfileId && (p.browser === "wayfern" || p.browser === "camoufox"), ); }, [profiles, selectedProfiles, sourceProfileId]); const filteredDomains = useMemo(() => { if (!cookieData) return []; if (!searchQuery.trim()) return cookieData.domains; const query = searchQuery.toLowerCase(); return cookieData.domains.filter( (d) => d.domain.toLowerCase().includes(query) || d.cookies.some((c) => c.name.toLowerCase().includes(query)), ); }, [cookieData, searchQuery]); const selectedCookieCount = useMemo(() => { let count = 0; for (const domain of Object.keys(selection)) { const domainSelection = selection[domain]; if (domainSelection.allSelected) { const domainData = cookieData?.domains.find((d) => d.domain === domain); count += domainData?.cookie_count ?? 0; } else { count += domainSelection.cookies.size; } } return count; }, [selection, cookieData]); const loadCookies = useCallback(async (profileId: string) => { setIsLoadingCookies(true); setError(null); setCookieData(null); setSelection({}); try { const result = await invoke("read_profile_cookies", { profileId, }); setCookieData(result); } catch (err) { console.error("Failed to load cookies:", err); setError(err instanceof Error ? err.message : String(err)); } finally { setIsLoadingCookies(false); } }, []); const handleSourceChange = useCallback( (profileId: string) => { setSourceProfileId(profileId); void loadCookies(profileId); }, [loadCookies], ); const toggleDomain = useCallback( (domain: string, cookies: UnifiedCookie[]) => { setSelection((prev) => { // `prev[domain]` is `undefined` for any domain not yet interacted with // and after the user fully deselects it (toggleCookie deletes the // entry on empty). Treat missing as "not selected". if (prev[domain]?.allSelected) { const newSelection = { ...prev }; delete newSelection[domain]; return newSelection; } return { ...prev, [domain]: { allSelected: true, cookies: new Set(cookies.map((c) => c.name)), }, }; }); }, [], ); const toggleCookie = useCallback( (domain: string, cookieName: string, totalCookies: number) => { setSelection((prev) => { const current = prev[domain] ?? { allSelected: false, cookies: new Set(), }; const newCookies = new Set(current.cookies); if (newCookies.has(cookieName)) { newCookies.delete(cookieName); } else { newCookies.add(cookieName); } const allSelected = newCookies.size === totalCookies; if (newCookies.size === 0) { const newSelection = { ...prev }; delete newSelection[domain]; return newSelection; } return { ...prev, [domain]: { allSelected, cookies: newCookies, }, }; }); }, [], ); const toggleExpand = useCallback((domain: string) => { setExpandedDomains((prev) => { const next = new Set(prev); if (next.has(domain)) { next.delete(domain); } else { next.add(domain); } return next; }); }, []); const buildSelectedCookies = useCallback((): SelectedCookie[] => { const result: SelectedCookie[] = []; for (const [domain, domainSelection] of Object.entries(selection)) { if (domainSelection.allSelected) { result.push({ domain, name: "" }); } else { for (const cookieName of domainSelection.cookies) { result.push({ domain, name: cookieName }); } } } return result; }, [selection]); const handleCopy = useCallback(async () => { if (!sourceProfileId || targetProfiles.length === 0) return; const runningTargets = targetProfiles.filter((p) => runningProfiles.has(p.id), ); if (runningTargets.length > 0) { const names = runningTargets.map((p) => p.name).join(", "); toast.error( runningTargets.length === 1 ? t("cookies.copy.cannotCopyRunningOne", { names }) : t("cookies.copy.cannotCopyRunningMany", { names }), ); return; } setIsCopying(true); setError(null); try { const selectedCookies = buildSelectedCookies(); const request: CookieCopyRequest = { source_profile_id: sourceProfileId, target_profile_ids: targetProfiles.map((p) => p.id), selected_cookies: selectedCookies, }; const results = await invoke("copy_profile_cookies", { request, }); let totalCopied = 0; let totalReplaced = 0; const errors: string[] = []; for (const result of results) { totalCopied += result.cookies_copied; totalReplaced += result.cookies_replaced; errors.push(...result.errors); } if (errors.length > 0) { toast.error( t("cookies.copy.someErrors", { errors: errors.join(", ") }), ); } else { toast.success( t("cookies.copy.successMessage", { copied: totalCopied + totalReplaced, replaced: totalReplaced, }), ); onCopyComplete?.(); onClose(); } } catch (err) { console.error("Failed to copy cookies:", err); toast.error( t("cookies.copy.failedMessage", { error: err instanceof Error ? err.message : String(err), }), ); } finally { setIsCopying(false); } }, [ sourceProfileId, targetProfiles, runningProfiles, buildSelectedCookies, onCopyComplete, onClose, t, ]); useEffect(() => { if (isOpen) { setSourceProfileId(null); setCookieData(null); setSelection({}); setSearchQuery(""); setExpandedDomains(new Set()); setError(null); } }, [isOpen]); const canCopy = sourceProfileId && targetProfiles.length > 0 && selectedCookieCount > 0 && !isCopying; return ( {t("cookies.copy.title")} {selectedProfiles.length === 1 ? t("cookies.copy.dialogDescription_one", { count: selectedProfiles.length, }) : t("cookies.copy.dialogDescription_other", { count: selectedProfiles.length, })}
{targetProfiles.length === 0 ? (

{sourceProfileId ? t("cookies.copy.noOtherTargets") : t("cookies.copy.selectSourceFirst")}

) : (
{targetProfiles.map((p) => ( {p.name} {runningProfiles.has(p.id) && ( {t("cookies.copy.running")} )} ))}
)}
{sourceProfileId && (
{ setSearchQuery(e.target.value); }} className="pl-8" />
{isLoadingCookies ? (
) : error ? (
{error}
) : filteredDomains.length === 0 ? (
{searchQuery ? t("cookies.copy.noMatching") : t("cookies.copy.noFound")}
) : (
{filteredDomains.map((domain) => ( ))}
)}

{t("cookies.copy.replaceNote")}

)}
{t("common.buttons.cancel")} void handleCopy()} disabled={!canCopy} > {selectedCookieCount === 0 ? t("cookies.copy.copyButtonEmpty") : selectedCookieCount === 1 ? t("cookies.copy.copyButton_one", { count: selectedCookieCount, }) : t("cookies.copy.copyButton_other", { count: selectedCookieCount, })}
); } interface DomainRowProps { domain: DomainCookies; selection: SelectionState; isExpanded: boolean; onToggleDomain: (domain: string, cookies: UnifiedCookie[]) => void; onToggleCookie: ( domain: string, cookieName: string, totalCookies: number, ) => void; onToggleExpand: (domain: string) => void; } function DomainRow({ domain, selection, isExpanded, onToggleDomain, onToggleCookie, onToggleExpand, }: DomainRowProps) { // `selection[domain.domain]` is `undefined` for domains the user hasn't // touched yet (initial state after loading cookies is `{}`) and for any // domain the user fully deselected (toggleCookie deletes the entry on // empty). Default to "no cookies selected" instead of crashing. const domainSelection = selection[domain.domain]; const isAllSelected = domainSelection?.allSelected ?? false; const selectedCount = domainSelection?.cookies.size ?? 0; const isPartial = selectedCount > 0 && selectedCount < domain.cookie_count && !isAllSelected; return (
{ onToggleDomain(domain.domain, domain.cookies); }} className={isPartial ? "opacity-70" : ""} />
{isExpanded && (
{domain.cookies.map((cookie) => { const isSelected = domainSelection?.cookies.has(cookie.name) ?? false; return (
{ onToggleCookie( domain.domain, cookie.name, domain.cookie_count, ); }} /> {cookie.name}
); })}
)}
); }