"use client"; import { invoke } from "@tauri-apps/api/core"; import { emit } from "@tauri-apps/api/event"; import { useCallback, useEffect, useState } from "react"; 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 { getCurrentOS } from "@/lib/browser-utils"; import type { ParsedProxyLine, ProxyImportResult, ProxyParseResult, } from "@/types"; import { RippleButton } from "./ui/ripple"; interface ProxyImportDialogProps { isOpen: boolean; onClose: () => void; } type ImportStep = "dropzone" | "preview" | "ambiguous" | "result"; interface AmbiguousProxy { line: string; possible_formats: string[]; selectedFormat?: string; } export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) { const [step, setStep] = useState("dropzone"); 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("Imported"); const os = getCurrentOS(); const modKey = os === "macos" ? "⌘" : "Ctrl"; const resetState = useCallback(() => { setStep("dropzone"); setIsDragOver(false); setParsedProxies([]); setAmbiguousProxies([]); setInvalidProxies([]); setImportResult(null); setIsImporting(false); setNamePrefix("Imported"); }, []); 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("No valid proxies found in the file"); } } } catch (error) { console.error("Failed to process content:", error); toast.error( error instanceof Error ? error.message : "Failed to process file", ); } finally { setIsImporting(false); } }, [], ); 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("Failed to read file"); }; reader.readAsText(file); }, [processContent], ); 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("Please drop a .json or .txt file"); } }, [handleFileRead], ); 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() || "Imported", }, ); 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 : "Failed to import proxies", ); } finally { setIsImporting(false); } }, [parsedProxies, namePrefix]); const handleAmbiguousFormatSelect = useCallback( (index: number, format: string) => { setAmbiguousProxies((prev) => prev.map((p, i) => i === index ? { ...p, selectedFormat: format } : p, ), ); }, [], ); const handleResolveAmbiguous = useCallback(() => { // Convert ambiguous proxies to parsed based on selected format const resolved: ParsedProxyLine[] = ambiguousProxies .filter((p) => p.selectedFormat) .map((p) => { const parts = p.line.split(":"); if (p.selectedFormat === "host:port:username:password") { return { proxy_type: "http", host: parts[0], port: Number.parseInt(parts[1], 10), username: parts[2], password: parts[3], original_line: p.line, }; } // username:password:host:port return { proxy_type: "http", host: parts[2], port: Number.parseInt(parts[3], 10), username: parts[0], password: parts[1], original_line: p.line, }; }); setParsedProxies((prev) => [...prev, ...resolved]); setStep("preview"); }, [ambiguousProxies]); const handleClose = useCallback(() => { resetState(); onClose(); }, [resetState, onClose]); return ( Import Proxies {step === "dropzone" && "Import proxies from a JSON or TXT file"} {step === "preview" && "Review the proxies to import"} {step === "ambiguous" && "Some proxies have ambiguous formats. Please select the correct format."} {step === "result" && "Import completed"} {step === "dropzone" && (
document.getElementById("proxy-file-input")?.click() } onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); document.getElementById("proxy-file-input")?.click(); } }} >

Drop a proxy config file
(.json, .txt)

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

Paste from clipboard with {modKey}+V

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

Proxies will be named "{namePrefix || "Imported"} Proxy 1", "{namePrefix || "Imported"} Proxy 2", etc.

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

The following proxies have an ambiguous format. Please select the correct interpretation for each.

{ambiguousProxies.map((proxy, i) => (
{proxy.line}
{proxy.possible_formats.map((format) => ( ))}
))}
)} {step === "result" && importResult && (
Imported: {importResult.imported_count}
{importResult.skipped_count > 0 && (
Skipped (duplicates): {importResult.skipped_count}
)} {importResult.errors.length > 0 && (
Errors: {importResult.errors.length}
)}
{importResult.errors.length > 0 && (
{importResult.errors.map((error, i) => (
{error}
))}
)}
)} {step === "dropzone" && ( Cancel )} {step === "preview" && ( <> Back void handleImport()} disabled={parsedProxies.length === 0} > Import {parsedProxies.length} Proxies )} {step === "ambiguous" && ( <> Back !p.selectedFormat)} > Continue )} {step === "result" && ( Done )}
); }