diff --git a/Changelog.md b/Changelog.md index c0853940e..ad1e120f2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ - 修复设置代理端口时检查端口占用 - 修复 Monaco 编辑器初始化卡 Loading - 修复恢复备份时 `config.yaml` / `profiles.yaml` 文件内字段未正确恢复 +- 修复 Windows 下系统主题同步问题
✨ 新增功能 diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 7da555257..76dd05d07 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -22,6 +22,8 @@ use clash_verge_signal; pub mod dns; pub mod scheme; +#[cfg(target_os = "windows")] +pub mod theme; pub mod ui; pub mod window; pub mod window_script; @@ -64,6 +66,8 @@ pub fn resolve_setup_async() { init_verge_config().await; Config::verify_config_initialization().await; init_window().await; + #[cfg(target_os = "windows")] + theme::start_windows_app_theme_watcher(); let core_init = AsyncHandler::spawn(|| async { init_service_manager().await; diff --git a/src-tauri/src/utils/resolve/theme.rs b/src-tauri/src/utils/resolve/theme.rs new file mode 100644 index 000000000..6046bb3e0 --- /dev/null +++ b/src-tauri/src/utils/resolve/theme.rs @@ -0,0 +1,55 @@ +//! Windows app theme watcher. +//! +//! NOTE: +//! Tauri's theme API is unreliable on Windows and may miss or delay +//! system theme change events. As a workaround, we poll the system +//! theme via the `dark-light` crate and emit a custom +//! `verge://app-theme-changed` event to keep the frontend in sync. +//! +//! Windows-only, best-effort. + +use std::time::Duration; + +use dark_light::{Mode as SystemTheme, detect as detect_system_theme}; +use tauri::Emitter as _; + +use crate::{core::handle, process::AsyncHandler}; + +const APP_THEME_EVENT: &str = "verge://app-theme-changed"; + +fn resolve_apps_theme_mode() -> Option<&'static str> { + match detect_system_theme().ok()? { + SystemTheme::Dark => Some("dark"), + SystemTheme::Light => Some("light"), + SystemTheme::Unspecified => None, + } +} + +pub fn start_windows_app_theme_watcher() { + AsyncHandler::spawn(|| async move { + let app_handle = handle::Handle::app_handle().clone(); + let mut last_theme = resolve_apps_theme_mode(); + + if let Some(theme) = last_theme { + let _ = app_handle.emit(APP_THEME_EVENT, theme); + } + + loop { + if handle::Handle::global().is_exiting() { + break; + } + + tokio::time::sleep(Duration::from_millis(500)).await; + let Some(theme) = resolve_apps_theme_mode() else { + continue; + }; + + if last_theme.as_ref() == Some(&theme) { + continue; + } + + last_theme = Some(theme); + let _ = app_handle.emit(APP_THEME_EVENT, theme); + } + }); +} diff --git a/src/components/layout/use-custom-theme.ts b/src/components/layout/use-custom-theme.ts index d65fbce00..1376606da 100644 --- a/src/components/layout/use-custom-theme.ts +++ b/src/components/layout/use-custom-theme.ts @@ -1,4 +1,6 @@ import { alpha, createTheme, Theme as MuiTheme, Shadows } from "@mui/material"; +import { isTauri as isTauriApp } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; import { getCurrentWebviewWindow, WebviewWindow, @@ -9,6 +11,7 @@ import { useEffect, useMemo } from "react"; import { useVerge } from "@/hooks/use-verge"; import { defaultDarkTheme, defaultTheme } from "@/pages/_theme"; import { useSetThemeMode, useThemeMode } from "@/services/states"; +import getSystem from "@/utils/get-system"; const CSS_INJECTION_SCOPE_ROOT = "[data-css-injection-root]"; const CSS_INJECTION_SCOPE_LIMIT = @@ -26,6 +29,7 @@ const TOP_LEVEL_AT_RULES = [ "@color-profile", ]; let cssScopeSupport: boolean | null = null; +const OS = getSystem(); const canUseCssScope = () => { if (cssScopeSupport !== null) { @@ -76,6 +80,8 @@ export const useCustomTheme = () => { const setMode = useSetThemeMode(); const userBackgroundImage = theme_setting?.background_image || ""; const hasUserBackground = !!userBackgroundImage; + const isTauri = typeof window !== "undefined" && isTauriApp(); + const isWindows = OS === "windows"; useEffect(() => { if (theme_mode === "light" || theme_mode === "dark") { @@ -84,17 +90,7 @@ export const useCustomTheme = () => { }, [theme_mode, setMode]); useEffect(() => { - if (theme_mode !== "system") { - return; - } - - const preferBrowserMatchMedia = - typeof window !== "undefined" && - typeof window.matchMedia === "function" && - // Skip Tauri flow when running purely in browser. - !("__TAURI__" in window); - - if (preferBrowserMatchMedia) { + if (theme_mode !== "system" || !isTauri || isWindows) { return; } @@ -133,7 +129,40 @@ export const useCustomTheme = () => { console.error("Failed to unlisten from theme changes:", err); }); }; - }, [theme_mode, appWindow, setMode]); + }, [theme_mode, appWindow, setMode, isTauri, isWindows]); + + // Windows-only: Tauri's theme API is unreliable. + // Theme changes are detected in Rust and propagated via a custom event. + useEffect(() => { + if (theme_mode !== "system" || !isTauri || !isWindows) { + return; + } + + let isMounted = true; + let unlisten: (() => void) | null = null; + + listen("verge://app-theme-changed", (event) => { + if (!isMounted) return; + if (event.payload === "dark" || event.payload === "light") { + setMode(event.payload); + } + }) + .then((unlistenFn) => { + if (typeof unlistenFn === "function") { + unlisten = unlistenFn; + } + }) + .catch((err) => { + console.error("Failed to listen to app theme changes:", err); + }); + + return () => { + isMounted = false; + if (typeof unlisten === "function") { + unlisten(); + } + }; + }, [theme_mode, setMode, isTauri, isWindows]); useEffect(() => { if (theme_mode !== "system") { @@ -147,6 +176,10 @@ export const useCustomTheme = () => { return; } + if (isTauri && isWindows) { + return; + } + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const syncMode = (isDark: boolean) => setMode(isDark ? "dark" : "light"); const handleChange = (event: MediaQueryListEvent) => @@ -190,7 +223,7 @@ export const useCustomTheme = () => { ).removeListener; legacyRemoveListener?.call(legacyQuery, handleChange); }; - }, [theme_mode, setMode]); + }, [theme_mode, setMode, isTauri, isWindows]); useEffect(() => { if (theme_mode === undefined) { @@ -198,18 +231,16 @@ export const useCustomTheme = () => { } if (theme_mode === "system") { - appWindow.setTheme(null).catch((err) => { - console.error( - "Failed to set window theme to follow system (setTheme(null)):", - err, - ); + const preferredTheme = isWindows ? (mode as TauriOsTheme) : null; + appWindow.setTheme(preferredTheme).catch((err) => { + console.error("Failed to set window theme for system mode:", err); }); } else if (mode) { appWindow.setTheme(mode as TauriOsTheme).catch((err) => { console.error(`Failed to set window theme to ${mode}:`, err); }); } - }, [mode, appWindow, theme_mode]); + }, [mode, appWindow, theme_mode, isWindows]); const theme = useMemo(() => { const setting = theme_setting || {};