"use client"; import { invoke } from "@tauri-apps/api/core"; import { save } from "@tauri-apps/plugin-dialog"; import { writeTextFile } from "@tauri-apps/plugin-fs"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuChevronDown, LuChevronRight, LuUpload } from "react-icons/lu"; import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { FadingScrollArea } from "@/components/ui/fading-scroll-area"; import { Label } from "@/components/ui/label"; import { RippleButton } from "@/components/ui/ripple"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { BrowserProfile, CookieReadResult, DomainCookies, UnifiedCookie, } from "@/types"; interface CookieImportResult { cookies_imported: number; cookies_replaced: number; errors: string[]; } interface CookieManagementDialogProps { isOpen: boolean; onClose: () => void; profile: BrowserProfile | null; initialTab?: "import" | "export"; } type SelectionState = Record< string, { allSelected: boolean; cookies: Set; } >; const countCookies = (content: string): number => { const trimmed = content.trim(); if (trimmed.startsWith("[")) { try { const arr = JSON.parse(trimmed); if (Array.isArray(arr)) return arr.length; } catch { // Fall through to Netscape counting } } return content.split("\n").filter((line) => { const l = line.trim(); return l && !l.startsWith("#"); }).length; }; function formatJsonCookies(cookies: UnifiedCookie[]): string { const arr = cookies.map((c) => { const sameSite = c.same_site === 1 ? "lax" : c.same_site === 2 ? "strict" : "no_restriction"; return { name: c.name, value: c.value, domain: c.domain, path: c.path, secure: c.is_secure, httpOnly: c.is_http_only, sameSite, expirationDate: c.expires, session: c.expires === 0, hostOnly: !c.domain.startsWith("."), }; }); return JSON.stringify(arr, null, 2); } function formatNetscapeCookies(cookies: UnifiedCookie[]): string { const lines = ["# Netscape HTTP Cookie File"]; for (const c of cookies) { const flag = c.domain.startsWith(".") ? "TRUE" : "FALSE"; const secure = c.is_secure ? "TRUE" : "FALSE"; lines.push( `${c.domain}\t${flag}\t${c.path}\t${secure}\t${c.expires}\t${c.name}\t${c.value}`, ); } return lines.join("\n"); } function initSelectionFromCookieData(data: CookieReadResult): SelectionState { const sel: SelectionState = {}; for (const d of data.domains) { sel[d.domain] = { allSelected: true, cookies: new Set(d.cookies.map((c) => c.name)), }; } return sel; } export function CookieManagementDialog({ isOpen, onClose, profile, initialTab = "import", }: CookieManagementDialogProps) { const { t } = useTranslation(); // Import state const [fileContent, setFileContent] = useState(null); const [fileName, setFileName] = useState(null); const [cookieCount, setCookieCount] = useState(0); const [isImporting, setIsImporting] = useState(false); const [importResult, setImportResult] = useState( null, ); // Export state const [format, setFormat] = useState<"netscape" | "json">("json"); const [isExporting, setIsExporting] = useState(false); const [exportCookieData, setExportCookieData] = useState(null); const [isLoadingExportCookies, setIsLoadingExportCookies] = useState(false); const [exportSelection, setExportSelection] = useState({}); const [expandedDomains, setExpandedDomains] = useState>( new Set(), ); const [activeTab, setActiveTab] = useState(initialTab); const selectedExportCount = useMemo(() => { let count = 0; for (const domain of Object.keys(exportSelection)) { const ds = exportSelection[domain]; if (ds.allSelected) { const domainData = exportCookieData?.domains.find( (d) => d.domain === domain, ); count += domainData?.cookie_count ?? 0; } else { count += ds.cookies.size; } } return count; }, [exportSelection, exportCookieData]); const loadExportCookies = useCallback( async (profileId: string) => { if (exportCookieData) return; setIsLoadingExportCookies(true); try { const result = await invoke("read_profile_cookies", { profileId, }); setExportCookieData(result); setExportSelection(initSelectionFromCookieData(result)); } catch (err) { toast.error( t("cookies.management.loadFailed", { error: err instanceof Error ? err.message : String(err), }), ); } finally { setIsLoadingExportCookies(false); } }, [exportCookieData, t], ); useEffect(() => { if (activeTab === "export" && profile && !exportCookieData) { void loadExportCookies(profile.id); } }, [activeTab, profile, exportCookieData, loadExportCookies]); const resetImportState = useCallback(() => { setFileContent(null); setFileName(null); setCookieCount(0); setIsImporting(false); setImportResult(null); }, []); const resetExportState = useCallback(() => { setFormat("json"); setIsExporting(false); setExportCookieData(null); setExportSelection({}); setExpandedDomains(new Set()); }, []); const handleClose = useCallback(() => { resetImportState(); resetExportState(); setActiveTab(initialTab); onClose(); }, [resetImportState, resetExportState, onClose, initialTab]); const handleTabChange = useCallback( (tab: string) => { setActiveTab(tab); resetImportState(); if (tab !== "export") { resetExportState(); } }, [resetImportState, resetExportState], ); const handleFileRead = useCallback( (file: File) => { const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; setFileContent(content); setFileName(file.name); setCookieCount(countCookies(content)); }; reader.onerror = () => { toast.error(t("cookies.management.fileReadError")); }; reader.readAsText(file); }, [t], ); const handleImport = useCallback(async () => { if (!fileContent || !profile) return; setIsImporting(true); try { const result = await invoke( "import_cookies_from_file", { profileId: profile.id, content: fileContent, }, ); setImportResult(result); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } finally { setIsImporting(false); } }, [fileContent, profile]); const getSelectedCookies = useCallback((): UnifiedCookie[] => { if (!exportCookieData) return []; const result: UnifiedCookie[] = []; for (const domain of exportCookieData.domains) { const ds = exportSelection[domain.domain]; if (!ds) continue; if (ds.allSelected) { result.push(...domain.cookies); } else { result.push(...domain.cookies.filter((c) => ds.cookies.has(c.name))); } } return result; }, [exportCookieData, exportSelection]); const handleExport = useCallback(async () => { if (!profile) return; setIsExporting(true); try { const cookies = getSelectedCookies(); const content = format === "json" ? formatJsonCookies(cookies) : formatNetscapeCookies(cookies); const ext = format === "json" ? "json" : "txt"; const defaultName = `${profile.name}_cookies.${ext}`; const filePath = await save({ defaultPath: defaultName, filters: [ { name: format === "json" ? "JSON" : "Text", extensions: [ext], }, ], }); if (!filePath) { setIsExporting(false); return; } await writeTextFile(filePath, content); toast.success(t("cookies.export.success")); handleClose(); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } finally { setIsExporting(false); } }, [profile, format, getSelectedCookies, handleClose, t]); const toggleDomain = useCallback( (domain: string, cookies: UnifiedCookie[]) => { setExportSelection((prev) => { // `prev[domain]` is `undefined` when the domain was previously fully // deselected (entries are deleted on empty — see toggleCookie). Treat // missing as "not selected" so re-enabling falls through to the add // branch instead of crashing on `.allSelected`. if (prev[domain]?.allSelected) { const next = { ...prev }; delete next[domain]; return next; } return { ...prev, [domain]: { allSelected: true, cookies: new Set(cookies.map((c) => c.name)), }, }; }); }, [], ); const toggleCookie = useCallback( (domain: string, cookieName: string, totalCookies: number) => { setExportSelection((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); } if (newCookies.size === 0) { const next = { ...prev }; delete next[domain]; return next; } return { ...prev, [domain]: { allSelected: newCookies.size === totalCookies, 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 toggleSelectAll = useCallback(() => { if (!exportCookieData) return; if (selectedExportCount === exportCookieData.total_count) { setExportSelection({}); } else { setExportSelection(initSelectionFromCookieData(exportCookieData)); } }, [exportCookieData, selectedExportCount]); return ( {t("cookies.management.title")} {t("cookies.management.tabImport")} {t("cookies.management.tabExport")} {!fileContent && (

{t("cookies.management.importDescription")}

document.getElementById("cookie-file-input")?.click() } onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); document.getElementById("cookie-file-input")?.click(); } }} >

{t("cookies.management.dropPrompt")}
{t("cookies.management.fileFormats")}

{ const file = e.target.files?.[0]; if (file) handleFileRead(file); e.target.value = ""; }} />
)} {fileContent && !importResult && (
{fileName}
{t("cookies.management.cookiesFound", { count: cookieCount, })}
{t("cookies.management.backButton")} void handleImport()} disabled={cookieCount === 0} > {t("cookies.management.importButton")}
)} {importResult && (
{t("cookies.management.importedSuccess", { imported: importResult.cookies_imported, replaced: importResult.cookies_replaced, })}
{importResult.errors.length > 0 && (
{t("cookies.management.linesSkipped", { count: importResult.errors.length, })}
)}
{t("cookies.management.doneButton")}
)}
{exportCookieData && exportCookieData.total_count > 0 && ( )}
{isLoadingExportCookies ? (
) : !exportCookieData || exportCookieData.domains.length === 0 ? (
{t("cookies.management.noCookies")}
) : (
{exportCookieData.domains.map((domain) => ( ))}
)}
{t("common.buttons.cancel")} void handleExport()} disabled={selectedExportCount === 0} > {t("cookies.management.exportButton")}
); } interface ExportDomainRowProps { 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 ExportDomainRow({ domain, selection, isExpanded, onToggleDomain, onToggleCookie, onToggleExpand, }: ExportDomainRowProps) { 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}
); })}
)}
); }