diff --git a/Changelog.md b/Changelog.md index 94b6045e2..c90fa75c4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,6 +15,7 @@
🚀 优化改进 +- 记住「使用系统标题栏」设置,Windows/macOS 即时生效,Linux 重启后生效 **🖥️ Windows** diff --git a/crates/clash-verge-i18n/locales/pt.yml b/crates/clash-verge-i18n/locales/pt.yml index 2373529ee..5999d1b91 100644 --- a/crates/clash-verge-i18n/locales/pt.yml +++ b/crates/clash-verge-i18n/locales/pt.yml @@ -70,6 +70,8 @@ tray: global: Global profiles: Perfis proxies: Proxies + testLatency: Test Latency + testingLatency: Testing latency… systemProxy: Proxy do sistema tunMode: Modo TUN closeAllConnections: Fechar todas as conexões diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index d6d6b8dfe..075582983 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -64,6 +64,9 @@ pub struct IVerge { pub collapse_navbar: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_system_titlebar: Option, + pub sysproxy_tray_icon: Option, pub tun_tray_icon: Option, @@ -343,6 +346,7 @@ impl IVerge { menu_icon: Some("monochrome".into()), notice_position: Some("top-right".into()), collapse_navbar: Some(false), + enable_system_titlebar: Some(true), common_tray_icon: Some(false), sysproxy_tray_icon: Some(false), tun_tray_icon: Some(false), @@ -430,6 +434,7 @@ impl IVerge { patch!(proxy_group_tools_position); patch!(notice_position); patch!(collapse_navbar); + patch!(enable_system_titlebar); patch!(common_tray_icon); patch!(sysproxy_tray_icon); patch!(tun_tray_icon); diff --git a/src-tauri/src/feat/effects.rs b/src-tauri/src/feat/effects.rs index e1ac370fb..55569aca5 100644 --- a/src-tauri/src/feat/effects.rs +++ b/src-tauri/src/feat/effects.rs @@ -67,6 +67,7 @@ verge_registry! { proxy_group_tools_position => NoEffect; notice_position => NoEffect; collapse_navbar => NoEffect; + enable_system_titlebar => NoEffect; sysproxy_tray_icon => [TrayIcon]; tun_tray_icon => [TrayIcon]; enable_tun_mode => Tun; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1df5b4582..e454d63b4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -118,9 +118,13 @@ mod app_init { /// Setup window state management pub fn setup_window_state(app: &tauri::App) -> Result<(), Box> { logging!(debug, Type::Setup, "初始化窗口状态管理..."); + // Decorations are config-driven (`enable_system_titlebar`); restoring a stale + // value here would override the builder's `.decorations(...)` on window recreate. + let state_flags = + tauri_plugin_window_state::StateFlags::all() & !tauri_plugin_window_state::StateFlags::DECORATIONS; let window_state_plugin = tauri_plugin_window_state::Builder::new() .with_filename(files::WINDOW_STATE) - .with_state_flags(tauri_plugin_window_state::StateFlags::default()) + .with_state_flags(state_flags) .build(); app.handle().plugin(window_state_plugin)?; Ok(()) diff --git a/src-tauri/src/utils/resolve/window.rs b/src-tauri/src/utils/resolve/window.rs index b8a548a00..f583c2113 100644 --- a/src-tauri/src/utils/resolve/window.rs +++ b/src-tauri/src/utils/resolve/window.rs @@ -19,11 +19,6 @@ const DEFAULT_HEIGHT: f64 = 700.0; const MINIMAL_WIDTH: f64 = 520.0; const MINIMAL_HEIGHT: f64 = 520.0; -#[cfg(target_os = "linux")] -const DEFAULT_DECORATIONS: bool = false; -#[cfg(not(target_os = "linux"))] -const DEFAULT_DECORATIONS: bool = true; - const fn restored_window_size_is_too_small(width: u32, height: u32) -> bool { width < MINIMAL_WIDTH as u32 || height < MINIMAL_HEIGHT as u32 } @@ -49,6 +44,7 @@ pub async fn build_new_window() -> Result { let config = Config::verge().await; let latest = config.latest_arc(); + let decorations = latest.enable_system_titlebar.unwrap_or(true); let start_page = latest.start_page.as_deref().unwrap_or("/"); let initial_theme_mode = match latest.theme_mode.as_deref() { Some("dark") => "dark", @@ -83,7 +79,7 @@ pub async fn build_new_window() -> Result { ) .title("Clash Verge") .center() - .decorations(DEFAULT_DECORATIONS) + .decorations(decorations) .fullscreen(false) .inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT) .min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT) diff --git a/src/assets/styles/layout.scss b/src/assets/styles/layout.scss index 6d077fde2..7995612e6 100644 --- a/src/assets/styles/layout.scss +++ b/src/assets/styles/layout.scss @@ -202,15 +202,9 @@ width: 100%; display: flex; justify-content: flex-end; - padding: 10px; box-sizing: border-box; height: 36px; border-bottom: 1px solid var(--divider-color); - - &-drag-region { - align-self: stretch; - flex: 1 1 auto; - } } .layout-content__left .the-logo { @@ -235,16 +229,9 @@ width: 100%; display: flex; justify-content: flex-start; - padding: 10px; box-sizing: border-box; height: 36px; border-bottom: 1px solid var(--divider-color); - - &-drag-region { - align-self: stretch; - flex: 1 1 auto; - order: 1; - } } .layout-content__left { diff --git a/src/components/layout/window-controller.tsx b/src/components/layout/window-controller.tsx index 1df4b5f65..461e43fd2 100644 --- a/src/components/layout/window-controller.tsx +++ b/src/components/layout/window-controller.tsx @@ -1,13 +1,13 @@ import { Close, CropSquare, FilterNone, Minimize } from '@mui/icons-material' import { Box, IconButton } from '@mui/material' +import { getCurrentWindow } from '@tauri-apps/api/window' import { - forwardRef, + type MouseEvent, type PointerEvent, useCallback, - useImperativeHandle, + useEffect, } from 'react' -import { useWindowControls } from '@/hooks/use-window' import getSystem from '@/utils/get-system' const RESIZE_HANDLES = [ @@ -21,9 +21,7 @@ const RESIZE_HANDLES = [ { direction: 'NorthWest', position: 'north-west' }, ] as const -export const WindowResizeHandles = () => { - const { currentWindow, maximized } = useWindowControls() - +export const WindowResizeHandles = ({ maximized }: { maximized: boolean }) => { const startResizeDragging = useCallback( (event: PointerEvent) => { if (event.button !== 0) return @@ -33,14 +31,14 @@ export const WindowResizeHandles = () => { const handle = RESIZE_HANDLES.find((item) => item.direction === direction) if (handle) { - void currentWindow + void getCurrentWindow() .startResizeDragging(handle.direction) .catch((error) => console.warn('[WindowResizeHandles] 调整窗口大小失败:', error), ) } }, - [currentWindow], + [], ) if (getSystem() !== 'linux' || maximized) return null @@ -63,44 +61,77 @@ export const WindowResizeHandles = () => { ) } -export const WindowControls = forwardRef(function WindowControls(props, ref) { +export const WindowControls = ({ + maximized, + setMaximized, +}: { + maximized: boolean + setMaximized: (value: boolean) => void +}) => { const OS = getSystem() - const { - currentWindow, - maximized, - minimize, - close, - toggleFullscreen, - toggleMaximize, - } = useWindowControls() - useImperativeHandle( - ref, - () => ({ - currentWindow, - maximized, - minimize, - close, - toggleFullscreen, - toggleMaximize, - }), - [ - currentWindow, - maximized, - minimize, - close, - toggleFullscreen, - toggleMaximize, - ], + const close = useCallback(async () => { + // Delay one frame so the UI can clear :hover before the window hides. + await new Promise((resolve) => setTimeout(resolve, 20)) + await getCurrentWindow().close() + }, []) + + const minimize = useCallback(async () => { + // Delay one frame so the UI can clear :hover before the window hides. + await new Promise((resolve) => setTimeout(resolve, 10)) + await getCurrentWindow().minimize() + }, []) + + const toggleMaximize = useCallback(async () => { + const currentWindow = getCurrentWindow() + if (await currentWindow.isMaximized()) { + await currentWindow.unmaximize() + setMaximized(false) + } else { + await currentWindow.maximize() + setMaximized(true) + } + }, [setMaximized]) + + useEffect(() => { + getCurrentWindow().setMinimizable(true) + }, []) + + const handleTitlebarMouseDown = useCallback( + (event: MouseEvent) => { + // Left button only, and only on the first press: detail 2 is the second + // press of a double-click, which `handleTitlebarDoubleClick` turns into maximize. + if (event.button !== 0 || event.detail !== 1) return + // Only the empty bar area drags; the control buttons keep their own clicks. + if (event.target !== event.currentTarget) return + event.preventDefault() + void getCurrentWindow() + .startDragging() + .catch((error) => console.warn('[WindowControls] 开始拖动失败:', error)) + }, + [], ) - // 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。 - // 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。 + const handleTitlebarDoubleClick = useCallback( + (event: MouseEvent) => { + if (event.target !== event.currentTarget) return + void toggleMaximize().catch((error) => + console.warn('[WindowControls] 切换最大化失败:', error), + ) + }, + [toggleMaximize], + ) return ( button': { @@ -186,4 +217,4 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) { )} ) -}) +} diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index f818d61de..9769bd0d5 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -12,16 +12,21 @@ import { } from '@mui/material' import { convertFileSrc } from '@tauri-apps/api/core' import { join } from '@tauri-apps/api/path' +import { getCurrentWindow } from '@tauri-apps/api/window' import { open as openDialog } from '@tauri-apps/plugin-dialog' import { exists } from '@tauri-apps/plugin-fs' import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' import { useTranslation } from 'react-i18next' -import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base' +import { + BaseDialog, + type DialogRef, + Switch, + TooltipIcon, +} from '@/components/base' 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' +import { copyIconFile, getAppDir, restartApp } from '@/services/cmds' import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import getSystem from '@/utils/get-system' @@ -58,8 +63,6 @@ export const LayoutViewer = forwardRef((_, ref) => { const [sysproxyIcon, setSysproxyIcon] = useState('') const [tunIcon, setTunIcon] = useState('') - const { decorated, toggleDecorations } = useWindowDecorations() - useEffect(() => { initIconPath() }, []) @@ -123,17 +126,50 @@ export const LayoutViewer = forwardRef((_, ref) => { + + {t( + 'settings.components.verge.layout.fields.enableSystemTitlebar', + )} + + + + } /> { - await toggleDecorations() + onChange={(e) => onChangeData({ enable_system_titlebar: e })} + onGuard={async (e) => { + await patchVerge({ enable_system_titlebar: e }) + if (OS === 'linux') { + void mutate(() => restartApp(), { + id: 'restart-app', + errorNotice: false, + }).catch((error) => { + console.warn('[LayoutViewer] failed to restart app:', error) + }) + } else { + const currentWindow = getCurrentWindow() + try { + await currentWindow.setDecorations(e) + // macOS clears the Miniaturizable style bit when decorations are disabled; re-enable minimize. + await currentWindow.setMinimizable(true) + } catch (error) { + console.warn( + '[LayoutViewer] failed to apply system titlebar setting:', + error, + ) + } + } }} > diff --git a/src/hooks/use-window-maximized.ts b/src/hooks/use-window-maximized.ts new file mode 100644 index 000000000..f078cae96 --- /dev/null +++ b/src/hooks/use-window-maximized.ts @@ -0,0 +1,41 @@ +import { getCurrentWindow } from '@tauri-apps/api/window' +import { useEffect, useState } from 'react' + +import debounce from '@/utils/debounce' + +export const useWindowMaximized = (enabled: boolean) => { + const [maximized, setMaximized] = useState(false) + + useEffect(() => { + if (!enabled) return + + const currentWindow = getCurrentWindow() + let isUnmounted = false + + const syncMaximized = async () => { + if (isUnmounted) return + try { + setMaximized(await currentWindow.isMaximized()) + } catch (error) { + console.warn('[useWindowMaximized] 读取最大化状态失败:', error) + } + } + + void syncMaximized() + + const unlistenPromise = currentWindow.onResized( + debounce(syncMaximized, 300), + ) + + return () => { + isUnmounted = true + unlistenPromise + .then((unlisten) => unlisten()) + .catch((err) => + console.warn('[useWindowMaximized] 清理监听器失败:', err), + ) + } + }, [enabled]) + + return { maximized, setMaximized } +} diff --git a/src/hooks/use-window.ts b/src/hooks/use-window.ts deleted file mode 100644 index 15bee2c18..000000000 --- a/src/hooks/use-window.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { use } from 'react' - -import { WindowContext, type WindowContextType } from '@/providers/window' - -const useWindow = () => { - const context = use(WindowContext) - if (context === undefined) { - throw new Error('useWindow must be used within WindowProvider') - } - return context -} - -export const useWindowControls = () => { - const { - maximized, - minimize, - toggleMaximize, - close, - toggleFullscreen, - currentWindow, - } = useWindow() - return { - maximized, - minimize, - toggleMaximize, - close, - toggleFullscreen, - currentWindow, - } satisfies Pick< - WindowContextType, - | 'maximized' - | 'minimize' - | 'toggleMaximize' - | 'close' - | 'toggleFullscreen' - | 'currentWindow' - > -} - -export const useWindowDecorations = () => { - const { decorated, toggleDecorations, refreshDecorated } = useWindow() - return { decorated, toggleDecorations, refreshDecorated } satisfies Pick< - WindowContextType, - 'decorated' | 'toggleDecorations' | 'refreshDecorated' - > -} diff --git a/src/locales/ar/settings.json b/src/locales/ar/settings.json index 379e17e96..4363356b7 100644 --- a/src/locales/ar/settings.json +++ b/src/locales/ar/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "إعدادات التخطيط", "fields": { - "preferSystemTitlebar": "Prefer System Titlebar", + "enableSystemTitlebar": "تفعيل شريط عنوان النظام", "trafficGraph": "مخطط حركة المرور", "memoryUsage": "استهلاك الذاكرة", "proxyGroupIcon": "أيقونة مجموعة الوكلاء", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "إيقاف عرض إحصاءات حركة المرور مؤقتًا عند فقدان التركيز" }, "tooltips": { + "enableSystemTitlebar": "استخدام شريط عنوان النظام الأصلي بدلاً من شريط العنوان المخصص المدمج (يلزم إعادة التشغيل على Linux).", "hoverNavigator": "Automatically scroll to the corresponding proxy group when hovering over alphabet letters", "hoverNavigatorDelay": "Delay before auto scrolling when hovering, in milliseconds" }, diff --git a/src/locales/de/settings.json b/src/locales/de/settings.json index 1556a404e..78fca0dc6 100644 --- a/src/locales/de/settings.json +++ b/src/locales/de/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "Layout-Einstellungen", "fields": { - "preferSystemTitlebar": "System-Titelleiste bevorzugen", + "enableSystemTitlebar": "System-Titelleiste aktivieren", "trafficGraph": "Verkehrsdiagramm", "memoryUsage": "Kern-Speichernutzung", "proxyGroupIcon": "Proxy-Gruppen-Symbol", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "Rendering der Verkehrsstatistiken bei Fokusverlust pausieren" }, "tooltips": { + "enableSystemTitlebar": "Die native Titelleiste des Systems anstelle der integrierten benutzerdefinierten Titelleiste verwenden (unter Linux ist ein Neustart erforderlich).", "hoverNavigator": "Beim Darüberfahren mit der Maus über die Buchstaben automatisch zur passenden Proxy-Gruppe scrollen", "hoverNavigatorDelay": "Wartezeit vor dem automatischen Scrollen beim Darüberfahren, in Millisekunden" }, diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index 277c0fc27..5ccdbb459 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "Layout Setting", "fields": { - "preferSystemTitlebar": "Prefer System Titlebar", + "enableSystemTitlebar": "Enable System Titlebar", "trafficGraph": "Traffic Graph", "memoryUsage": "Core Usage", "proxyGroupIcon": "Proxy Group Icon", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "Pause rendering traffic statistics when blurred" }, "tooltips": { + "enableSystemTitlebar": "Use the system's native title bar instead of the built-in custom title bar (restart required on Linux).", "hoverNavigator": "Automatically scroll to the corresponding proxy group when hovering over alphabet letters", "hoverNavigatorDelay": "Delay before auto scrolling when hovering, in milliseconds" }, diff --git a/src/locales/es/settings.json b/src/locales/es/settings.json index 5d7252513..64f4d0d63 100644 --- a/src/locales/es/settings.json +++ b/src/locales/es/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "Configuración de la interfaz", "fields": { - "preferSystemTitlebar": "Prefer System Titlebar", + "enableSystemTitlebar": "Activar barra de título del sistema", "trafficGraph": "Gráfico de tráfico", "memoryUsage": "Uso de memoria del núcleo", "proxyGroupIcon": "Icono del grupo de proxy", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "Pausar el renderizado de estadísticas de tráfico al perder el foco" }, "tooltips": { + "enableSystemTitlebar": "Usar la barra de título nativa del sistema en lugar de la barra de título personalizada integrada (se requiere reiniciar en Linux).", "hoverNavigator": "Automatically scroll to the corresponding proxy group when hovering over alphabet letters", "hoverNavigatorDelay": "Delay before auto scrolling when hovering, in milliseconds" }, diff --git a/src/locales/fa/settings.json b/src/locales/fa/settings.json index 79dcc96f9..b900af92d 100644 --- a/src/locales/fa/settings.json +++ b/src/locales/fa/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "تنظیمات چیدمان", "fields": { - "preferSystemTitlebar": "Prefer System Titlebar", + "enableSystemTitlebar": "فعالسازی نوار عنوان سیستم", "trafficGraph": "نمودار ترافیک", "memoryUsage": "استفاده از حافظه", "proxyGroupIcon": "آیکون گروه پراکسی", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "توقف رندر آمار ترافیک هنگام از دست رفتن فوکوس" }, "tooltips": { + "enableSystemTitlebar": "استفاده از نوار عنوان بومی سیستم بهجای نوار عنوان سفارشی داخلی (در لینوکس نیاز به راهاندازی مجدد است).", "hoverNavigator": "Automatically scroll to the corresponding proxy group when hovering over alphabet letters", "hoverNavigatorDelay": "Delay before auto scrolling when hovering, in milliseconds" }, diff --git a/src/locales/id/settings.json b/src/locales/id/settings.json index 341cf9676..9d214d2ce 100644 --- a/src/locales/id/settings.json +++ b/src/locales/id/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "Pengaturan Tata Letak", "fields": { - "preferSystemTitlebar": "Prefer System Titlebar", + "enableSystemTitlebar": "Aktifkan bilah judul sistem", "trafficGraph": "Grafik Lalu Lintas", "memoryUsage": "Penggunaan Memori", "proxyGroupIcon": "Ikon Grup Proksi", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "Jeda render statistik lalu lintas saat jendela tidak fokus" }, "tooltips": { + "enableSystemTitlebar": "Gunakan bilah judul asli sistem sebagai ganti bilah judul kustom bawaan (perlu mulai ulang di Linux).", "hoverNavigator": "Automatically scroll to the corresponding proxy group when hovering over alphabet letters", "hoverNavigatorDelay": "Delay before auto scrolling when hovering, in milliseconds" }, diff --git a/src/locales/jp/settings.json b/src/locales/jp/settings.json index 06af65f1c..1078bb69f 100644 --- a/src/locales/jp/settings.json +++ b/src/locales/jp/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "レイアウト設定", "fields": { - "preferSystemTitlebar": "Prefer System Titlebar", + "enableSystemTitlebar": "システムタイトルバーを有効にする", "trafficGraph": "トラフィックグラフ", "memoryUsage": "コアメモリ使用量", "proxyGroupIcon": "プロキシグループアイコン", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "フォーカスを失ったときにトラフィック統計の描画を一時停止" }, "tooltips": { + "enableSystemTitlebar": "内蔵のカスタムタイトルバーの代わりにシステム標準のタイトルバーを使用します(Linux では再起動が必要です)。", "hoverNavigator": "Automatically scroll to the corresponding proxy group when hovering over alphabet letters", "hoverNavigatorDelay": "Delay before auto scrolling when hovering, in milliseconds" }, diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index 6bbcf08a0..3954800e7 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "레이아웃 설정", "fields": { - "preferSystemTitlebar": "시스템 제목 표시줄 사용", + "enableSystemTitlebar": "시스템 제목 표시줄 활성화", "trafficGraph": "트래픽 그래프", "memoryUsage": "메모리 사용량", "proxyGroupIcon": "프록시 그룹 아이콘", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "포커스를 잃으면 트래픽 통계 렌더링 일시 중지" }, "tooltips": { + "enableSystemTitlebar": "내장 사용자 지정 제목 표시줄 대신 시스템 기본 제목 표시줄을 사용합니다(Linux에서는 다시 시작해야 함).", "hoverNavigator": "알파벳에 마우스를 올리면 해당 프록시 그룹으로 자동 스크롤합니다", "hoverNavigatorDelay": "호버 시 자동 스크롤까지의 지연(밀리초)" }, diff --git a/src/locales/pt/settings.json b/src/locales/pt/settings.json index 1f95164bc..e31c1fd5a 100644 --- a/src/locales/pt/settings.json +++ b/src/locales/pt/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "Configurações de Layout", "fields": { - "preferSystemTitlebar": "Preferir Barra de Título do Sistema", + "enableSystemTitlebar": "Ativar barra de título do sistema", "trafficGraph": "Gráfico de Tráfego", "memoryUsage": "Uso do Núcleo", "proxyGroupIcon": "Ícone do Grupo de Proxy", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "Pausar a renderização das estatísticas de tráfego quando sem foco" }, "tooltips": { + "enableSystemTitlebar": "Usar a barra de título nativa do sistema em vez da barra de título personalizada integrada (é necessário reiniciar no Linux).", "hoverNavigator": "Rola automaticamente até o grupo de proxy correspondente ao passar o mouse sobre as letras do alfabeto", "hoverNavigatorDelay": "Atraso antes da rolagem automática ao passar o mouse, em milissegundos" }, diff --git a/src/locales/ru/settings.json b/src/locales/ru/settings.json index 550bd9615..0667fb7c3 100644 --- a/src/locales/ru/settings.json +++ b/src/locales/ru/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "Настройки раскладки", "fields": { - "preferSystemTitlebar": "Использовать системную панель заголовка", + "enableSystemTitlebar": "Включить системную панель заголовка", "trafficGraph": "График трафика", "memoryUsage": "Использование памяти", "proxyGroupIcon": "Значок группы прокси", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "При потере фокуса приостанавливать отрисовку статистики трафика" }, "tooltips": { + "enableSystemTitlebar": "Использовать системную панель заголовка вместо встроенной настраиваемой (в Linux требуется перезапуск).", "hoverNavigator": "Автоматически прокручивает к группе прокси при наведении на буквы", "hoverNavigatorDelay": "Задержка перед прокруткой при наведении (в миллисекундах)" }, diff --git a/src/locales/tr/settings.json b/src/locales/tr/settings.json index fad3447e0..7a1cf4637 100644 --- a/src/locales/tr/settings.json +++ b/src/locales/tr/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "Düzen Ayarı", "fields": { - "preferSystemTitlebar": "Sistem Başlık Çubuğunu Tercih Et", + "enableSystemTitlebar": "Sistem başlık çubuğunu etkinleştir", "trafficGraph": "Trafik Grafiği", "memoryUsage": "Çekirdek Kullanımı", "proxyGroupIcon": "Vekil Grup Simgesi", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "Odak kaybolduğunda trafik istatistiklerinin işlenmesini duraklat" }, "tooltips": { + "enableSystemTitlebar": "Yerleşik özel başlık çubuğu yerine sistemin yerel başlık çubuğunu kullan (Linux'ta yeniden başlatma gerekir).", "hoverNavigator": "Alfabe harflerinin üzerine gelindiğinde ilgili vekil grubuna otomatik olarak kaydırır", "hoverNavigatorDelay": "Üzerine gelindiğinde otomatik kaydırmadan önceki gecikme, milisaniye cinsinden" }, diff --git a/src/locales/tt/settings.json b/src/locales/tt/settings.json index 68154eae4..6372b92d4 100644 --- a/src/locales/tt/settings.json +++ b/src/locales/tt/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "Расположение көйләүләре", "fields": { - "preferSystemTitlebar": "Prefer System Titlebar", + "enableSystemTitlebar": "Enable System Titlebar", "trafficGraph": "Трафик графигы", "memoryUsage": "Хәтер куллану", "proxyGroupIcon": "Прокси төркеме иконкасы", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "Фокус югалганда трафик статистикасын сызуны туктатып тору" }, "tooltips": { + "enableSystemTitlebar": "Use the system's native title bar instead of the built-in custom title bar (restart required on Linux).", "hoverNavigator": "Automatically scroll to the corresponding proxy group when hovering over alphabet letters", "hoverNavigatorDelay": "Delay before auto scrolling when hovering, in milliseconds" }, diff --git a/src/locales/zh/settings.json b/src/locales/zh/settings.json index 936f2cb15..c3e62e28b 100644 --- a/src/locales/zh/settings.json +++ b/src/locales/zh/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "界面设置", "fields": { - "preferSystemTitlebar": "优先使用系统标题栏", + "enableSystemTitlebar": "启用系统标题栏", "trafficGraph": "流量图显", "memoryUsage": "内核占用", "proxyGroupIcon": "代理组图标", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "失去焦点时暂停渲染流量统计" }, "tooltips": { + "enableSystemTitlebar": "使用系统原生标题栏替代内置的自定义标题栏(Linux 下需重启生效)", "hoverNavigator": "鼠标悬停在字母上时自动滚动到对应代理组", "hoverNavigatorDelay": "鼠标悬停后触发自动跳转前等待的毫秒数" }, diff --git a/src/locales/zhtw/settings.json b/src/locales/zhtw/settings.json index 569ce3347..d6644f799 100644 --- a/src/locales/zhtw/settings.json +++ b/src/locales/zhtw/settings.json @@ -231,7 +231,7 @@ "layout": { "title": "介面設定", "fields": { - "preferSystemTitlebar": "優先使用系統標題欄", + "enableSystemTitlebar": "啟用系統標題欄", "trafficGraph": "流量圖表", "memoryUsage": "內核佔用", "proxyGroupIcon": "代理組圖示", @@ -251,6 +251,7 @@ "pauseRenderTrafficStatsOnBlur": "失去焦點時暫停渲染流量統計" }, "tooltips": { + "enableSystemTitlebar": "使用系統原生標題欄取代內建的自訂標題欄(Linux 需重新啟動生效)", "hoverNavigator": "滑鼠懸停在字母上時自動捲動到對應代理組", "hoverNavigatorDelay": "滑鼠懸停後觸發自動跳轉前等待的毫秒數" }, diff --git a/src/main.tsx b/src/main.tsx index 5a0f0d76d..ead76e4e3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,7 +12,6 @@ import { BaseErrorBoundary } from './components/base' import { router } from './pages/_routers' import { preloadHomePageCards } from './pages/home' import { AppDataProvider } from './providers/app-data-provider' -import { WindowProvider } from './providers/window' import { FALLBACK_LANGUAGE, initializeLanguage } from './services/i18n' import { preloadAppData, @@ -50,11 +49,9 @@ const initializeApp = (initialThemeMode: 'light' | 'dark') => { - - - - - + + + diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 59625514e..4cb344c4b 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -1,7 +1,7 @@ import { Paper, ThemeProvider } from '@mui/material' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Outlet, useNavigate } from 'react-router' @@ -16,7 +16,7 @@ import { } from '@/components/layout/window-controller' import { useI18n } from '@/hooks/use-i18n' import { useVerge } from '@/hooks/use-verge' -import { useWindowDecorations } from '@/hooks/use-window' +import { useWindowMaximized } from '@/hooks/use-window-maximized' import { useEventBus } from '@/services/bus' import { handleNoticeMessage } from '@/services/notice-handlers' import { useThemeMode } from '@/services/states' @@ -46,21 +46,17 @@ const Layout = () => { const { switchLanguage } = useI18n() const navigate = useNavigate() const themeReady = useMemo(() => Boolean(theme), [theme]) - const windowControlsRef = useRef(null) - const { decorated } = useWindowDecorations() + const showCustomTitlebar = verge?.enable_system_titlebar === false + const { maximized, setMaximized } = useWindowMaximized(showCustomTitlebar) const customTitlebar = useMemo( () => - decorated === false ? ( + showCustomTitlebar ? (
-
- +
) : null, - [decorated], + [showCustomTitlebar, maximized, setMaximized], ) useLoadingOverlay(themeReady) @@ -154,9 +150,9 @@ const Layout = () => { : {}, ]} > - {decorated === false && } + {showCustomTitlebar && } - {/* Custom titlebar - rendered only when decorated is false, memoized for performance */} + {/* Custom titlebar - rendered only when the system titlebar is not preferred, memoized for performance */} {customTitlebar}
diff --git a/src/providers/window/index.ts b/src/providers/window/index.ts deleted file mode 100644 index 9d68cbe82..000000000 --- a/src/providers/window/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { WindowContext } from './window-context' -export type { WindowContextType } from './window-context' -export { WindowProvider } from './window-provider' diff --git a/src/providers/window/window-context.ts b/src/providers/window/window-context.ts deleted file mode 100644 index 2bd7a5905..000000000 --- a/src/providers/window/window-context.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getCurrentWindow } from '@tauri-apps/api/window' -import { createContext } from 'react' - -export interface WindowContextType { - decorated: boolean | null - maximized: boolean | null - toggleDecorations: () => Promise - refreshDecorated: () => Promise - minimize: () => Promise - close: () => Promise - toggleMaximize: () => Promise - toggleFullscreen: () => Promise - currentWindow: ReturnType -} - -export const WindowContext = createContext( - undefined, -) diff --git a/src/providers/window/window-provider.tsx b/src/providers/window/window-provider.tsx deleted file mode 100644 index 2c25325db..000000000 --- a/src/providers/window/window-provider.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { getCurrentWindow } from '@tauri-apps/api/window' -import React, { useCallback, useEffect, useMemo, useState } from 'react' - -import debounce from '@/utils/debounce' - -import { WindowContext } from './window-context' - -export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { - const currentWindow = useMemo(() => getCurrentWindow(), []) - const [decorated, setDecorated] = useState(null) - const [maximized, setMaximized] = useState(null) - - const close = useCallback(async () => { - // Delay one frame so the UI can clear :hover before the window hides. - await new Promise((resolve) => setTimeout(resolve, 20)) - await currentWindow.close() - }, [currentWindow]) - const minimize = useCallback(async () => { - // Delay one frame so the UI can clear :hover before the window hides. - await new Promise((resolve) => setTimeout(resolve, 10)) - await currentWindow.minimize() - }, [currentWindow]) - - useEffect(() => { - let isUnmounted = false - let lastWidth = -1 - let lastHeight = -1 - - const checkMaximized = debounce( - async (event: { payload: { width: number; height: number } }) => { - if (isUnmounted) return - const { width, height } = event.payload - if (width === lastWidth && height === lastHeight) return - lastWidth = width - lastHeight = height - const value = await currentWindow.isMaximized() - setMaximized(value) - }, - 300, - ) - - const unlistenPromise = currentWindow.onResized(checkMaximized) - - return () => { - isUnmounted = true - unlistenPromise - .then((unlisten) => unlisten()) - .catch((err) => console.warn('[WindowProvider] 清理监听器失败:', err)) - } - }, [currentWindow]) - - const toggleMaximize = useCallback(async () => { - if (await currentWindow.isMaximized()) { - await currentWindow.unmaximize() - setMaximized(false) - } else { - await currentWindow.maximize() - setMaximized(true) - } - }, [currentWindow]) - - const toggleFullscreen = useCallback(async () => { - await currentWindow.setFullscreen(!(await currentWindow.isFullscreen())) - }, [currentWindow]) - - const refreshDecorated = useCallback(async () => { - const val = await currentWindow.isDecorated() - setDecorated(val) - return val - }, [currentWindow]) - - const toggleDecorations = useCallback(async () => { - const currentVal = await currentWindow.isDecorated() - await currentWindow.setDecorations(!currentVal) - setDecorated(!currentVal) - }, [currentWindow]) - - useEffect(() => { - refreshDecorated() - currentWindow.setMinimizable?.(true) - }, [currentWindow, refreshDecorated]) - - const contextValue = useMemo( - () => ({ - decorated, - maximized, - toggleDecorations, - refreshDecorated, - minimize, - close, - toggleMaximize, - toggleFullscreen, - currentWindow, - }), - [ - decorated, - maximized, - toggleDecorations, - refreshDecorated, - minimize, - close, - toggleMaximize, - toggleFullscreen, - currentWindow, - ], - ) - - return {children} -} diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 4e566dce4..e47d6c320 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -533,7 +533,7 @@ export const translationKeys = [ 'settings.components.verge.theme.actions.editCss', 'settings.components.verge.theme.dialogs.editCssTitle', 'settings.components.verge.layout.title', - 'settings.components.verge.layout.fields.preferSystemTitlebar', + 'settings.components.verge.layout.fields.enableSystemTitlebar', 'settings.components.verge.layout.fields.trafficGraph', 'settings.components.verge.layout.fields.memoryUsage', 'settings.components.verge.layout.fields.proxyGroupIcon', @@ -551,6 +551,7 @@ export const translationKeys = [ 'settings.components.verge.layout.fields.enableTrayIcon', 'settings.components.verge.layout.fields.enableTraySpeed', 'settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur', + 'settings.components.verge.layout.tooltips.enableSystemTitlebar', 'settings.components.verge.layout.tooltips.hoverNavigator', 'settings.components.verge.layout.tooltips.hoverNavigatorDelay', 'settings.components.verge.layout.options.icon.monochrome', diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index 87aba3645..1c15680dd 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -746,6 +746,7 @@ export interface TranslationResources { fields: { collapseNavBar: string commonTrayIcon: string + enableSystemTitlebar: string enableTrayIcon: string enableTraySpeed: string hoverNavigator: string @@ -753,7 +754,6 @@ export interface TranslationResources { memoryUsage: string navIcon: string pauseRenderTrafficStatsOnBlur: string - preferSystemTitlebar: string proxyGroupIcon: string proxyGroupsDisplayMode: string showOutboundModesInline: string @@ -783,6 +783,7 @@ export interface TranslationResources { } title: string tooltips: { + enableSystemTitlebar: string hoverNavigator: string hoverNavigatorDelay: string } diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 4083bc95d..41ae7a164 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -876,6 +876,7 @@ interface IVergeConfig { proxy_group_tools_position?: 'left' | 'right' notice_position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' collapse_navbar?: boolean + enable_system_titlebar?: boolean tray_icon?: 'monochrome' | 'colorful' common_tray_icon?: boolean sysproxy_tray_icon?: boolean diff --git a/src/utils/get-system.ts b/src/utils/get-system.ts index 6bd17dd03..7c9ad1721 100644 --- a/src/utils/get-system.ts +++ b/src/utils/get-system.ts @@ -8,7 +8,7 @@ export default function getSystem() { if (/win64|win32/i.test(ua) || platform === 'win32') return 'windows' - if (/linux/i.test(ua)) return 'linux' + if (/linux/i.test(ua) || platform === 'linux') return 'linux' return 'unknown' }