diff --git a/Cargo.lock b/Cargo.lock index efc878f58..c527b5c21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,6 +170,24 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ashpd" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3d60bee1a1d38c2077030f4788e1b4e31058d2e79a8cfc8f2b440bd44db290" +dependencies = [ + "async-fs 2.2.0", + "async-net 2.0.0", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "url", + "zbus", +] + [[package]] name = "ashpd" version = "0.11.0" @@ -252,6 +270,32 @@ dependencies = [ "futures-lite 1.13.0", ] +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock 3.4.1", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io 2.6.0", + "async-lock 3.4.1", + "blocking", + "futures-lite 2.6.1", + "once_cell", +] + [[package]] name = "async-io" version = "1.13.0" @@ -321,6 +365,17 @@ dependencies = [ "futures-lite 1.13.0", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io 2.6.0", + "blocking", + "futures-lite 2.6.1", +] + [[package]] name = "async-process" version = "1.8.1" @@ -385,6 +440,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 2.6.0", + "async-lock 3.4.1", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.6.1", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1124,6 +1205,7 @@ dependencies = [ "compact_str", "console-subscriber", "criterion", + "dark-light", "deelevate", "delay_timer", "dunce", @@ -1705,6 +1787,20 @@ dependencies = [ "cipher", ] +[[package]] +name = "dark-light" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e1a09f280e29a8b00bc7e81eca5ac87dca0575639c9422a5fa25a07bb884b8" +dependencies = [ + "ashpd 0.10.3", + "async-std", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "web-sys", + "winreg 0.52.0", +] + [[package]] name = "darling" version = "0.21.3" @@ -2977,6 +3073,18 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -3969,6 +4077,15 @@ dependencies = [ "selectors", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -4107,6 +4224,9 @@ name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +dependencies = [ + "value-bag", +] [[package]] name = "loom" @@ -6227,7 +6347,7 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ - "ashpd", + "ashpd 0.11.0", "block2 0.6.2", "dispatch2", "glib-sys", @@ -7049,10 +7169,10 @@ checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" dependencies = [ "async-channel 1.9.0", "async-executor", - "async-fs", + "async-fs 1.6.0", "async-io 1.13.0", "async-lock 2.8.0", - "async-net", + "async-net 1.8.0", "async-process 1.8.1", "blocking", "futures-lite 1.13.0", @@ -8940,6 +9060,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -9939,6 +10065,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.55.0" diff --git a/Changelog.md b/Changelog.md index fd3ba8bbc..a5ebdc7c9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -21,7 +21,7 @@ - 修复解锁测试部分地区图标编码不正确 - 修复 IP 检测切页后强制刷新,改为仅在必要时更新 - 修复在搜索框输入不完整正则直接崩溃 -- 修复创建窗口时在非简体中文环境下的短暂闪烁 +- 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁
✨ 新增功能 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3e25a73b3..b0e58a25f 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -99,6 +99,7 @@ clash_verge_service_ipc = { version = "2.0.21", features = [ arc-swap = "1.7.1" rust-i18n = "3.1.5" rust_iso3166 = "0.1.14" +dark-light = "2.0.0" [target.'cfg(windows)'.dependencies] deelevate = { workspace = true } diff --git a/src-tauri/src/utils/resolve/window.rs b/src-tauri/src/utils/resolve/window.rs index 0e33ff73a..df0df0412 100644 --- a/src-tauri/src/utils/resolve/window.rs +++ b/src-tauri/src/utils/resolve/window.rs @@ -1,12 +1,19 @@ -use tauri::WebviewWindow; +use dark_light::{Mode as SystemTheme, detect as detect_system_theme}; +use tauri::utils::config::Color; +use tauri::{Theme, WebviewWindow}; use crate::{ config::Config, core::handle, - utils::resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT}, + utils::resolve::window_script::{INITIAL_LOADING_OVERLAY, build_window_initial_script}, }; use clash_verge_logging::{Type, logging_error}; +const DARK_BACKGROUND_COLOR: Color = Color(46, 48, 61, 255); // #2E303D +const LIGHT_BACKGROUND_COLOR: Color = Color(245, 245, 245, 255); // #F5F5F5 +const DARK_BACKGROUND_HEX: &str = "#2E303D"; +const LIGHT_BACKGROUND_HEX: &str = "#F5F5F5"; + // 定义默认窗口尺寸常量 const DEFAULT_WIDTH: f64 = 940.0; const DEFAULT_HEIGHT: f64 = 700.0; @@ -21,8 +28,37 @@ pub async fn build_new_window() -> Result { let config = Config::verge().await; let latest = config.latest_arc(); let start_page = latest.start_page.as_deref().unwrap_or("/"); + let initial_theme_mode = match latest.theme_mode.as_deref() { + Some("dark") => "dark", + Some("light") => "light", + _ => "system", + }; - match tauri::WebviewWindowBuilder::new( + let resolved_theme = match initial_theme_mode { + "dark" => Some(Theme::Dark), + "light" => Some(Theme::Light), + _ => None, + }; + + let prefers_dark_background = match resolved_theme { + Some(Theme::Dark) => true, + Some(Theme::Light) => false, + _ => !matches!(detect_system_theme().ok(), Some(SystemTheme::Light)), + }; + + let background_color = if prefers_dark_background { + DARK_BACKGROUND_COLOR + } else { + LIGHT_BACKGROUND_COLOR + }; + + let initial_script = build_window_initial_script( + initial_theme_mode, + DARK_BACKGROUND_HEX, + LIGHT_BACKGROUND_HEX, + ); + + let mut builder = tauri::WebviewWindowBuilder::new( app_handle, "main", /* the unique window label */ tauri::WebviewUrl::App(start_page.into()), @@ -34,11 +70,21 @@ pub async fn build_new_window() -> Result { .fullscreen(false) .inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT) .min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT) - .visible(true) // 立即显示窗口,避免用户等待 - .initialization_script(WINDOW_INITIAL_SCRIPT) - .build() - { + .visible(false) // 等待主题色准备好后再展示,避免启动色差 + .initialization_script(&initial_script); + + if let Some(theme) = resolved_theme { + builder = builder.theme(Some(theme)); + } + + builder = builder.background_color(background_color); + + match builder.build() { Ok(window) => { + logging_error!( + Type::Window, + window.set_background_color(Some(background_color)) + ); logging_error!(Type::Window, window.eval(INITIAL_LOADING_OVERLAY)); Ok(window) } diff --git a/src-tauri/src/utils/resolve/window_script.rs b/src-tauri/src/utils/resolve/window_script.rs index 632b0bf86..c331dedf6 100644 --- a/src-tauri/src/utils/resolve/window_script.rs +++ b/src-tauri/src/utils/resolve/window_script.rs @@ -1,10 +1,121 @@ -pub const WINDOW_INITIAL_SCRIPT: &str = r#" +pub fn build_window_initial_script( + initial_theme_mode: &str, + dark_background: &str, + light_background: &str, +) -> String { + let theme_mode = match initial_theme_mode { + "dark" => "dark", + "light" => "light", + _ => "system", + }; + format!( + r#" + window.__VERGE_INITIAL_THEME_MODE = "{theme_mode}"; + window.__VERGE_INITIAL_THEME_COLORS = {{ + darkBg: "{dark_background}", + lightBg: "{light_background}", + }}; +{script} +"#, + theme_mode = theme_mode, + dark_background = dark_background, + light_background = light_background, + script = WINDOW_INITIAL_SCRIPT, + ) +} + +pub const WINDOW_INITIAL_SCRIPT: &str = r##" console.log('[Tauri] 窗口初始化脚本开始执行'); - function createLoadingOverlay() { + const initialColors = (() => { + try { + const colors = window.__VERGE_INITIAL_THEME_COLORS; + if (colors && typeof colors === "object") { + const { darkBg, lightBg } = colors; + if (typeof darkBg === "string" && typeof lightBg === "string") { + return { darkBg, lightBg }; + } + } + } catch (error) { + console.warn("[Tauri] 读取初始主题颜色失败:", error); + } + return { darkBg: "#2E303D", lightBg: "#F5F5F5" }; + })(); - if (document.getElementById('initial-loading-overlay')) { - console.log('[Tauri] 加载指示器已存在'); + const prefersDark = (() => { + try { + return !!window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)")?.matches; + } catch (error) { + console.warn("[Tauri] 读取系统主题失败:", error); + return false; + } + })(); + + const initialThemeMode = typeof window.__VERGE_INITIAL_THEME_MODE === "string" + ? window.__VERGE_INITIAL_THEME_MODE + : "system"; + + let initialTheme = prefersDark ? "dark" : "light"; + if (initialThemeMode === "dark") { + initialTheme = "dark"; + } else if (initialThemeMode === "light") { + initialTheme = "light"; + } + + const applyInitialTheme = (theme) => { + const isDark = theme === "dark"; + const root = document.documentElement; + const bgColor = isDark ? initialColors.darkBg : initialColors.lightBg; + const textColor = isDark ? "#ffffff" : "#333"; + if (root) { + root.dataset.theme = theme; + root.style.setProperty("--bg-color", bgColor); + root.style.setProperty("--text-color", textColor); + root.style.colorScheme = isDark ? "dark" : "light"; + root.style.backgroundColor = bgColor; + root.style.color = textColor; + } + const paintBody = () => { + if (!document.body) return; + document.body.style.backgroundColor = bgColor; + document.body.style.color = textColor; + }; + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", paintBody, { once: true }); + } else { + paintBody(); + } + try { + localStorage.setItem("verge-theme-mode-cache", theme); + } catch (error) { + console.warn("[Tauri] 缓存主题模式失败:", error); + } + return isDark; + }; + + const isDarkTheme = applyInitialTheme(initialTheme); + + const getInitialOverlayColors = () => ({ + bg: isDarkTheme ? initialColors.darkBg : initialColors.lightBg, + text: isDarkTheme ? "#ffffff" : "#333", + spinnerTrack: isDarkTheme ? "#3a3a3a" : "#e3e3e3", + spinnerTop: isDarkTheme ? "#0a84ff" : "#3498db", + }); + + function createOrUpdateLoadingOverlay() { + const colors = getInitialOverlayColors(); + const existed = document.getElementById('initial-loading-overlay'); + + const applyOverlayColors = (element) => { + element.style.setProperty("--bg-color", colors.bg); + element.style.setProperty("--text-color", colors.text); + element.style.setProperty("--spinner-track", colors.spinnerTrack); + element.style.setProperty("--spinner-top", colors.spinnerTop); + }; + + if (existed) { + console.log('[Tauri] 复用已有加载指示器'); + applyOverlayColors(existed); return; } @@ -14,7 +125,7 @@ pub const WINDOW_INITIAL_SCRIPT: &str = r#" loadingDiv.innerHTML = `
@@ -34,12 +145,11 @@ pub const WINDOW_INITIAL_SCRIPT: &str = r#" 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } - @media (prefers-color-scheme: dark) { - :root { --bg-color: #1a1a1a; --text-color: #ffffff; } - } `; + applyOverlayColors(loadingDiv); + if (document.body) { document.body.appendChild(loadingDiv); } else { @@ -51,16 +161,16 @@ pub const WINDOW_INITIAL_SCRIPT: &str = r#" } } - createLoadingOverlay(); + createOrUpdateLoadingOverlay(); if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', createLoadingOverlay); + document.addEventListener('DOMContentLoaded', createOrUpdateLoadingOverlay); } else { - createLoadingOverlay(); + createOrUpdateLoadingOverlay(); } console.log('[Tauri] 窗口初始化脚本执行完成'); -"#; +"##; pub const INITIAL_LOADING_OVERLAY: &str = r" const overlay = document.getElementById('initial-loading-overlay'); diff --git a/src/index.html b/src/index.html index ac7da5cc2..ebd340e25 100644 --- a/src/index.html +++ b/src/index.html @@ -9,8 +9,93 @@ /> Clash Verge + +
+
+
+ Loading Clash Verge... +
+
diff --git a/src/main.tsx b/src/main.tsx index 1b63d0d04..41a703b3c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -55,9 +55,45 @@ document.addEventListener("keydown", (event) => { } }); -const initializeApp = () => { +let cachedVergeConfig: IVergeConfig | null = null; + +const detectSystemTheme = (): "light" | "dark" => { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") + return "light"; + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; +}; + +const getInitialThemeModeFromWindow = (): + | IVergeConfig["theme_mode"] + | undefined => { + if (typeof window === "undefined") return undefined; + const mode = ( + window as typeof window & { + __VERGE_INITIAL_THEME_MODE?: unknown; + } + ).__VERGE_INITIAL_THEME_MODE; + if (mode === "light" || mode === "dark" || mode === "system") { + return mode; + } + return undefined; +}; + +const resolveInitialThemeMode = ( + vergeConfig?: IVergeConfig | null, +): "light" | "dark" => { + const initialMode = + vergeConfig?.theme_mode ?? getInitialThemeModeFromWindow(); + if (initialMode === "dark" || initialMode === "light") { + return initialMode; + } + return detectSystemTheme(); +}; + +const initializeApp = (initialThemeMode: "light" | "dark") => { const contexts = [ - , + , , , ]; @@ -78,24 +114,47 @@ const initializeApp = () => { ); }; -const determineInitialLanguage = async () => { +const determineInitialLanguage = async ( + vergeConfig?: IVergeConfig | null, + loadVergeConfig?: () => Promise, +) => { const cachedLanguage = getCachedLanguage(); if (cachedLanguage) { return cachedLanguage; } - try { - const vergeConfig = await getVergeConfig(); - if (vergeConfig?.language) { - const resolved = resolveLanguage(vergeConfig.language); - cacheLanguage(resolved); - return resolved; + let resolvedConfig = vergeConfig; + + if (resolvedConfig === undefined) { + if (loadVergeConfig) { + try { + resolvedConfig = await loadVergeConfig(); + } catch (error) { + console.warn( + "[main.tsx] Failed to read language from Verge config:", + error, + ); + resolvedConfig = null; + } + } else { + try { + resolvedConfig = await getVergeConfig(); + cachedVergeConfig = resolvedConfig; + } catch (error) { + console.warn( + "[main.tsx] Failed to read language from Verge config:", + error, + ); + resolvedConfig = null; + } } - } catch (error) { - console.warn( - "[main.tsx] Failed to read language from Verge config:", - error, - ); + } + + const languageFromConfig = resolvedConfig?.language; + if (languageFromConfig) { + const resolved = resolveLanguage(languageFromConfig); + cacheLanguage(resolved); + return resolved; } const browserLanguage = resolveLanguage( @@ -105,10 +164,29 @@ const determineInitialLanguage = async () => { return browserLanguage; }; +const fetchVergeConfig = async () => { + try { + const config = await getVergeConfig(); + cachedVergeConfig = config; + return config; + } catch (error) { + console.warn("[main.tsx] Failed to read Verge config:", error); + return null; + } +}; + const bootstrap = async () => { - const initialLanguage = await determineInitialLanguage(); - await initializeLanguage(initialLanguage); - initializeApp(); + const vergeConfigPromise = fetchVergeConfig(); + const initialLanguage = await determineInitialLanguage( + undefined, + () => vergeConfigPromise, + ); + const [vergeConfig] = await Promise.all([ + vergeConfigPromise, + initializeLanguage(initialLanguage), + ]); + const initialThemeMode = resolveInitialThemeMode(vergeConfig); + initializeApp(initialThemeMode); }; bootstrap().catch((error) => { @@ -124,7 +202,7 @@ bootstrap().catch((error) => { ); }) .finally(() => { - initializeApp(); + initializeApp(resolveInitialThemeMode(cachedVergeConfig)); }); }); diff --git a/src/services/states.ts b/src/services/states.ts index 895d9b9c9..e8a308fd2 100644 --- a/src/services/states.ts +++ b/src/services/states.ts @@ -4,7 +4,7 @@ import { LogLevel } from "tauri-plugin-mihomo-api"; const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState< "light" | "dark" ->("light"); +>(); export type LogFilter = "all" | "debug" | "info" | "warn" | "err"; export type LogOrder = "asc" | "desc";