diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 5bc4f518..4677b0d6 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -214,3 +214,11 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult { Err("file not found".to_string()) } } + +/// 通知UI已准备就绪 +#[tauri::command] +pub fn notify_ui_ready() -> CmdResult<()> { + log::info!(target: "app", "前端UI已准备就绪"); + crate::utils::resolve::mark_ui_ready(); + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 946095eb..33acf0a4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,8 +19,8 @@ use tauri::AppHandle; use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_deep_link::DeepLinkExt; -use utils::logging::Type; use tauri_plugin_window_state; +use utils::logging::Type; /// A global singleton handle to the application. pub struct AppHandleManager { @@ -118,9 +118,11 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_deep_link::init()) - .plugin(tauri_plugin_window_state::Builder::default() - .with_state_flags(tauri_plugin_window_state::StateFlags::all()) - .build()) + .plugin( + tauri_plugin_window_state::Builder::default() + .with_state_flags(tauri_plugin_window_state::StateFlags::all()) + .build(), + ) .setup(|app| { #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { @@ -157,6 +159,7 @@ pub fn run() { cmd::restart_core, cmd::restart_app, // 添加新的命令 + cmd::notify_ui_ready, cmd::get_running_mode, cmd::get_app_uptime, cmd::get_auto_launch_status, diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index fc58f40a..09bafbd9 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -11,9 +11,12 @@ use crate::{ }; use anyhow::{bail, Result}; use once_cell::sync::OnceCell; +use parking_lot::RwLock; use percent_encoding::percent_decode_str; +use serde_json; use serde_yaml::Mapping; use std::net::TcpListener; +use std::sync::Arc; use tauri::{App, Manager}; use tauri::Url; @@ -22,6 +25,23 @@ use tauri::Url; pub static VERSION: OnceCell = OnceCell::new(); +// 窗口状态文件中的尺寸 +static STATE_WIDTH: OnceCell = OnceCell::new(); +static STATE_HEIGHT: OnceCell = OnceCell::new(); + +// 添加全局UI准备就绪标志 +static UI_READY: OnceCell>> = OnceCell::new(); + +fn get_ui_ready() -> &'static Arc> { + UI_READY.get_or_init(|| Arc::new(RwLock::new(false))) +} + +// 标记UI已准备就绪 +pub fn mark_ui_ready() { + let mut ready = get_ui_ready().write(); + *ready = true; +} + pub fn find_unused_port() -> Result { match TcpListener::bind("127.0.0.1:0") { Ok(listener) => { @@ -123,6 +143,71 @@ pub async fn resolve_reset_async() { /// create main window pub fn create_window(is_showup: bool) { + // 打印 .window-state.json 文件路径 + if let Ok(app_dir) = crate::utils::dirs::app_home_dir() { + let window_state_path = app_dir.join(".window-state.json"); + logging!( + info, + Type::Window, + true, + "窗口状态文件路径: {:?}", + window_state_path + ); + + // 尝试读取窗口状态文件内容 + if window_state_path.exists() { + match std::fs::read_to_string(&window_state_path) { + Ok(content) => { + logging!(info, Type::Window, true, "窗口状态文件内容: {}", content); + + // 解析窗口状态文件 + match serde_json::from_str::(&content) { + Ok(state_json) => { + if let Some(main_window) = state_json.get("main") { + let width = main_window + .get("width") + .and_then(|v| v.as_u64()) + .unwrap_or(0) + as u32; + let height = main_window + .get("height") + .and_then(|v| v.as_u64()) + .unwrap_or(0) + as u32; + + logging!( + info, + Type::Window, + true, + "窗口状态文件中的尺寸: {}x{}", + width, + height + ); + + // 保存读取到的尺寸,用于后续检查 + STATE_WIDTH.get_or_init(|| width); + STATE_HEIGHT.get_or_init(|| height); + } + } + Err(e) => { + logging!(error, Type::Window, true, "解析窗口状态文件失败: {:?}", e); + } + } + } + Err(e) => { + logging!(error, Type::Window, true, "读取窗口状态文件失败: {:?}", e); + } + } + } else { + logging!( + info, + Type::Window, + true, + "窗口状态文件不存在,将使用默认设置" + ); + } + } + if !is_showup { logging!(info, Type::Window, "Not to display create window"); return; @@ -156,6 +241,12 @@ pub fn create_window(is_showup: bool) { return; } + // 定义默认窗口大小 + const DEFAULT_WIDTH: u32 = 900; + const DEFAULT_HEIGHT: u32 = 700; + const MIN_WIDTH: u32 = 650; + const MIN_HEIGHT: u32 = 580; + #[cfg(target_os = "windows")] let window = tauri::WebviewWindowBuilder::new( &app_handle, @@ -163,14 +254,14 @@ pub fn create_window(is_showup: bool) { tauri::WebviewUrl::App("index.html".into()), ) .title("Clash Verge") - .inner_size(900.0, 700.0) - .min_inner_size(650.0, 580.0) + .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) + .min_inner_size(MIN_WIDTH as f64, MIN_HEIGHT as f64) .decorations(false) .maximizable(true) .additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling") .transparent(true) .shadow(true) - .visible(true) + .visible(false) // 初始不可见,等待UI加载完成后再显示 .build(); #[cfg(target_os = "macos")] @@ -182,9 +273,9 @@ pub fn create_window(is_showup: bool) { .decorations(true) .hidden_title(true) .title_bar_style(tauri::TitleBarStyle::Overlay) - .inner_size(900.0, 700.0) - .min_inner_size(650.0, 580.0) - .visible(true) + .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) + .min_inner_size(MIN_WIDTH as f64, MIN_HEIGHT as f64) + .visible(false) // 初始不可见,等待UI加载完成后再显示 .build(); #[cfg(target_os = "linux")] @@ -195,17 +286,63 @@ pub fn create_window(is_showup: bool) { ) .title("Clash Verge") .decorations(false) - .inner_size(900.0, 700.0) - .min_inner_size(650.0, 580.0) + .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) + .min_inner_size(MIN_WIDTH as f64, MIN_HEIGHT as f64) .transparent(true) - .visible(true) + .visible(false) // 初始不可见,等待UI加载完成后再显示 .build(); match window { - Ok(_) => { + Ok(window) => { logging!(info, Type::Window, true, "Window created successfully"); + // 标记前端UI已准备就绪,向前端发送启动完成事件 let app_handle_clone = app_handle.clone(); + + // 获取窗口创建后的初始大小 + if let Ok(size) = window.inner_size() { + let state_width = STATE_WIDTH.get().copied().unwrap_or(DEFAULT_WIDTH); + let state_height = STATE_HEIGHT.get().copied().unwrap_or(DEFAULT_HEIGHT); + + // 输出所有尺寸信息 + logging!( + info, + Type::Window, + true, + "API报告的窗口尺寸: {}x{}, 状态文件尺寸: {}x{}, 默认尺寸: {}x{}", + size.width, + size.height, + state_width, + state_height, + DEFAULT_WIDTH, + DEFAULT_HEIGHT + ); + + if state_width < DEFAULT_WIDTH || state_height < DEFAULT_HEIGHT { + logging!( + info, + Type::Window, + true, + "状态文件窗口尺寸小于默认值,将使用默认尺寸: {}x{}", + DEFAULT_WIDTH, + DEFAULT_HEIGHT + ); + + let _ = window.set_size(tauri::LogicalSize::new( + DEFAULT_WIDTH as f64, + DEFAULT_HEIGHT as f64, + )); + } else if size.width != state_width || size.height != state_height { + // 如果API报告的尺寸与状态文件不一致,记录日志 + logging!( + warn, + Type::Window, + true, + "API报告的窗口尺寸与状态文件不一致" + ); + } + } + AsyncHandler::spawn(move || async move { use tauri::Emitter; @@ -213,7 +350,74 @@ pub fn create_window(is_showup: bool) { handle::Handle::global().mark_startup_completed(); if let Some(window) = app_handle_clone.get_webview_window("main") { + // 检查窗口大小 + match window.inner_size() { + Ok(size) => { + let width = size.width; + let height = size.height; + + let state_width = STATE_WIDTH.get().copied().unwrap_or(DEFAULT_WIDTH); + let state_height = + STATE_HEIGHT.get().copied().unwrap_or(DEFAULT_HEIGHT); + + logging!( + info, + Type::Window, + true, + "异步任务中窗口尺寸: {}x{}, 状态文件尺寸: {}x{}", + width, + height, + state_width, + state_height + ); + } + Err(e) => { + logging!( + error, + Type::Window, + true, + "Failed to get window size: {:?}", + e + ); + } + } + + // 发送启动完成事件 let _ = window.emit("verge://startup-completed", ()); + + if is_showup { + // 启动一个任务等待UI准备就绪再显示窗口 + let window_clone = window.clone(); + AsyncHandler::spawn(move || async move { + async fn wait_for_ui_ready() { + while !*get_ui_ready().read() { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + } + + match tokio::time::timeout( + std::time::Duration::from_secs(5), + wait_for_ui_ready(), + ) + .await + { + Ok(_) => { + logging!(info, Type::Window, true, "UI准备就绪,显示窗口"); + } + Err(_) => { + logging!( + warn, + Type::Window, + true, + "等待UI准备就绪超时,强制显示窗口" + ); + } + } + + let _ = window_clone.show(); + let _ = window_clone.set_focus(); + }); + } } }); } diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 94367d77..8282a4d3 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -30,6 +30,7 @@ import { useListen } from "@/hooks/use-listen"; import { listen } from "@tauri-apps/api/event"; import { useClashInfo } from "@/hooks/use-clash"; import { initGlobalLogService } from "@/services/global-log-service"; +import { invoke } from "@tauri-apps/api/core"; const appWindow = getCurrentWebviewWindow(); export let portableFlag = false; @@ -209,6 +210,34 @@ const Layout = () => { }; }, [handleNotice]); + // 监听启动完成事件并通知UI已加载 + useEffect(() => { + const notifyUiReady = async () => { + try { + await new Promise(resolve => setTimeout(resolve, 200)); + await invoke("notify_ui_ready"); + console.log("已通知后端UI准备就绪"); + } catch (err) { + console.error("通知UI准备就绪失败:", err); + } + }; + + // 监听后端发送的启动完成事件 + const listenStartupCompleted = async () => { + const unlisten = await listen("verge://startup-completed", () => { + console.log("收到启动完成事件"); + }); + return unlisten; + }; + + notifyUiReady(); + const unlistenPromise = listenStartupCompleted(); + + return () => { + unlistenPromise.then(unlisten => unlisten()); + }; + }, []); + // 语言和起始页设置 useEffect(() => { if (language) {