diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index bf2e0dc4..105aa6dd 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -74,6 +74,8 @@ pub struct IVerge { /// enable dns settings - this controls whether dns_config.yaml is applied pub enable_dns_settings: Option, + pub primary_action: Option, + /// always use default bypass pub use_default_bypass: Option, @@ -401,6 +403,7 @@ impl IVerge { enable_auto_light_weight_mode: Some(false), auto_light_weight_minutes: Some(10), enable_dns_settings: Some(false), + primary_action: Some("tun-mode".into()), home_cards: None, service_state: None, ..Self::default() @@ -489,6 +492,7 @@ impl IVerge { patch!(enable_auto_light_weight_mode); patch!(auto_light_weight_minutes); patch!(enable_dns_settings); + patch!(primary_action); patch!(home_cards); patch!(service_state); } @@ -584,6 +588,7 @@ pub struct IVergeResponse { pub enable_auto_light_weight_mode: Option, pub auto_light_weight_minutes: Option, pub enable_dns_settings: Option, + pub primary_action: Option, pub home_cards: Option, pub enable_hover_jump_navigator: Option, pub service_state: Option, @@ -656,6 +661,7 @@ impl From for IVergeResponse { enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode, auto_light_weight_minutes: verge.auto_light_weight_minutes, enable_dns_settings: verge.enable_dns_settings, + primary_action: verge.primary_action, home_cards: verge.home_cards, enable_hover_jump_navigator: verge.enable_hover_jump_navigator, service_state: verge.service_state, diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx index db2499a6..185c7c9c 100644 --- a/src/components/setting/setting-system.tsx +++ b/src/components/setting/setting-system.tsx @@ -10,36 +10,94 @@ import { useVerge } from "@/hooks/use-verge"; import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; // Ваш хук import { useSystemState } from "@/hooks/use-system-state"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; -import { uninstallService, restartCore, stopCore, invoke_uwp_tool } from "@/services/cmds"; +import { + uninstallService, + restartCore, + stopCore, + invoke_uwp_tool, +} from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; // Компоненты import { DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { Button } from "@/components/ui/button"; -import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; +import { + Tooltip, + TooltipProvider, + TooltipTrigger, + TooltipContent, +} from "@/components/ui/tooltip"; import { GuardState } from "./mods/guard-state"; // Иконки -import { Settings, PlayCircle, PauseCircle, AlertTriangle, Wrench, Trash2, Funnel, Monitor, Power, BellOff, Repeat } from "lucide-react"; +import { + Settings, + PlayCircle, + PauseCircle, + AlertTriangle, + Wrench, + Trash2, + Funnel, + Monitor, + Power, + BellOff, + Repeat, +} from "lucide-react"; // Модальные окна import { SysproxyViewer } from "./mods/sysproxy-viewer"; import { TunViewer } from "./mods/tun-viewer"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; const isWIN = getSystem() === "windows"; -interface Props { onError?: (err: Error) => void; } +interface Props { + onError?: (err: Error) => void; +} -const SettingRow = ({ label, extra, children, onClick }: { label: React.ReactNode; extra?: React.ReactNode; children?: React.ReactNode; onClick?: () => void; }) => ( -
-
{label}
{extra &&
{extra}
}
-
{children}
+const SettingRow = ({ + label, + extra, + children, + onClick, +}: { + label: React.ReactNode; + extra?: React.ReactNode; + children?: React.ReactNode; + onClick?: () => void; +}) => ( +
+
+
{label}
+ {extra &&
{extra}
}
+
{children}
+
); -const LabelWithIcon = ({ icon, text }: { icon: React.ElementType, text: string }) => { - const Icon = icon; - return ( {text} ); +const LabelWithIcon = ({ + icon, + text, +}: { + icon: React.ElementType; + text: string; +}) => { + const Icon = icon; + return ( + + + {text} + + ); }; const SettingSystem = ({ onError }: Props) => { @@ -59,41 +117,55 @@ const SettingSystem = ({ onError }: Props) => { const sysproxyRef = useRef(null); const tunRef = useRef(null); - const { enable_tun_mode, enable_auto_launch, enable_silent_start } = verge ?? {}; + const { enable_tun_mode, enable_auto_launch, enable_silent_start } = + verge ?? {}; const onSwitchFormat = (val: boolean) => val; const onChangeData = (patch: Partial) => { mutateVerge({ ...verge, ...patch }, false); }; - const handleServiceOperation = useLockFn(async ({ beforeMsg, action, actionMsg, successMsg }: { beforeMsg: string; action: () => Promise; actionMsg: string; successMsg: string; }) => { - try { - showNotice("info", beforeMsg); - await stopCore(); - showNotice("info", actionMsg); - await action(); - showNotice("success", successMsg); - showNotice("info", t("Restarting Core...")); - await restartCore(); - await mutateRunningMode(); - } catch (err: any) { - showNotice("error", err.message || err.toString()); + const handleServiceOperation = useLockFn( + async ({ + beforeMsg, + action, + actionMsg, + successMsg, + }: { + beforeMsg: string; + action: () => Promise; + actionMsg: string; + successMsg: string; + }) => { try { - showNotice("info", t("Try running core as Sidecar...")); + showNotice("info", beforeMsg); + await stopCore(); + showNotice("info", actionMsg); + await action(); + showNotice("success", successMsg); + showNotice("info", t("Restarting Core...")); await restartCore(); await mutateRunningMode(); - } catch (e: any) { - showNotice("error", e?.message || e?.toString()); + } catch (err: any) { + showNotice("error", err.message || err.toString()); + try { + showNotice("info", t("Try running core as Sidecar...")); + await restartCore(); + await mutateRunningMode(); + } catch (e: any) { + showNotice("error", e?.message || e?.toString()); + } } - } - }); + }, + ); - const onUninstallService = () => handleServiceOperation({ + const onUninstallService = () => + handleServiceOperation({ beforeMsg: t("Stopping Core..."), action: uninstallService, actionMsg: t("Uninstalling Service..."), successMsg: t("Service Uninstalled Successfully"), - }); + }); return (
@@ -106,10 +178,61 @@ const SettingSystem = ({ onError }: Props) => { label={} extra={
- } onClick={() => tunRef.current?.open()} /> - {!isTunAvailable &&

{t("TUN requires Service Mode or Admin Mode")}

} - {!isServiceMode && !isAdminMode &&

{t("Install Service")}

} - {isServiceMode &&

{t("Uninstall Service")}

} + } + onClick={() => tunRef.current?.open()} + /> + {!isTunAvailable && ( + + + + + + +

{t("TUN requires Service Mode or Admin Mode")}

+
+
+
+ )} + {!isServiceMode && !isAdminMode && ( + + + + + + +

{t("Install Service")}

+
+
+
+ )} + {isServiceMode && ( + + + + + + +

{t("Uninstall Service")}

+
+
+
+ )}
} > @@ -119,7 +242,22 @@ const SettingSystem = ({ onError }: Props) => { onChangeProps="onCheckedChange" onFormat={onSwitchFormat} onChange={(e) => onChangeData({ enable_tun_mode: e })} - onGuard={(e) => { if (!isTunAvailable) { showNotice("error", t("TUN requires Service Mode or Admin Mode")); return Promise.reject(new Error(t("TUN requires Service Mode or Admin Mode"))); } return patchVerge({ enable_tun_mode: e }); }} + onGuard={(e) => { + if (!isTunAvailable) { + showNotice( + "error", + t("TUN requires Service Mode or Admin Mode"), + ); + return Promise.reject( + new Error(t("TUN requires Service Mode or Admin Mode")), + ); + } + if (e) { + return patchVerge({ enable_tun_mode: true, enable_system_proxy: false }); + } else { + return patchVerge({ enable_tun_mode: false }); + } + }} onCatch={onError} > @@ -130,19 +268,54 @@ const SettingSystem = ({ onError }: Props) => { label={} extra={
- } onClick={() => sysproxyRef.current?.open()} /> - {systemProxyIndicator ? : } + } + onClick={() => sysproxyRef.current?.open()} + /> + {systemProxyIndicator ? ( + + ) : ( + + )}
} > - toggleSystemProxy(e)} onCatch={onError}> + { + if (e) { + patchVerge({ enable_tun_mode: false }); + return toggleSystemProxy(true); + } else { + return toggleSystemProxy(false); + } + }} + onCatch={onError} + > } - extra={isAdminMode &&

{t("Administrator mode may not support auto launch")}

} + extra={ + isAdminMode && ( + + + + + + +

{t("Administrator mode may not support auto launch")}

+
+
+
+ ) + } > { onChangeProps="onCheckedChange" onFormat={onSwitchFormat} onChange={(e) => onChangeData({ enable_auto_launch: e })} - onGuard={async (e) => { if (isAdminMode) { showNotice("info", t("Administrator mode may not support auto launch")); } try { onChangeData({ enable_auto_launch: e }); await patchVerge({ enable_auto_launch: e }); await mutate("getAutoLaunchStatus"); return Promise.resolve(); } catch (error) { onChangeData({ enable_auto_launch: !e }); return Promise.reject(error); } }} + onGuard={async (e) => { + if (isAdminMode) { + showNotice( + "info", + t("Administrator mode may not support auto launch"), + ); + } + try { + onChangeData({ enable_auto_launch: e }); + await patchVerge({ enable_auto_launch: e }); + await mutate("getAutoLaunchStatus"); + return Promise.resolve(); + } catch (error) { + onChangeData({ enable_auto_launch: !e }); + return Promise.reject(error); + } + }} onCatch={onError} >
- } extra={}> + } + extra={} + > { + } + > + val} + onGuard={(value) => + patchVerge({ + primary_action: value as "tun-mode" | "system-proxy", + }) + } + onCatch={onError} + > + + +
); diff --git a/src/locales/en.json b/src/locales/en.json index 2814824f..e2072306 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -658,5 +658,6 @@ "Get Started": "Get Started", "You don't have any profiles yet. Add your first one to begin.": "You don't have any profiles yet.\nAdd your first one to begin.", "Show Advanced Settings": "Show Advanced Settings", - "Hide Advanced Settings": "Hide Advanced Settings" + "Hide Advanced Settings": "Hide Advanced Settings", + "Main Toggle Action": "Main Toggle Action" } diff --git a/src/locales/ru.json b/src/locales/ru.json index f07b8602..cba1561c 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -610,5 +610,6 @@ "Get Started": "Приступить к работе", "You don't have any profiles yet. Add your first one to begin.": "У вас еще нет профилей.\nДобавьте свой первый профиль, чтобы начать.", "Show Advanced Settings": "Показать дополнительные настройки", - "Hide Advanced Settings": "Скрыть дополнительные настройки" + "Hide Advanced Settings": "Скрыть дополнительные настройки", + "Main Toggle Action": "Действие главного переключателя" } diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 2eaf6060..4ffe3300 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,55 +1,77 @@ -import React, { useRef, useMemo, useCallback, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useLockFn } from 'ahooks'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; +import React, { useRef, useMemo, useCallback, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; -import { useProfiles } from '@/hooks/use-profiles'; -import { ProfileViewer, ProfileViewerRef } from '@/components/profile/profile-viewer'; +import { useProfiles } from "@/hooks/use-profiles"; import { - DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Button } from '@/components/ui/button'; -import { ChevronsUpDown, Check, PlusCircle, Menu, Wrench, AlertTriangle, Loader2 } from 'lucide-react'; -import { useVerge } from '@/hooks/use-verge'; -import { useSystemState } from '@/hooks/use-system-state'; -import { useServiceInstaller } from '@/hooks/useServiceInstaller'; + ProfileViewer, + ProfileViewerRef, +} from "@/components/profile/profile-viewer"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { + ChevronsUpDown, + Check, + PlusCircle, + Menu, + Wrench, + AlertTriangle, + Loader2, +} from "lucide-react"; +import { useVerge } from "@/hooks/use-verge"; +import { useSystemState } from "@/hooks/use-system-state"; +import { useServiceInstaller } from "@/hooks/useServiceInstaller"; import { Switch } from "@/components/ui/switch"; -import { ProxySelectors } from '@/components/home/proxy-selectors'; +import { ProxySelectors } from "@/components/home/proxy-selectors"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { closeAllConnections } from '@/services/api'; - +import { closeAllConnections } from "@/services/api"; const MinimalHomePage: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [isToggling, setIsToggling] = useState(false); - const { profiles, patchProfiles, activateSelected, mutateProfiles } = useProfiles(); + const { profiles, patchProfiles, activateSelected, mutateProfiles } = + useProfiles(); const viewerRef = useRef(null); const profileItems = useMemo(() => { - const items = profiles && Array.isArray(profiles.items) ? profiles.items : []; + const items = + profiles && Array.isArray(profiles.items) ? profiles.items : []; const allowedTypes = ["local", "remote"]; return items.filter((i: any) => i && allowedTypes.includes(i.type!)); }, [profiles]); const currentProfileName = useMemo(() => { - return profileItems.find(p => p.uid === profiles?.current)?.name || profiles?.current; + return ( + profileItems.find((p) => p.uid === profiles?.current)?.name || + profiles?.current + ); }, [profileItems, profiles?.current]); - const activateProfile = useCallback(async (uid: string, notifySuccess: boolean) => { - try { - await patchProfiles({ current: uid }); - await closeAllConnections(); - await activateSelected(); - if (notifySuccess) { - toast.success(t("Profile Switched")); + const activateProfile = useCallback( + async (uid: string, notifySuccess: boolean) => { + try { + await patchProfiles({ current: uid }); + await closeAllConnections(); + await activateSelected(); + if (notifySuccess) { + toast.success(t("Profile Switched")); + } + } catch (err: any) { + toast.error(err.message || err.toString()); + mutateProfiles(); } - } catch (err: any) { - toast.error(err.message || err.toString()); - mutateProfiles(); - } - }, [patchProfiles, activateSelected, mutateProfiles, t]); + }, + [patchProfiles, activateSelected, mutateProfiles, t], + ); const handleProfileChange = useLockFn(async (uid: string) => { if (profiles?.current === uid) return; @@ -61,33 +83,55 @@ const MinimalHomePage: React.FC = () => { const { installServiceAndRestartCore } = useServiceInstaller(); const isTunAvailable = isServiceMode || isAdminMode; const isProxyEnabled = verge?.enable_system_proxy || verge?.enable_tun_mode; + const showTunAlert = + (verge?.primary_action ?? "tun-mode") === "tun-mode" && !isTunAvailable; const handleToggleProxy = useLockFn(async () => { const turningOn = !isProxyEnabled; + const primaryAction = verge?.primary_action || "tun-mode"; setIsToggling(true); + try { if (turningOn) { - await patchVerge({ enable_tun_mode: true, enable_system_proxy: false }); - toast.success(t('Proxy enabled')); + if (primaryAction === "tun-mode") { + if (!isTunAvailable) { + toast.error(t("TUN requires Service Mode or Admin Mode")); + setIsToggling(false); + return; + } + await patchVerge({ + enable_tun_mode: true, + enable_system_proxy: false, + }); + } else { + await patchVerge({ + enable_system_proxy: true, + enable_tun_mode: false, + }); + } + toast.success(t("Proxy enabled")); } else { - await patchVerge({ enable_tun_mode: false, enable_system_proxy: false }); - toast.success(t('Proxy disabled')); + await patchVerge({ + enable_tun_mode: false, + enable_system_proxy: false, + }); + toast.success(t("Proxy disabled")); } mutateVerge(); } catch (error: any) { - toast.error(t('Failed to toggle proxy'), { description: error.message }); + toast.error(t("Failed to toggle proxy"), { description: error.message }); } finally { setIsToggling(false); } }); const navMenuItems = [ - { label: 'Profiles', path: '/profile' }, - { label: 'Settings', path: '/settings' }, - { label: 'Logs', path: '/logs' }, - { label: 'Proxies', path: '/proxies' }, - { label: 'Connections', path: '/connections' }, - { label: 'Rules', path: '/rules' }, + { label: "Profiles", path: "/profile" }, + { label: "Settings", path: "/settings" }, + { label: "Logs", path: "/logs" }, + { label: "Proxies", path: "/proxies" }, + { label: "Connections", path: "/connections" }, + { label: "Rules", path: "/rules" }, ]; return ( @@ -99,7 +143,10 @@ const MinimalHomePage: React.FC = () => {
- @@ -108,15 +155,20 @@ const MinimalHomePage: React.FC = () => { {t("Profiles")} {profileItems.map((p) => ( - handleProfileChange(p.uid)}> + handleProfileChange(p.uid)} + > {p.name} - {profiles?.current === p.uid && } + {profiles?.current === p.uid && ( + + )} ))} viewerRef.current?.create()}> - {t("Add New Profile")} + {t("Add Profile")} @@ -124,61 +176,74 @@ const MinimalHomePage: React.FC = () => { )}
- - - - - - {t("Menu")} - - {navMenuItems.map((item) => ( - navigate(item.path)}> - {t(item.label)} - - ))} - - + + + + + + {t("Menu")} + + {navMenuItems.map((item) => ( + navigate(item.path)} + > + {t(item.label)} + + ))} + +
-
-

- {isProxyEnabled ? t('Connected') : t('Disconnected')} +

+ {isProxyEnabled ? t("Connected") : t("Disconnected")}

- {isToggling && (isProxyEnabled ? t('Disconnecting...') : t('Connecting...'))} + {isToggling && + (isProxyEnabled ? t("Disconnecting...") : t("Connecting..."))}

- {!isTunAvailable && ( + {showTunAlert && (
- - - {t("Attention Required")} - - {t("TUN requires Service Mode or Admin Mode")} - - {!isServiceMode && !isAdminMode && ( - - )} - + + + {t("Attention Required")} + + {t("TUN requires Service Mode or Admin Mode")} + + {!isServiceMode && !isAdminMode && ( + + )} +
)} @@ -190,15 +255,19 @@ const MinimalHomePage: React.FC = () => { {t("Get Started")} - {t("You don't have any profiles yet. Add your first one to begin.")} + {t( + "You don't have any profiles yet. Add your first one to begin.", + )} - )}
-
diff --git a/src/services/types.d.ts b/src/services/types.d.ts index 4bd03235..823c21b1 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -748,6 +748,7 @@ interface IVergeConfig { enable_system_proxy?: boolean; enable_global_hotkey?: boolean; enable_dns_settings?: boolean; + primary_action?: "tun-mode" | "system-proxy"; proxy_auto_config?: boolean; pac_file_content?: string; proxy_host?: string;