"use client"; import { invoke } from "@tauri-apps/api/core"; import { emit } from "@tauri-apps/api/event"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuShield, 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 { RippleButton } from "@/components/ui/ripple"; import { ScrollArea } from "@/components/ui/scroll-area"; import { getCurrentOS } from "@/lib/browser-utils"; import type { VpnImportResult, VpnType } from "@/types"; interface VpnImportDialogProps { isOpen: boolean; onClose: () => void; } type ImportStep = "dropzone" | "vpn-preview" | "vpn-result"; interface VpnPreviewData { content: string; filename: string; detectedType: VpnType | null; endpoint: string | null; } const detectVpnType = ( content: string, filename: string, ): { isVpn: boolean; type: VpnType | null; endpoint: string | null } => { const lowerFilename = filename.toLowerCase(); if ( lowerFilename.endsWith(".conf") && content.includes("[Interface]") && content.includes("[Peer]") ) { const endpointMatch = content.match(/Endpoint\s*=\s*([^\s\n]+)/i); return { isVpn: true, type: "WireGuard", endpoint: endpointMatch ? endpointMatch[1] : null, }; } return { isVpn: false, type: null, endpoint: null }; }; export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) { const { t } = useTranslation(); const [step, setStep] = useState("dropzone"); const [isDragOver, setIsDragOver] = useState(false); const [vpnPreview, setVpnPreview] = useState(null); const [vpnName, setVpnName] = useState(""); const [vpnImportResult, setVpnImportResult] = useState(null); const [isImporting, setIsImporting] = useState(false); const os = getCurrentOS(); const modKey = os === "macos" ? "⌘" : "Ctrl"; const resetState = useCallback(() => { setStep("dropzone"); setIsDragOver(false); setVpnPreview(null); setVpnName(""); setVpnImportResult(null); setIsImporting(false); }, []); const handleClose = useCallback(() => { resetState(); onClose(); }, [resetState, onClose]); const processContent = useCallback( (content: string, filename: string) => { const detection = detectVpnType(content, filename); if (!detection.isVpn) { toast.error(t("vpns.import.invalidContent")); return; } setVpnPreview({ content, filename, detectedType: detection.type, endpoint: detection.endpoint, }); const baseName = filename .replace(/\.conf$/i, "") .replace(/_/g, " ") .replace(/-/g, " "); setVpnName(baseName || `${detection.type} VPN`); setStep("vpn-preview"); }, [t], ); const handleFileRead = useCallback( (file: File) => { const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; processContent(content, file.name); }; reader.onerror = () => { toast.error(t("vpns.import.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(".conf")); if (validFile) { handleFileRead(validFile); } else { toast.error(t("vpns.import.wrongFileType")); } }, [handleFileRead, t], ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); }, []); useEffect(() => { if (!isOpen || step !== "dropzone") return; const handlePaste = (e: ClipboardEvent) => { const text = e.clipboardData?.getData("text"); if (text) { processContent(text, "pasted.conf"); } }; document.addEventListener("paste", handlePaste); return () => { document.removeEventListener("paste", handlePaste); }; }, [isOpen, step, processContent]); const handleImport = useCallback(async () => { if (!vpnPreview) return; setIsImporting(true); try { const result = await invoke("import_vpn_config", { content: vpnPreview.content, filename: vpnPreview.filename, name: vpnName.trim() || null, }); setVpnImportResult(result); setStep("vpn-result"); if (result.success) { await emit("vpn-configs-changed"); } } catch (error) { toast.error( error instanceof Error ? error.message : t("vpns.import.failedGeneric"), ); } finally { setIsImporting(false); } }, [vpnPreview, vpnName, t]); return ( {t("vpns.import.title")} {step === "dropzone" && t("vpns.import.descDropzone")} {step === "vpn-preview" && t("vpns.import.descPreview")} {step === "vpn-result" && t("vpns.import.descResult")} {step === "dropzone" && (
document.getElementById("vpn-file-input")?.click()} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); document.getElementById("vpn-file-input")?.click(); } }} >

{t("vpns.import.dropzonePrompt")}

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

{t("vpns.import.pasteHint", { modKey })}

)} {step === "vpn-preview" && vpnPreview && (
{t("vpns.import.configurationLabel", { type: vpnPreview.detectedType, })}
{vpnPreview.endpoint && (
{t("vpns.import.endpointLabel", { endpoint: vpnPreview.endpoint, })}
)}
{ setVpnName(e.target.value); }} />
                  {vpnPreview.content.slice(0, 1000)}
                  {vpnPreview.content.length > 1000 && "..."}
                
)} {step === "vpn-result" && vpnImportResult && (
{vpnImportResult.success ? (
{t("vpns.import.importedSuccess")}
{vpnImportResult.name} ({vpnImportResult.vpn_type})
) : (
{t("vpns.import.importFailed")}
{vpnImportResult.error}
)}
)} {step === "dropzone" && ( {t("common.buttons.cancel")} )} {step === "vpn-preview" && ( <> {t("common.buttons.back")} void handleImport()} > {t("vpns.import.importButton")} )} {step === "vpn-result" && ( {t("vpns.import.doneButton")} )}
); }