mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-12 09:47:51 +02:00
feat: add more import/export formats for cookies
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user