"use client"; import { invoke } from "@tauri-apps/api/core"; import { emit, listen } from "@tauri-apps/api/event"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; 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, t: (key: string, options?: Record) => string, 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: t("syncTooltips.syncing"), animate: true, }; case "synced": return { color: "bg-success", tooltip: item.last_sync ? t("syncTooltips.syncedAt", { time: new Date(item.last_sync * 1000).toLocaleString(), }) : t("syncTooltips.synced"), animate: false, }; case "waiting": return { color: "bg-warning", tooltip: t("syncTooltips.waiting"), animate: false, }; case "error": return { color: "bg-destructive", tooltip: errorMessage ? t("syncTooltips.errorWith", { error: errorMessage }) : t("syncTooltips.error"), animate: false, }; default: return { color: "bg-muted-foreground", tooltip: t("syncTooltips.notSynced"), animate: false, }; } } interface ProxyManagementDialogProps { isOpen: boolean; onClose: () => void; } export function ProxyManagementDialog({ isOpen, onClose, }: ProxyManagementDialogProps) { const { t } = useTranslation(); // 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(t("proxies.management.deleteSuccess")); await emit("stored-proxies-changed"); } catch (error) { console.error("Failed to delete proxy:", error); toast.error(t("proxies.management.deleteFailed")); } finally { setIsDeleting(false); setProxyToDelete(null); } }, [proxyToDelete, t]); 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 ? t("proxies.management.syncDisabled") : t("proxies.management.syncEnabled"), ); await emit("stored-proxies-changed"); } catch (error) { console.error("Failed to toggle sync:", error); showErrorToast( error instanceof Error ? error.message : t("proxies.management.updateSyncFailed"), ); } finally { setIsTogglingSync((prev) => ({ ...prev, [proxy.id]: false })); } }, [t], ); // 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(t("vpns.management.deleteSuccess")); await emit("vpn-configs-changed"); } catch (error) { console.error("Failed to delete VPN:", error); toast.error(t("vpns.management.deleteFailed")); } finally { setIsDeletingVpn(false); setVpnToDelete(null); } }, [vpnToDelete, t]); 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 ? t("proxies.management.syncDisabled") : t("proxies.management.syncEnabled"), ); await emit("vpn-configs-changed"); } catch (error) { console.error("Failed to toggle VPN sync:", error); showErrorToast( error instanceof Error ? error.message : t("proxies.management.updateSyncFailed"), ); } finally { setIsTogglingVpnSync((prev) => ({ ...prev, [vpn.id]: false })); } }, [t], ); return ( <> {t("proxies.management.title")} {t("proxies.management.description")} {t("proxies.management.tabProxies")} {t("proxies.management.tabVpns")}
{ setShowImportDialog(true); }} className="flex gap-2 items-center" > {t("common.buttons.import")} { setShowExportDialog(true); }} className="flex gap-2 items-center" disabled={storedProxies.length === 0} > {t("common.buttons.export")}
{t("proxies.management.create")}
{isLoading ? (
{t("proxies.management.loading")}
) : storedProxies.length === 0 ? (
{t("proxies.management.noneCreated")}
) : (
{t("common.labels.name")} {t("proxies.management.usage")} {t("proxies.management.syncCol")} {t("common.labels.actions")} {storedProxies.map((proxy) => { const syncDot = getSyncStatusDot( proxy, proxySyncStatus[proxy.id], t, proxySyncErrors[proxy.id], ); return (

{syncDot.tooltip}

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

{t( "proxies.management.syncCannotDisable", )}

) : (

{proxy.sync_enabled ? t( "proxies.management.disableSync", ) : t( "proxies.management.enableSync", )}

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

{t("proxies.management.editProxy")}

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

{(proxyUsage[proxy.id] ?? 0) === 1 ? t( "proxies.management.cannotDelete_one", { count: proxyUsage[proxy.id], }, ) : t( "proxies.management.cannotDelete_other", { count: proxyUsage[proxy.id], }, )}

) : (

{t( "proxies.management.deleteProxy", )}

)}
); })}
)}
{ setShowVpnImportDialog(true); }} className="flex gap-2 items-center" > {t("common.buttons.import")}
{t("proxies.management.create")}
{isLoadingVpns ? (
{t("vpns.management.loading")}
) : vpnConfigs.length === 0 ? (
{t("vpns.management.noneCreated")}
) : (
{t("common.labels.name")} {t("common.labels.type")} {t("proxies.management.usage")} {t("proxies.management.syncCol")} {t("common.labels.actions")} {vpnConfigs.map((vpn) => { const syncDot = getSyncStatusDot( vpn, vpnSyncStatus[vpn.id], t, vpnSyncErrors[vpn.id], ); return (

{syncDot.tooltip}

{vpn.name}
WG {vpnUsage[vpn.id] ?? 0}
void handleToggleVpnSync(vpn) } disabled={ isTogglingVpnSync[vpn.id] || vpnInUse[vpn.id] } />
{vpnInUse[vpn.id] ? (

{t( "vpns.management.syncCannotDisable", )}

) : (

{vpn.sync_enabled ? t( "proxies.management.disableSync", ) : t( "proxies.management.enableSync", )}

)}

{t("vpns.management.editVpn")}

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

{(vpnUsage[vpn.id] ?? 0) === 1 ? t( "vpns.management.cannotDelete_one", { count: vpnUsage[vpn.id] }, ) : t( "vpns.management.cannotDelete_other", { count: vpnUsage[vpn.id] }, )}

) : (

{t("vpns.management.deleteVpn")}

)}
); })}
)}
{t("common.buttons.close")}
{ setProxyToDelete(null); }} onConfirm={handleConfirmDelete} title={t("proxies.management.deleteTitle")} description={t("proxies.management.deleteDescription", { name: proxyToDelete?.name ?? "", })} confirmButtonText={t("common.buttons.delete")} isLoading={isDeleting} /> { setShowImportDialog(false); }} /> { setShowExportDialog(false); }} /> { setVpnToDelete(null); }} onConfirm={handleConfirmDeleteVpn} title={t("vpns.management.deleteTitle")} description={t("vpns.management.deleteDescription", { name: vpnToDelete?.name ?? "", })} confirmButtonText={t("common.buttons.delete")} isLoading={isDeletingVpn} /> { setShowVpnImportDialog(false); }} /> ); }