"use client"; import { invoke } from "@tauri-apps/api/core"; import { emit } from "@tauri-apps/api/event"; import { useCallback, useEffect, useReducer, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuUpload } 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 { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; import { StepTransition } from "@/components/ui/step-transition"; import { getCurrentOS } from "@/lib/browser-utils"; import { resolveAmbiguousProxyLine } from "@/lib/proxy-string"; import type { ParsedProxyLine, ProxyImportResult, ProxyParseResult, } from "@/types"; import { RippleButton } from "./ui/ripple"; interface ProxyImportDialogProps { isOpen: boolean; onClose: () => void; } type ImportStep = "dropzone" | "preview" | "ambiguous" | "result"; const STEP_ORDER: Record = { dropzone: 0, ambiguous: 1, preview: 2, result: 3, }; interface StepState { step: ImportStep; direction: 1 | -1; } function transitionStep(current: StepState, next: ImportStep): StepState { return { step: next, direction: STEP_ORDER[next] >= STEP_ORDER[current.step] ? 1 : -1, }; } interface AmbiguousProxy { line: string; possible_formats: string[]; selectedFormat?: string; } export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) { const { t } = useTranslation(); const [{ step, direction: stepDirection }, setStep] = useReducer( transitionStep, { step: "dropzone", direction: 1 }, ); const [isDragOver, setIsDragOver] = useState(false); const [parsedProxies, setParsedProxies] = useState([]); const [ambiguousProxies, setAmbiguousProxies] = useState( [], ); const [invalidProxies, setInvalidProxies] = useState< { line: string; reason: string }[] >([]); const [importResult, setImportResult] = useState( null, ); const [isImporting, setIsImporting] = useState(false); const [namePrefix, setNamePrefix] = useState( t("proxies.importDialog.namePrefixDefault"), ); const os = getCurrentOS(); const modKey = os === "macos" ? "⌘" : "Ctrl"; const resetState = useCallback(() => { setStep("dropzone"); setIsDragOver(false); setParsedProxies([]); setAmbiguousProxies([]); setInvalidProxies([]); setImportResult(null); setIsImporting(false); setNamePrefix(t("proxies.importDialog.namePrefixDefault")); }, [t]); const processContent = useCallback( async (content: string, isJson: boolean, _filename = "") => { try { if (isJson) { setIsImporting(true); const result = await invoke( "import_proxies_json", { content, }, ); setImportResult(result); setStep("result"); await emit("stored-proxies-changed"); } else { const results = await invoke( "parse_txt_proxies", { content, }, ); const parsed: ParsedProxyLine[] = []; const ambiguous: AmbiguousProxy[] = []; const invalid: { line: string; reason: string }[] = []; for (const result of results) { if (result.status === "parsed") { parsed.push(result); } else if (result.status === "ambiguous") { ambiguous.push({ line: result.line, possible_formats: result.possible_formats, }); } else if (result.status === "invalid") { invalid.push({ line: result.line, reason: result.reason }); } } setParsedProxies(parsed); setAmbiguousProxies(ambiguous); setInvalidProxies(invalid); if (ambiguous.length > 0) { setStep("ambiguous"); } else if (parsed.length > 0) { setStep("preview"); } else { toast.error(t("proxies.importDialog.noValidProxies")); } } } catch (error) { console.error("Failed to process content:", error); toast.error( error instanceof Error ? error.message : t("proxies.importDialog.fileProcessError"), ); } finally { setIsImporting(false); } }, [t], ); const handleFileRead = useCallback( (file: File) => { const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; const isJson = file.name.endsWith(".json"); void processContent(content, isJson, file.name); }; reader.onerror = () => { toast.error(t("proxies.importDialog.fileReadError")); }; reader.readAsText(file); }, [processContent, t], ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const files = Array.from(e.dataTransfer.files); const validFile = files.find( (f) => f.name.endsWith(".json") || f.name.endsWith(".txt"), ); if (validFile) { handleFileRead(validFile); } else { toast.error(t("proxies.importDialog.wrongFileType")); } }, [handleFileRead, t], ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); }, []); // Handle paste from clipboard useEffect(() => { if (!isOpen || step !== "dropzone") return; const handlePaste = (e: ClipboardEvent) => { const text = e.clipboardData?.getData("text"); if (text) { // Try to detect if it's JSON const trimmed = text.trim(); const isJson = (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")); // Use "pasted.txt" as filename to trigger content-based detection void processContent(text, isJson, "pasted.txt"); } }; document.addEventListener("paste", handlePaste); return () => { document.removeEventListener("paste", handlePaste); }; }, [isOpen, step, processContent]); const handleImport = useCallback(async () => { setIsImporting(true); try { const result = await invoke( "import_proxies_from_parsed", { parsedProxies, namePrefix: namePrefix.trim() || t("proxies.importDialog.namePrefixDefault"), }, ); setImportResult(result); setStep("result"); await emit("stored-proxies-changed"); } catch (error) { console.error("Failed to import proxies:", error); toast.error( error instanceof Error ? error.message : t("proxies.importDialog.failed"), ); } finally { setIsImporting(false); } }, [parsedProxies, namePrefix, t]); const handleAmbiguousFormatSelect = useCallback( (index: number, format: string) => { setAmbiguousProxies((prev) => prev.map((p, i) => i === index ? { ...p, selectedFormat: format } : p, ), ); }, [], ); const handleResolveAmbiguous = useCallback(() => { const resolved = ambiguousProxies.flatMap((p) => { const parsed = p.selectedFormat ? resolveAmbiguousProxyLine(p.line, p.selectedFormat) : null; return parsed ? [parsed] : []; }); setParsedProxies((prev) => [...prev, ...resolved]); setStep("preview"); }, [ambiguousProxies]); const handleClose = useCallback(() => { resetState(); onClose(); }, [resetState, onClose]); return ( {t("proxies.importDialog.title")} {step === "dropzone" && t("proxies.importDialog.descDropzone")} {step === "preview" && t("proxies.importDialog.descPreview")} {step === "ambiguous" && t("proxies.importDialog.descAmbiguous")} {step === "result" && t("proxies.importDialog.descResult")} {step === "dropzone" && (
document.getElementById("proxy-file-input")?.click() } onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); document.getElementById("proxy-file-input")?.click(); } }} >

{t("proxies.importDialog.dropzonePrompt")}
{t("proxies.importDialog.dropzoneFormats")}

{ const file = e.target.files?.[0]; if (file) handleFileRead(file); e.target.value = ""; }} />

{t("proxies.importDialog.pasteHint", { modKey })}

)} {step === "preview" && (
{ setNamePrefix(e.target.value); }} />

{t("proxies.importDialog.namePrefixHint", { prefix: namePrefix || t("proxies.importDialog.namePrefixDefault"), })}

{parsedProxies.map((proxy, i) => (
{proxy.proxy_type}:// {proxy.username && ( {proxy.username}:***@ )} {proxy.host}:{proxy.port}
))}
)} {step === "ambiguous" && (

{t("proxies.importDialog.ambiguousIntro")}

{ambiguousProxies.map((proxy, i) => (
{proxy.line}
{proxy.possible_formats.map((format) => ( ))}
))}
)} {step === "result" && importResult && (
{t("proxies.importDialog.imported")} {importResult.imported_count}
{importResult.skipped_count > 0 && (
{t("proxies.importDialog.skippedDuplicates")} {importResult.skipped_count}
)} {importResult.errors.length > 0 && (
{t("proxies.importDialog.errors")} {importResult.errors.length}
)}
{importResult.errors.length > 0 && (
{importResult.errors.map((error, i) => (
{error}
))}
)}
)} {step === "dropzone" && ( {t("common.buttons.cancel")} )} {step === "preview" && ( <> {t("common.buttons.back")} void handleImport()} disabled={parsedProxies.length === 0} > {t("proxies.importDialog.importButton", { count: parsedProxies.length, })} )} {step === "ambiguous" && ( <> {t("common.buttons.back")} !p.selectedFormat)} > {t("proxies.importDialog.continueButton")} )} {step === "result" && ( {t("proxies.importDialog.doneButton")} )}
); }