From 8657cedca0ff2ab4ce1646da5cf6c00f6190eb62 Mon Sep 17 00:00:00 2001 From: Sline Date: Thu, 23 Oct 2025 13:14:01 +0800 Subject: [PATCH] feat: add configurable hover jump navigator delay (#5178) * fix: hover options * feat: add configurable hover jump navigator delay - Added `hover_jump_navigator_delay` to Verge config defaults, patch flow, and response payload for persistent app-wide settings. - Made proxy navigator respect configurable delay via `DEFAULT_HOVER_DELAY` and new `hoverDelay` prop. - Threaded stored delay through proxy list so hover scrolling uses Verge-configured value. - Added "Hover Jump Navigator Delay" control in Layout settings with clamped numeric input, tooltip, and toggle-aware disabling. - Localized new labels in English, Simplified Chinese, and Traditional Chinese. - Extended frontend Verge config type to include delay field for type-safe access. * docs: UPDATELOG.md --- UPDATELOG.md | 3 +- src-tauri/src/config/verge.rs | 7 ++ .../proxy/proxy-group-navigator.tsx | 47 ++++++++++++-- src/components/proxy/proxy-groups.tsx | 17 +++-- src/components/setting/mods/layout-viewer.tsx | 65 ++++++++++++++++++- src/locales/en.json | 2 + src/locales/zh.json | 2 + src/locales/zhtw.json | 4 ++ src/services/types.d.ts | 1 + 9 files changed, 135 insertions(+), 13 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index 82c4129d..e8a040fb 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -38,7 +38,8 @@ - 启用 TUN 前等待服务就绪 - 卸载 TUN 时会先关闭 - 优化应用启动页 -- 优化首页当前节点对MATCH规则的支持 +- 优化首页当前节点对 MATCH 规则的支持 +- 允许在 `界面设置` 修改 `悬浮跳转导航延迟` ### 🐞 修复问题 diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 083d071f..8533231e 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -217,6 +217,9 @@ pub struct IVerge { /// 启用代理页面自动滚动 pub enable_hover_jump_navigator: Option, + /// 代理页面自动滚动延迟(毫秒) + pub hover_jump_navigator_delay: Option, + /// 启用外部控制器 pub enable_external_controller: Option, } @@ -387,6 +390,7 @@ impl IVerge { enable_auto_launch: Some(false), enable_silent_start: Some(false), enable_hover_jump_navigator: Some(true), + hover_jump_navigator_delay: Some(280), enable_system_proxy: Some(false), proxy_auto_config: Some(false), pac_file_content: Some(DEFAULT_PAC.into()), @@ -468,6 +472,7 @@ impl IVerge { patch!(enable_auto_launch); patch!(enable_silent_start); patch!(enable_hover_jump_navigator); + patch!(hover_jump_navigator_delay); #[cfg(not(target_os = "windows"))] patch!(verge_redir_port); #[cfg(not(target_os = "windows"))] @@ -610,6 +615,7 @@ pub struct IVergeResponse { pub enable_dns_settings: Option, pub home_cards: Option, pub enable_hover_jump_navigator: Option, + pub hover_jump_navigator_delay: Option, pub enable_external_controller: Option, } @@ -686,6 +692,7 @@ impl From for IVergeResponse { enable_dns_settings: verge.enable_dns_settings, home_cards: verge.home_cards, enable_hover_jump_navigator: verge.enable_hover_jump_navigator, + hover_jump_navigator_delay: verge.hover_jump_navigator_delay, enable_external_controller: verge.enable_external_controller, } } diff --git a/src/components/proxy/proxy-group-navigator.tsx b/src/components/proxy/proxy-group-navigator.tsx index c27750b5..1ca67a75 100644 --- a/src/components/proxy/proxy-group-navigator.tsx +++ b/src/components/proxy/proxy-group-navigator.tsx @@ -1,11 +1,15 @@ import { Box, Button, Tooltip } from "@mui/material"; -import { useCallback, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; interface ProxyGroupNavigatorProps { proxyGroupNames: string[]; onGroupLocation: (groupName: string) => void; + enableHoverJump?: boolean; + hoverDelay?: number; } +export const DEFAULT_HOVER_DELAY = 280; + // 提取代理组名的第一个字符 const getGroupDisplayChar = (groupName: string): string => { if (!groupName) return "?"; @@ -18,28 +22,58 @@ const getGroupDisplayChar = (groupName: string): string => { export const ProxyGroupNavigator = ({ proxyGroupNames, onGroupLocation, + enableHoverJump = true, + hoverDelay = DEFAULT_HOVER_DELAY, }: ProxyGroupNavigatorProps) => { const lastHoveredRef = useRef(null); + const hoverTimerRef = useRef | null>(null); + + const hoverDelayMs = hoverDelay >= 0 ? hoverDelay : 0; + + const clearHoverTimer = useCallback(() => { + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + hoverTimerRef.current = null; + } + }, []); + + useEffect(() => { + if (!enableHoverJump) { + clearHoverTimer(); + lastHoveredRef.current = null; + } + return () => { + clearHoverTimer(); + }; + }, [clearHoverTimer, enableHoverJump]); const handleGroupClick = useCallback( (groupName: string) => { + clearHoverTimer(); + lastHoveredRef.current = groupName; onGroupLocation(groupName); }, - [onGroupLocation], + [clearHoverTimer, onGroupLocation], ); const handleGroupHover = useCallback( (groupName: string) => { + if (!enableHoverJump) return; if (lastHoveredRef.current === groupName) return; - lastHoveredRef.current = groupName; - onGroupLocation(groupName); + clearHoverTimer(); + hoverTimerRef.current = setTimeout(() => { + hoverTimerRef.current = null; + lastHoveredRef.current = groupName; + onGroupLocation(groupName); + }, hoverDelayMs); }, - [onGroupLocation], + [clearHoverTimer, enableHoverJump, hoverDelayMs, onGroupLocation], ); const handleButtonLeave = useCallback(() => { + clearHoverTimer(); lastHoveredRef.current = null; - }, []); + }, [clearHoverTimer]); // 处理代理组数据,去重和排序 const processedGroups = useMemo(() => { @@ -84,6 +118,7 @@ export const ProxyGroupNavigator = ({ onMouseEnter={() => handleGroupHover(name)} onFocus={() => handleGroupHover(name)} onMouseLeave={handleButtonLeave} + onBlur={handleButtonLeave} sx={{ minWidth: 28, minHeight: 28, diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index 535458af..eec3f2e8 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -25,7 +25,10 @@ import { BaseEmpty } from "../base"; import { ScrollTopButton } from "../layout/scroll-top-button"; import { ProxyChain } from "./proxy-chain"; -import { ProxyGroupNavigator } from "./proxy-group-navigator"; +import { + ProxyGroupNavigator, + DEFAULT_HOVER_DELAY, +} from "./proxy-group-navigator"; import { ProxyRender } from "./proxy-render"; import { useRenderList } from "./use-render-list"; @@ -515,10 +518,12 @@ export const ProxyGroups = (props: Props) => { anchorEl={ruleMenuAnchor} open={Boolean(ruleMenuAnchor)} onClose={handleGroupMenuClose} - PaperProps={{ - sx: { - maxHeight: 300, - minWidth: 200, + slotProps={{ + paper: { + sx: { + maxHeight: 300, + minWidth: 200, + }, }, }} > @@ -569,6 +574,8 @@ export const ProxyGroups = (props: Props) => { )} diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index 980f161c..0c9d2c5a 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -1,11 +1,13 @@ import { Box, Button, + InputAdornment, List, ListItem, ListItemText, MenuItem, Select, + TextField, styled, } from "@mui/material"; import { convertFileSrc } from "@tauri-apps/api/core"; @@ -17,6 +19,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; +import { DEFAULT_HOVER_DELAY } from "@/components/proxy/proxy-group-navigator"; import { useVerge } from "@/hooks/use-verge"; import { useWindowDecorations } from "@/hooks/use-window"; import { copyIconFile, getAppDir } from "@/services/cmds"; @@ -27,6 +30,13 @@ import { GuardState } from "./guard-state"; const OS = getSystem(); +const clampHoverDelay = (value: number) => { + if (!Number.isFinite(value)) { + return DEFAULT_HOVER_DELAY; + } + return Math.min(5000, Math.max(0, Math.round(value))); +}; + const getIcons = async (icon_dir: string, name: string) => { const updateTime = localStorage.getItem(`icon_${name}_update_time`) || ""; @@ -39,7 +49,7 @@ const getIcons = async (icon_dir: string, name: string) => { }; }; -export const LayoutViewer = forwardRef((props, ref) => { +export const LayoutViewer = forwardRef((_, ref) => { const { t } = useTranslation(); const { verge, patchVerge, mutateVerge } = useVerge(); @@ -192,6 +202,59 @@ export const LayoutViewer = forwardRef((props, ref) => { + + + {t("Hover Jump Navigator Delay")} + + + } + /> + clampHoverDelay(Number(e.target.value))} + onChange={(value) => + onChangeData({ + hover_jump_navigator_delay: clampHoverDelay(value), + }) + } + onGuard={(value) => + patchVerge({ hover_jump_navigator_delay: clampHoverDelay(value) }) + } + > + + {t("millis")} + + ), + }, + htmlInput: { + min: 0, + max: 5000, + step: 20, + }, + }} + /> + + + ; enable_hover_jump_navigator?: boolean; + hover_jump_navigator_delay?: number; enable_external_controller?: boolean; }