"use client"; import { invoke } from "@tauri-apps/api/core"; import { emit, listen } from "@tauri-apps/api/event"; import { useCallback, useEffect, useState } from "react"; import { GoPlus } from "react-icons/go"; import { LuDownload, LuPencil, LuTrash2, LuUpload } from "react-icons/lu"; import { toast } from "sonner"; import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; import { ProxyExportDialog } from "@/components/proxy-export-dialog"; import { ProxyFormDialog } from "@/components/proxy-form-dialog"; import { ProxyImportDialog } from "@/components/proxy-import-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { useProxyEvents } from "@/hooks/use-proxy-events"; import { useVpnEvents } from "@/hooks/use-vpn-events"; import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; import type { ProxyCheckResult, StoredProxy, VpnConfig } from "@/types"; import { ProxyCheckButton } from "./proxy-check-button"; import { RippleButton } from "./ui/ripple"; import { VpnCheckButton } from "./vpn-check-button"; import { VpnFormDialog } from "./vpn-form-dialog"; import { VpnImportDialog } from "./vpn-import-dialog"; type SyncStatus = "disabled" | "syncing" | "synced" | "error" | "waiting"; function getSyncStatusDot( item: { sync_enabled?: boolean; last_sync?: number }, liveStatus: SyncStatus | undefined, errorMessage?: string, ): { color: string; tooltip: string; animate: boolean } { const status = liveStatus ?? (item.sync_enabled ? "synced" : "disabled"); switch (status) { case "syncing": return { color: "bg-warning", tooltip: "Syncing...", animate: true }; case "synced": return { color: "bg-success", tooltip: item.last_sync ? `Synced ${new Date(item.last_sync * 1000).toLocaleString()}` : "Synced", animate: false, }; case "waiting": return { color: "bg-warning", tooltip: "Waiting to sync", animate: false, }; case "error": return { color: "bg-destructive", tooltip: errorMessage ? `Sync error: ${errorMessage}` : "Sync error", animate: false, }; default: return { color: "bg-muted-foreground", tooltip: "Not synced", animate: false, }; } } interface ProxyManagementDialogProps { isOpen: boolean; onClose: () => void; } export function ProxyManagementDialog({ isOpen, onClose, }: ProxyManagementDialogProps) { // Proxy state const [showProxyForm, setShowProxyForm] = useState(false); const [showImportDialog, setShowImportDialog] = useState(false); const [showExportDialog, setShowExportDialog] = useState(false); const [editingProxy, setEditingProxy] = useState(null); const [proxyToDelete, setProxyToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [checkingProxyId, setCheckingProxyId] = useState(null); const [proxyCheckResults, setProxyCheckResults] = useState< Record >({}); const [proxySyncStatus, setProxySyncStatus] = useState< Record >({}); const [proxySyncErrors, setProxySyncErrors] = useState< Record >({}); const [proxyInUse, setProxyInUse] = useState>({}); const [isTogglingSync, setIsTogglingSync] = useState>( {}, ); // VPN state const [showVpnForm, setShowVpnForm] = useState(false); const [showVpnImportDialog, setShowVpnImportDialog] = useState(false); const [editingVpn, setEditingVpn] = useState(null); const [vpnToDelete, setVpnToDelete] = useState(null); const [isDeletingVpn, setIsDeletingVpn] = useState(false); const [checkingVpnId, setCheckingVpnId] = useState(null); const [vpnSyncStatus, setVpnSyncStatus] = useState< Record >({}); const [vpnSyncErrors, setVpnSyncErrors] = useState>( {}, ); const [vpnInUse, setVpnInUse] = useState>({}); const [isTogglingVpnSync, setIsTogglingVpnSync] = useState< Record >({}); const { storedProxies: rawProxies, proxyUsage, isLoading } = useProxyEvents(); const { vpnConfigs, vpnUsage, isLoading: isLoadingVpns } = useVpnEvents(); // Filter out cloud-managed and cloud-derived proxies (cloud proxies are deprecated) const storedProxies = rawProxies .filter((p) => !p.is_cloud_managed && !p.is_cloud_derived) .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); // Listen for proxy sync status events useEffect(() => { let unlisten: (() => void) | undefined; const setupListener = async () => { unlisten = await listen<{ id: string; status: string; error?: string }>( "proxy-sync-status", (event) => { const { id, status, error } = event.payload; setProxySyncStatus((prev) => ({ ...prev, [id]: status as SyncStatus, })); if (error) { setProxySyncErrors((prev) => ({ ...prev, [id]: error })); } }, ); }; void setupListener(); return () => { unlisten?.(); }; }, []); // Listen for VPN sync status events useEffect(() => { let unlisten: (() => void) | undefined; const setupListener = async () => { unlisten = await listen<{ id: string; status: string; error?: string }>( "vpn-sync-status", (event) => { const { id, status, error } = event.payload; setVpnSyncStatus((prev) => ({ ...prev, [id]: status as SyncStatus, })); if (error) { setVpnSyncErrors((prev) => ({ ...prev, [id]: error })); } }, ); }; void setupListener(); return () => { unlisten?.(); }; }, []); // Load cached check results on mount and when proxies change useEffect(() => { const loadCachedResults = async () => { const results: Record = {}; const inUse: Record = {}; for (const proxy of storedProxies) { try { const cached = await invoke( "get_cached_proxy_check", { proxyId: proxy.id }, ); if (cached) { results[proxy.id] = cached; } const inUseBySynced = await invoke( "is_proxy_in_use_by_synced_profile", { proxyId: proxy.id }, ); inUse[proxy.id] = inUseBySynced; } catch (_error) { // Ignore errors } } setProxyCheckResults(results); setProxyInUse(inUse); }; if (storedProxies.length > 0) { void loadCachedResults(); } }, [storedProxies]); // Load VPN in-use status useEffect(() => { const loadVpnInUse = async () => { const inUse: Record = {}; for (const vpn of vpnConfigs) { try { const inUseBySynced = await invoke( "is_vpn_in_use_by_synced_profile", { vpnId: vpn.id }, ); inUse[vpn.id] = inUseBySynced; } catch (_error) { // Ignore errors } } setVpnInUse(inUse); }; if (vpnConfigs.length > 0) { void loadVpnInUse(); } }, [vpnConfigs]); // Proxy handlers const handleDeleteProxy = useCallback((proxy: StoredProxy) => { setProxyToDelete(proxy); }, []); const handleConfirmDelete = useCallback(async () => { if (!proxyToDelete) return; setIsDeleting(true); try { await invoke("delete_stored_proxy", { proxyId: proxyToDelete.id }); toast.success("Proxy deleted successfully"); await emit("stored-proxies-changed"); } catch (error) { console.error("Failed to delete proxy:", error); toast.error("Failed to delete proxy"); } finally { setIsDeleting(false); setProxyToDelete(null); } }, [proxyToDelete]); const handleCreateProxy = useCallback(() => { setEditingProxy(null); setShowProxyForm(true); }, []); const handleEditProxy = useCallback((proxy: StoredProxy) => { setEditingProxy(proxy); setShowProxyForm(true); }, []); const handleProxyFormClose = useCallback(() => { setShowProxyForm(false); setEditingProxy(null); }, []); const handleToggleSync = useCallback(async (proxy: StoredProxy) => { setIsTogglingSync((prev) => ({ ...prev, [proxy.id]: true })); try { await invoke("set_proxy_sync_enabled", { proxyId: proxy.id, enabled: !proxy.sync_enabled, }); showSuccessToast(proxy.sync_enabled ? "Sync disabled" : "Sync enabled"); await emit("stored-proxies-changed"); } catch (error) { console.error("Failed to toggle sync:", error); showErrorToast( error instanceof Error ? error.message : "Failed to update sync", ); } finally { setIsTogglingSync((prev) => ({ ...prev, [proxy.id]: false })); } }, []); // VPN handlers const handleDeleteVpn = useCallback((vpn: VpnConfig) => { setVpnToDelete(vpn); }, []); const handleConfirmDeleteVpn = useCallback(async () => { if (!vpnToDelete) return; setIsDeletingVpn(true); try { await invoke("delete_vpn_config", { vpnId: vpnToDelete.id }); toast.success("VPN deleted successfully"); await emit("vpn-configs-changed"); } catch (error) { console.error("Failed to delete VPN:", error); toast.error("Failed to delete VPN"); } finally { setIsDeletingVpn(false); setVpnToDelete(null); } }, [vpnToDelete]); const handleCreateVpn = useCallback(() => { setEditingVpn(null); setShowVpnForm(true); }, []); const handleEditVpn = useCallback((vpn: VpnConfig) => { setEditingVpn(vpn); setShowVpnForm(true); }, []); const handleVpnFormClose = useCallback(() => { setShowVpnForm(false); setEditingVpn(null); }, []); const handleToggleVpnSync = useCallback(async (vpn: VpnConfig) => { setIsTogglingVpnSync((prev) => ({ ...prev, [vpn.id]: true })); try { await invoke("set_vpn_sync_enabled", { vpnId: vpn.id, enabled: !vpn.sync_enabled, }); showSuccessToast(vpn.sync_enabled ? "Sync disabled" : "Sync enabled"); await emit("vpn-configs-changed"); } catch (error) { console.error("Failed to toggle VPN sync:", error); showErrorToast( error instanceof Error ? error.message : "Failed to update sync", ); } finally { setIsTogglingVpnSync((prev) => ({ ...prev, [vpn.id]: false })); } }, []); return ( <> Proxies & VPNs Manage your proxy and VPN configurations for reuse across profiles Proxies VPNs
setShowImportDialog(true)} className="flex gap-2 items-center" > Import setShowExportDialog(true)} className="flex gap-2 items-center" disabled={storedProxies.length === 0} > Export
Create
{isLoading ? (
Loading proxies...
) : storedProxies.length === 0 ? (
No proxies created yet. Create your first proxy using the button above.
) : (
Name Usage Sync Actions {storedProxies.map((proxy) => { const syncDot = getSyncStatusDot( proxy, proxySyncStatus[proxy.id], proxySyncErrors[proxy.id], ); return (

{syncDot.tooltip}

{proxy.name} {proxy.dynamic_proxy_url && ( Dynamic )}
{proxyUsage[proxy.id] ?? 0}
handleToggleSync(proxy) } disabled={ isTogglingSync[proxy.id] || proxyInUse[proxy.id] } />
{proxyInUse[proxy.id] ? (

Sync cannot be disabled while this proxy is used by synced profiles

) : (

{proxy.sync_enabled ? "Disable sync" : "Enable sync"}

)}
{ setProxyCheckResults((prev) => ({ ...prev, [proxy.id]: result, })); }} onCheckFailed={(result) => { setProxyCheckResults((prev) => ({ ...prev, [proxy.id]: result, })); }} />

Edit proxy

{(proxyUsage[proxy.id] ?? 0) > 0 ? (

Cannot delete: in use by{" "} {proxyUsage[proxy.id]} profile {proxyUsage[proxy.id] > 1 ? "s" : ""}

) : (

Delete proxy

)}
); })}
)}
setShowVpnImportDialog(true)} className="flex gap-2 items-center" > Import
Create
{isLoadingVpns ? (
Loading VPNs...
) : vpnConfigs.length === 0 ? (
No VPN configs created yet. Import or create one using the buttons above.
) : (
Name Type Usage Sync Actions {vpnConfigs.map((vpn) => { const syncDot = getSyncStatusDot( vpn, vpnSyncStatus[vpn.id], vpnSyncErrors[vpn.id], ); return (

{syncDot.tooltip}

{vpn.name}
{vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"} {vpnUsage[vpn.id] ?? 0}
handleToggleVpnSync(vpn) } disabled={ isTogglingVpnSync[vpn.id] || vpnInUse[vpn.id] } />
{vpnInUse[vpn.id] ? (

Sync cannot be disabled while this VPN is used by synced profiles

) : (

{vpn.sync_enabled ? "Disable sync" : "Enable sync"}

)}

Edit VPN

{(vpnUsage[vpn.id] ?? 0) > 0 ? (

Cannot delete: in use by{" "} {vpnUsage[vpn.id]} profile {vpnUsage[vpn.id] > 1 ? "s" : ""}

) : (

Delete VPN

)}
); })}
)}
Close
setProxyToDelete(null)} onConfirm={handleConfirmDelete} title="Delete Proxy" description={`This action cannot be undone. This will permanently delete the proxy "${proxyToDelete?.name ?? ""}".`} confirmButtonText="Delete" isLoading={isDeleting} /> setShowImportDialog(false)} /> setShowExportDialog(false)} /> setVpnToDelete(null)} onConfirm={handleConfirmDeleteVpn} title="Delete VPN" description={`This action cannot be undone. This will permanently delete the VPN "${vpnToDelete?.name ?? ""}".`} confirmButtonText="Delete" isLoading={isDeletingVpn} /> setShowVpnImportDialog(false)} /> ); }