feat: add more import/export formats for cookies

This commit is contained in:
zhom
2026-02-21 22:13:02 +04:00
parent 206be3ff12
commit 8b9ad44ebc
17 changed files with 769 additions and 42 deletions
+19
View File
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
import { CommercialTrialModal } from "@/components/commercial-trial-modal";
import { CookieCopyDialog } from "@/components/cookie-copy-dialog";
import { CookieExportDialog } from "@/components/cookie-export-dialog";
import { CookieImportDialog } from "@/components/cookie-import-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog";
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
@@ -146,6 +147,9 @@ export default function Home() {
const [cookieImportDialogOpen, setCookieImportDialogOpen] = useState(false);
const [currentProfileForCookieImport, setCurrentProfileForCookieImport] =
useState<BrowserProfile | null>(null);
const [cookieExportDialogOpen, setCookieExportDialogOpen] = useState(false);
const [currentProfileForCookieExport, setCurrentProfileForCookieExport] =
useState<BrowserProfile | null>(null);
const [selectedProfilesForCookies, setSelectedProfilesForCookies] = useState<
string[]
>([]);
@@ -697,6 +701,11 @@ export default function Home() {
setCookieImportDialogOpen(true);
}, []);
const handleExportCookies = useCallback((profile: BrowserProfile) => {
setCurrentProfileForCookieExport(profile);
setCookieExportDialogOpen(true);
}, []);
const handleGroupAssignmentComplete = useCallback(async () => {
// No need to manually reload - useProfileEvents will handle the update
setGroupAssignmentDialogOpen(false);
@@ -1004,6 +1013,7 @@ export default function Home() {
onConfigureCamoufox={handleConfigureCamoufox}
onCopyCookiesToProfile={handleCopyCookiesToProfile}
onImportCookies={handleImportCookies}
onExportCookies={handleExportCookies}
runningProfiles={runningProfiles}
isUpdating={isUpdating}
onDeleteSelectedProfiles={handleDeleteSelectedProfiles}
@@ -1156,6 +1166,15 @@ export default function Home() {
profile={currentProfileForCookieImport}
/>
<CookieExportDialog
isOpen={cookieExportDialogOpen}
onClose={() => {
setCookieExportDialogOpen(false);
setCurrentProfileForCookieExport(null);
}}
profile={currentProfileForCookieExport}
/>
<DeleteConfirmationDialog
isOpen={showBulkDeleteConfirmation}
onClose={() => setShowBulkDeleteConfirmation(false)}
+129
View File
@@ -0,0 +1,129 @@
"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, useState } from "react";
import { LuDownload } from "react-icons/lu";
import { toast } from "sonner";
import { LoadingButton } from "@/components/loading-button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { RippleButton } from "@/components/ui/ripple";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { BrowserProfile } from "@/types";
interface CookieExportDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
}
export function CookieExportDialog({
isOpen,
onClose,
profile,
}: CookieExportDialogProps) {
const [format, setFormat] = useState<"netscape" | "json">("json");
const [isExporting, setIsExporting] = useState(false);
const handleClose = useCallback(() => {
setFormat("json");
setIsExporting(false);
onClose();
}, [onClose]);
const handleExport = useCallback(async () => {
if (!profile) return;
setIsExporting(true);
try {
const content = await invoke<string>("export_profile_cookies", {
profileId: profile.id,
format,
});
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("Cookies exported successfully");
handleClose();
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
setIsExporting(false);
}
}, [profile, format, handleClose]);
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Export Cookies</DialogTitle>
<DialogDescription>
Export cookies from this profile.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Format</Label>
<Select
value={format}
onValueChange={(v) => setFormat(v as "netscape" | "json")}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="netscape">Netscape TXT</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<RippleButton variant="outline" onClick={handleClose}>
Cancel
</RippleButton>
<LoadingButton
isLoading={isExporting}
onClick={() => void handleExport()}
>
<LuDownload className="w-4 h-4 mr-2" />
Export
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+16 -6
View File
@@ -29,9 +29,18 @@ interface CookieImportDialogProps {
}
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 trimmed = line.trim();
return trimmed && !trimmed.startsWith("#");
const l = line.trim();
return l && !l.startsWith("#");
}).length;
};
@@ -98,7 +107,8 @@ export function CookieImportDialog({
<DialogHeader>
<DialogTitle>Import Cookies</DialogTitle>
<DialogDescription>
{!fileContent && "Import cookies from a Netscape format file."}
{!fileContent &&
"Import cookies from a Netscape or JSON format file."}
{fileContent &&
!result &&
`${cookieCount} cookies found in ${fileName}`}
@@ -124,14 +134,14 @@ export function CookieImportDialog({
>
<LuUpload className="w-10 h-10 text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground text-center">
Click to choose a Netscape cookie file
Click to choose a cookie file
<br />
<span className="text-xs">(.txt or .cookies)</span>
<span className="text-xs">(.txt, .cookies, or .json)</span>
</p>
<input
id="cookie-file-input"
type="file"
accept=".txt,.cookies"
accept=".txt,.cookies,.json"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
+22
View File
@@ -177,6 +177,7 @@ type TableMeta = {
onCloneProfile?: (profile: BrowserProfile) => void;
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
onImportCookies?: (profile: BrowserProfile) => void;
onExportCookies?: (profile: BrowserProfile) => void;
// Traffic snapshots (lightweight real-time data)
trafficSnapshots: Record<string, TrafficSnapshot>;
@@ -758,6 +759,7 @@ interface ProfilesDataTableProps {
onConfigureCamoufox: (profile: BrowserProfile) => void;
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
onImportCookies?: (profile: BrowserProfile) => void;
onExportCookies?: (profile: BrowserProfile) => void;
runningProfiles: Set<string>;
isUpdating: (browser: string) => boolean;
onDeleteSelectedProfiles: (profileIds: string[]) => Promise<void>;
@@ -785,6 +787,7 @@ export function ProfilesDataTable({
onConfigureCamoufox,
onCopyCookiesToProfile,
onImportCookies,
onExportCookies,
runningProfiles,
isUpdating,
onAssignProfilesToGroup,
@@ -1475,6 +1478,7 @@ export function ProfilesDataTable({
onConfigureCamoufox,
onCopyCookiesToProfile,
onImportCookies,
onExportCookies,
// Traffic snapshots (lightweight real-time data)
trafficSnapshots,
@@ -1537,6 +1541,7 @@ export function ProfilesDataTable({
onConfigureCamoufox,
onCopyCookiesToProfile,
onImportCookies,
onExportCookies,
syncStatuses,
onOpenProfileSyncDialog,
onToggleProfileSync,
@@ -2424,6 +2429,23 @@ export function ProfilesDataTable({
</span>
</DropdownMenuItem>
)}
{(profile.browser === "camoufox" ||
profile.browser === "wayfern") &&
meta.onExportCookies && (
<DropdownMenuItem
onClick={() => {
if (meta.crossOsUnlocked) {
meta.onExportCookies?.(profile);
}
}}
disabled={isDisabled || !meta.crossOsUnlocked}
>
<span className="flex items-center gap-2">
Export Cookies
{!meta.crossOsUnlocked && <ProBadge />}
</span>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
meta.onCloneProfile?.(profile);
+12 -2
View File
@@ -507,18 +507,28 @@
"cookies": {
"import": {
"title": "Import Cookies",
"description": "Import cookies from a Netscape format file.",
"description": "Import cookies from a Netscape or JSON format file.",
"selectFile": "Choose File",
"preview": "{{count}} cookies found",
"success": "Successfully imported {{imported}} cookies ({{replaced}} replaced)",
"error": "Failed to import cookies",
"proFeature": "Cookie import is a Pro feature"
},
"export": {
"title": "Export Cookies",
"description": "Export cookies from this profile.",
"formatLabel": "Format",
"netscape": "Netscape TXT",
"json": "JSON",
"success": "Cookies exported successfully",
"error": "Failed to export cookies"
}
},
"pro": {
"badge": "PRO",
"fingerprintLocked": "Fingerprint editing is a Pro feature",
"cookieCopyLocked": "Cookie copying is a Pro feature",
"cookieImportLocked": "Cookie import is a Pro feature"
"cookieImportLocked": "Cookie import is a Pro feature",
"cookieExportLocked": "Cookie export is a Pro feature"
}
}
+12 -2
View File
@@ -507,18 +507,28 @@
"cookies": {
"import": {
"title": "Importar Cookies",
"description": "Importar cookies desde un archivo en formato Netscape.",
"description": "Importar cookies desde un archivo en formato Netscape o JSON.",
"selectFile": "Elegir Archivo",
"preview": "{{count}} cookies encontradas",
"success": "Se importaron {{imported}} cookies exitosamente ({{replaced}} reemplazadas)",
"error": "Error al importar cookies",
"proFeature": "La importación de cookies es una función Pro"
},
"export": {
"title": "Exportar Cookies",
"description": "Exportar cookies de este perfil.",
"formatLabel": "Formato",
"netscape": "Netscape TXT",
"json": "JSON",
"success": "Cookies exportadas exitosamente",
"error": "Error al exportar cookies"
}
},
"pro": {
"badge": "PRO",
"fingerprintLocked": "La edición de huellas digitales es una función Pro",
"cookieCopyLocked": "La copia de cookies es una función Pro",
"cookieImportLocked": "La importación de cookies es una función Pro"
"cookieImportLocked": "La importación de cookies es una función Pro",
"cookieExportLocked": "La exportación de cookies es una función Pro"
}
}
+12 -2
View File
@@ -507,18 +507,28 @@
"cookies": {
"import": {
"title": "Importer des Cookies",
"description": "Importer des cookies depuis un fichier au format Netscape.",
"description": "Importer des cookies depuis un fichier au format Netscape ou JSON.",
"selectFile": "Choisir un Fichier",
"preview": "{{count}} cookies trouvés",
"success": "{{imported}} cookies importés avec succès ({{replaced}} remplacés)",
"error": "Échec de l'importation des cookies",
"proFeature": "L'importation de cookies est une fonctionnalité Pro"
},
"export": {
"title": "Exporter les Cookies",
"description": "Exporter les cookies de ce profil.",
"formatLabel": "Format",
"netscape": "Netscape TXT",
"json": "JSON",
"success": "Cookies exportés avec succès",
"error": "Échec de l'exportation des cookies"
}
},
"pro": {
"badge": "PRO",
"fingerprintLocked": "La modification d'empreinte est une fonctionnalité Pro",
"cookieCopyLocked": "La copie de cookies est une fonctionnalité Pro",
"cookieImportLocked": "L'importation de cookies est une fonctionnalité Pro"
"cookieImportLocked": "L'importation de cookies est une fonctionnalité Pro",
"cookieExportLocked": "L'exportation de cookies est une fonctionnalité Pro"
}
}
+12 -2
View File
@@ -507,18 +507,28 @@
"cookies": {
"import": {
"title": "Cookieのインポート",
"description": "Netscape形式のファイルからCookieをインポートします。",
"description": "NetscapeまたはJSON形式のファイルからCookieをインポートします。",
"selectFile": "ファイルを選択",
"preview": "{{count}}件のCookieが見つかりました",
"success": "{{imported}}件のCookieをインポートしました({{replaced}}件を置換)",
"error": "Cookieのインポートに失敗しました",
"proFeature": "Cookieのインポートはプロ機能です"
},
"export": {
"title": "Cookieのエクスポート",
"description": "このプロファイルからCookieをエクスポートします。",
"formatLabel": "形式",
"netscape": "Netscape TXT",
"json": "JSON",
"success": "Cookieのエクスポートに成功しました",
"error": "Cookieのエクスポートに失敗しました"
}
},
"pro": {
"badge": "PRO",
"fingerprintLocked": "フィンガープリント編集はプロ機能です",
"cookieCopyLocked": "Cookieのコピーはプロ機能です",
"cookieImportLocked": "Cookieのインポートはプロ機能です"
"cookieImportLocked": "Cookieのインポートはプロ機能です",
"cookieExportLocked": "Cookieのエクスポートはプロ機能です"
}
}
+12 -2
View File
@@ -507,18 +507,28 @@
"cookies": {
"import": {
"title": "Importar Cookies",
"description": "Importar cookies de um arquivo no formato Netscape.",
"description": "Importar cookies de um arquivo no formato Netscape ou JSON.",
"selectFile": "Escolher Arquivo",
"preview": "{{count}} cookies encontrados",
"success": "{{imported}} cookies importados com sucesso ({{replaced}} substituídos)",
"error": "Falha ao importar cookies",
"proFeature": "A importação de cookies é um recurso Pro"
},
"export": {
"title": "Exportar Cookies",
"description": "Exportar cookies deste perfil.",
"formatLabel": "Formato",
"netscape": "Netscape TXT",
"json": "JSON",
"success": "Cookies exportados com sucesso",
"error": "Falha ao exportar cookies"
}
},
"pro": {
"badge": "PRO",
"fingerprintLocked": "A edição de impressão digital é um recurso Pro",
"cookieCopyLocked": "A cópia de cookies é um recurso Pro",
"cookieImportLocked": "A importação de cookies é um recurso Pro"
"cookieImportLocked": "A importação de cookies é um recurso Pro",
"cookieExportLocked": "A exportação de cookies é um recurso Pro"
}
}
+12 -2
View File
@@ -507,18 +507,28 @@
"cookies": {
"import": {
"title": "Импорт Cookies",
"description": "Импорт cookies из файла в формате Netscape.",
"description": "Импорт cookies из файла в формате Netscape или JSON.",
"selectFile": "Выбрать файл",
"preview": "Найдено {{count}} cookies",
"success": "Успешно импортировано {{imported}} cookies ({{replaced}} заменено)",
"error": "Ошибка импорта cookies",
"proFeature": "Импорт cookies — функция Pro"
},
"export": {
"title": "Экспорт Cookies",
"description": "Экспорт cookies из этого профиля.",
"formatLabel": "Формат",
"netscape": "Netscape TXT",
"json": "JSON",
"success": "Cookies успешно экспортированы",
"error": "Ошибка экспорта cookies"
}
},
"pro": {
"badge": "PRO",
"fingerprintLocked": "Редактирование отпечатка — функция Pro",
"cookieCopyLocked": "Копирование cookies — функция Pro",
"cookieImportLocked": "Импорт cookies — функция Pro"
"cookieImportLocked": "Импорт cookies — функция Pro",
"cookieExportLocked": "Экспорт cookies — функция Pro"
}
}
+12 -2
View File
@@ -507,18 +507,28 @@
"cookies": {
"import": {
"title": "导入 Cookies",
"description": "从 Netscape 格式文件导入 Cookies。",
"description": "从 Netscape 或 JSON 格式文件导入 Cookies。",
"selectFile": "选择文件",
"preview": "找到 {{count}} 个 Cookies",
"success": "成功导入 {{imported}} 个 Cookies(替换了 {{replaced}} 个)",
"error": "导入 Cookies 失败",
"proFeature": "导入 Cookies 是 Pro 功能"
},
"export": {
"title": "导出 Cookies",
"description": "从此配置文件导出 Cookies。",
"formatLabel": "格式",
"netscape": "Netscape TXT",
"json": "JSON",
"success": "Cookies 导出成功",
"error": "导出 Cookies 失败"
}
},
"pro": {
"badge": "PRO",
"fingerprintLocked": "指纹编辑是 Pro 功能",
"cookieCopyLocked": "Cookie 复制是 Pro 功能",
"cookieImportLocked": "Cookie 导入是 Pro 功能"
"cookieImportLocked": "Cookie 导入是 Pro 功能",
"cookieExportLocked": "Cookie 导出是 Pro 功能"
}
}