use super::*; #[cfg(target_os = "macos")] const MACOS_TRAY_ID: &str = "macos-tray"; #[cfg(target_os = "macos")] #[derive(Default)] pub(crate) struct MacosTrayClickState { last_click: Option, sequence: u64, } #[cfg(target_os = "macos")] pub(crate) fn set_macos_dock_visible(app_handle: &tauri::AppHandle, visible: bool) { if let Err(error) = app_handle.set_dock_visibility(visible) { eprintln!("更新 Dock 图标状态失败: {error}"); } } #[cfg(target_os = "macos")] pub(crate) fn show_main_window_on_main_thread(app_handle: &tauri::AppHandle) { let Some(window) = app_handle.get_webview_window("main") else { return; }; set_macos_dock_visible(app_handle, true); if let Err(error) = window.show() { eprintln!("显示主窗口失败: {error}"); set_macos_dock_visible(app_handle, false); return; } if window.is_minimized().unwrap_or(false) { if let Err(error) = window.unminimize() { eprintln!("恢复主窗口失败: {error}"); } } if let Err(error) = window.set_focus() { eprintln!("聚焦主窗口失败: {error}"); } } #[cfg(target_os = "macos")] pub(crate) fn show_main_window(app_handle: &tauri::AppHandle) { if MainThreadMarker::new().is_some() { show_main_window_on_main_thread(app_handle); return; } let app_handle = app_handle.clone(); if let Err(error) = app_handle.clone().run_on_main_thread(move || { show_main_window_on_main_thread(&app_handle); }) { eprintln!("调度主窗口显示失败: {error}"); } } #[cfg(target_os = "macos")] pub(crate) fn show_macos_tray_menu(tray: &TrayIcon) { let result = tray.with_inner_tray_icon(|tray_icon| { let Some(status_item) = tray_icon.ns_status_item() else { return; }; let mtm = MainThreadMarker::new().expect("tray menu must be shown on the main thread"); if let Some(menu) = status_item.menu(mtm) { #[allow(deprecated)] status_item.popUpStatusItemMenu(&menu); } }); if let Err(error) = result { eprintln!("显示托盘菜单失败: {error}"); } } #[cfg(target_os = "macos")] pub(crate) fn setup_macos_tray(app: &mut tauri::App) -> tauri::Result<()> { let locale = app .state::() .snapshot() .map(|config| config.locale) .unwrap_or_else(|_| "zh-CN".to_string()); let open_main_window = MenuItem::with_id( app, "open-main-window", locale_text(&locale, "打开主界面", "Open Main Window"), true, None::<&str>, )?; let quit = MenuItem::with_id( app, "quit", locale_text(&locale, "退出", "Quit"), true, None::<&str>, )?; let menu = Menu::with_items(app, &[&open_main_window, &quit])?; let click_state = Arc::new(Mutex::new(MacosTrayClickState::default())); let double_click_interval = Duration::from_secs_f64(NSEvent::doubleClickInterval()); TrayIconBuilder::with_id(MACOS_TRAY_ID) .icon( app.default_window_icon() .cloned() .expect("application icon is required for the tray"), ) .menu(&menu) .show_menu_on_left_click(false) .on_menu_event(move |app_handle, event| match event.id().as_ref() { "open-main-window" => show_main_window(app_handle), "quit" => app_handle.exit(0), _ => {} }) .on_tray_icon_event(move |tray, event| { if !matches!( event, TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } ) { return; } let now = Instant::now(); let Ok(mut state) = click_state.lock() else { eprintln!("读取托盘点击状态失败"); return; }; state.sequence += 1; let sequence = state.sequence; if state .last_click .is_some_and(|last_click| now.duration_since(last_click) <= double_click_interval) { state.last_click = None; drop(state); show_main_window(tray.app_handle()); return; } state.last_click = Some(now); drop(state); let app_handle = tray.app_handle().clone(); let click_state = Arc::clone(&click_state); let tray_id = tray.id().clone(); thread::spawn(move || { thread::sleep(double_click_interval); let should_show_menu = match click_state.lock() { Ok(mut state) if state.sequence == sequence => { state.last_click = None; true } Ok(_) => false, Err(_) => { eprintln!("读取托盘点击状态失败"); false } }; if !should_show_menu { return; } if let Some(tray) = app_handle.tray_by_id(&tray_id) { show_macos_tray_menu(&tray); } }); }) .build(app)?; Ok(()) } #[cfg(target_os = "windows")] const WINDOWS_TRAY_ID: &str = "windows-tray"; #[cfg(target_os = "windows")] const WINDOWS_TRAY_OPEN_MENU_ID: &str = "windows-tray-open-main-window"; #[cfg(target_os = "windows")] const WINDOWS_TRAY_STATUS_MENU_ID: &str = "windows-tray-core-status"; #[cfg(target_os = "windows")] const WINDOWS_TRAY_TOGGLE_CORE_MENU_ID: &str = "windows-tray-toggle-core"; #[cfg(target_os = "windows")] const WINDOWS_TRAY_RESTART_CORE_MENU_ID: &str = "windows-tray-restart-core"; #[cfg(target_os = "windows")] const WINDOWS_TRAY_QUIT_MENU_ID: &str = "windows-tray-quit"; #[cfg(target_os = "windows")] #[derive(Clone, Copy)] pub(crate) enum WindowsTrayCoreAction { Toggle, Restart, } #[cfg(target_os = "windows")] pub(crate) struct WindowsTrayPresentation { pub(crate) status_text: String, pub(crate) toggle_text: String, pub(crate) toggle_enabled: bool, pub(crate) restart_enabled: bool, pub(crate) tooltip: String, } #[cfg(target_os = "windows")] pub(crate) fn windows_tray_presentation( status: &CoreStatus, busy: bool, locale: &str, ) -> WindowsTrayPresentation { let status_text = if busy { locale_text(locale, "内核状态:处理中", "Core status: Working") } else if !status.installed { locale_text(locale, "内核状态:未安装", "Core status: Not installed") } else if status.running { locale_text(locale, "内核状态:运行中", "Core status: Running") } else { locale_text(locale, "内核状态:已停止", "Core status: Stopped") }; let toggle_text = if busy { locale_text(locale, "处理中...", "Working...") } else if status.running { locale_text(locale, "停止内核", "Stop Core") } else { locale_text(locale, "启动内核", "Start Core") }; let tooltip = if busy { locale_text( locale, "EasyCLIProxyAPI · 内核处理中", "EasyCLIProxyAPI · Core working", ) } else if !status.installed { locale_text( locale, "EasyCLIProxyAPI · 内核未安装", "EasyCLIProxyAPI · Core not installed", ) } else if status.running { locale_text( locale, "EasyCLIProxyAPI · 内核运行中", "EasyCLIProxyAPI · Core running", ) } else { locale_text( locale, "EasyCLIProxyAPI · 内核已停止", "EasyCLIProxyAPI · Core stopped", ) }; WindowsTrayPresentation { status_text: status_text.to_string(), toggle_text: toggle_text.to_string(), toggle_enabled: status.installed && !busy, restart_enabled: status.installed && status.running && !busy, tooltip: tooltip.to_string(), } } #[cfg(target_os = "windows")] pub(crate) struct WindowsTrayState { open_main_window: MenuItem, status_item: MenuItem, toggle_core_item: MenuItem, restart_core_item: MenuItem, quit_item: MenuItem, locale: Mutex, busy: AtomicBool, } #[cfg(target_os = "windows")] impl WindowsTrayState { fn locale(&self) -> String { self.locale .lock() .map(|locale| locale.clone()) .unwrap_or_else(|_| "zh-CN".to_string()) } fn set_locale(&self, locale: &str) { let normalized = normalize_app_locale(locale); if let Ok(mut current) = self.locale.lock() { *current = normalized.to_string(); } let labels = [ ( &self.open_main_window, locale_text(normalized, "打开主界面", "Open Main Window"), ), ( &self.restart_core_item, locale_text(normalized, "重启内核", "Restart Core"), ), (&self.quit_item, locale_text(normalized, "退出", "Quit")), ]; for (item, label) in labels { if let Err(error) = item.set_text(label) { eprintln!("更新 Windows 托盘语言失败: {error}"); } } } fn begin_action(&self) -> bool { if self .busy .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .is_err() { return false; } let locale = self.locale(); if let Err(error) = self.status_item.set_text(locale_text( &locale, "内核状态:处理中", "Core status: Working", )) { eprintln!("更新 Windows 托盘内核状态失败: {error}"); } if let Err(error) = self.toggle_core_item .set_text(locale_text(&locale, "处理中...", "Working...")) { eprintln!("更新 Windows 托盘操作文本失败: {error}"); } if let Err(error) = self.toggle_core_item.set_enabled(false) { eprintln!("更新 Windows 托盘操作状态失败: {error}"); } if let Err(error) = self.restart_core_item.set_enabled(false) { eprintln!("更新 Windows 托盘重启状态失败: {error}"); } true } fn finish_action(&self) { self.busy.store(false, Ordering::Release); } fn update(&self, status: &CoreStatus) -> String { let presentation = windows_tray_presentation(status, self.busy.load(Ordering::Acquire), &self.locale()); if let Err(error) = self.status_item.set_text(presentation.status_text.clone()) { eprintln!("更新 Windows 托盘内核状态失败: {error}"); } if let Err(error) = self .toggle_core_item .set_text(presentation.toggle_text.clone()) { eprintln!("更新 Windows 托盘操作文本失败: {error}"); } if let Err(error) = self .toggle_core_item .set_enabled(presentation.toggle_enabled) { eprintln!("更新 Windows 托盘操作状态失败: {error}"); } if let Err(error) = self .restart_core_item .set_enabled(presentation.restart_enabled) { eprintln!("更新 Windows 托盘重启状态失败: {error}"); } presentation.tooltip } fn show_error(&self, error: &str) { let mut summary = error.chars().take(48).collect::(); if error.chars().count() > 48 { summary.push('…'); } let text = match normalize_app_locale(&self.locale()) { "en" => format!("Operation failed: {summary}"), "zh-TW" => format!("操作失敗:{summary}"), "ja" => format!("操作に失敗しました:{summary}"), _ => format!("操作失败:{summary}"), }; if let Err(update_error) = self.status_item.set_text(text) { eprintln!("更新 Windows 托盘错误状态失败: {update_error}"); } } } #[cfg(target_os = "windows")] pub(crate) fn show_windows_main_window(app_handle: &tauri::AppHandle) { let Some(window) = app_handle.get_webview_window("main") else { return; }; if let Err(error) = window.show() { eprintln!("显示主窗口失败: {error}"); return; } if window.is_minimized().unwrap_or(false) { if let Err(error) = window.unminimize() { eprintln!("恢复主窗口失败: {error}"); } } if let Err(error) = window.set_focus() { eprintln!("聚焦主窗口失败: {error}"); } } #[cfg(target_os = "windows")] pub(crate) fn update_windows_tray_status(app_handle: &tauri::AppHandle, status: &CoreStatus) { let tooltip = app_handle .try_state::() .map(|tray_state| tray_state.update(status)) .unwrap_or_else(|| windows_tray_presentation(status, false, "zh-CN").tooltip); if let Some(tray) = app_handle.tray_by_id(WINDOWS_TRAY_ID) { if let Err(error) = tray.set_tooltip(Some(&tooltip)) { eprintln!("更新 Windows 托盘提示失败: {error}"); } } } #[cfg(target_os = "windows")] pub(crate) fn update_windows_tray_locale( app_handle: &tauri::AppHandle, locale: &str, status: &CoreStatus, ) { if let Some(tray_state) = app_handle.try_state::() { tray_state.set_locale(locale); } update_windows_tray_status(app_handle, status); } #[cfg(target_os = "windows")] pub(crate) fn show_windows_tray_action_error(app_handle: &tauri::AppHandle, error: &str) { eprintln!("Windows 托盘内核操作失败: {error}"); let locale = app_handle .try_state::() .map(|state| state.locale()) .unwrap_or_else(|| "zh-CN".to_string()); if let Some(tray_state) = app_handle.try_state::() { tray_state.show_error(error); } if let Some(tray) = app_handle.tray_by_id(WINDOWS_TRAY_ID) { if let Err(update_error) = tray.set_tooltip(Some(locale_text( &locale, "EasyCLIProxyAPI · 内核操作失败", "EasyCLIProxyAPI · Core operation failed", ))) { eprintln!("更新 Windows 托盘错误提示失败: {update_error}"); } } } #[cfg(target_os = "windows")] pub(crate) fn run_windows_tray_core_action( app_handle: &tauri::AppHandle, action: WindowsTrayCoreAction, ) { let Some(tray_state) = app_handle.try_state::() else { return; }; if !tray_state.begin_action() { return; } if let Some(tray) = app_handle.tray_by_id(WINDOWS_TRAY_ID) { if let Err(error) = tray.set_tooltip(Some(locale_text( &tray_state.locale(), "EasyCLIProxyAPI · 内核处理中", "EasyCLIProxyAPI · Core working", ))) { eprintln!("更新 Windows 托盘处理中提示失败: {error}"); } } let app_handle = app_handle.clone(); tauri::async_runtime::spawn_blocking(move || { let process_state = app_handle.state::(); let gui_config_state = app_handle.state::(); let result = (|| -> Result { match action { WindowsTrayCoreAction::Toggle => { let config = gui_config_state.snapshot()?; let status = current_core_status(Some(process_state.inner()), Some(config.port))?; if status.running { stop_core_process_with_state( process_state.inner(), gui_config_state.inner(), ) } else { start_core_process_with_state( process_state.inner(), gui_config_state.inner(), ) } } WindowsTrayCoreAction::Restart => { restart_core_process_with_state(process_state.inner(), gui_config_state.inner()) } } })(); if let Some(tray_state) = app_handle.try_state::() { tray_state.finish_action(); } match result { Ok(status) => emit_core_status(&app_handle, &status), Err(error) => { if let Ok(config) = gui_config_state.snapshot() { if let Ok(status) = current_core_status(Some(process_state.inner()), Some(config.port)) { emit_core_status(&app_handle, &status); } } show_windows_tray_action_error(&app_handle, &error); } } }); } #[cfg(target_os = "windows")] pub(crate) fn setup_windows_tray(app: &mut tauri::App) -> tauri::Result<()> { let locale = app .state::() .snapshot() .map(|config| config.locale) .unwrap_or_else(|_| "zh-CN".to_string()); let open_main_window = MenuItem::with_id( app, WINDOWS_TRAY_OPEN_MENU_ID, locale_text(&locale, "打开主界面", "Open Main Window"), true, None::<&str>, )?; let status_item = MenuItem::with_id( app, WINDOWS_TRAY_STATUS_MENU_ID, locale_text(&locale, "内核状态:正在检查", "Core status: Checking"), false, None::<&str>, )?; let toggle_core_item = MenuItem::with_id( app, WINDOWS_TRAY_TOGGLE_CORE_MENU_ID, locale_text(&locale, "启动内核", "Start Core"), false, None::<&str>, )?; let restart_core_item = MenuItem::with_id( app, WINDOWS_TRAY_RESTART_CORE_MENU_ID, locale_text(&locale, "重启内核", "Restart Core"), false, None::<&str>, )?; let quit = MenuItem::with_id( app, WINDOWS_TRAY_QUIT_MENU_ID, locale_text(&locale, "退出", "Quit"), true, None::<&str>, )?; let separator_one = PredefinedMenuItem::separator(app)?; let separator_two = PredefinedMenuItem::separator(app)?; let menu = Menu::with_items( app, &[ &open_main_window, &separator_one, &status_item, &toggle_core_item, &restart_core_item, &separator_two, &quit, ], )?; TrayIconBuilder::with_id(WINDOWS_TRAY_ID) .icon( app.default_window_icon() .cloned() .expect("application icon is required for the tray"), ) .tooltip("EasyCLIProxyAPI") .menu(&menu) .show_menu_on_left_click(false) .on_menu_event(move |app_handle, event| match event.id().as_ref() { WINDOWS_TRAY_OPEN_MENU_ID => show_windows_main_window(app_handle), WINDOWS_TRAY_TOGGLE_CORE_MENU_ID => { run_windows_tray_core_action(app_handle, WindowsTrayCoreAction::Toggle) } WINDOWS_TRAY_RESTART_CORE_MENU_ID => { run_windows_tray_core_action(app_handle, WindowsTrayCoreAction::Restart) } WINDOWS_TRAY_QUIT_MENU_ID => app_handle.exit(0), _ => {} }) .on_tray_icon_event(move |tray, event| { if matches!( event, TrayIconEvent::DoubleClick { button: MouseButton::Left, .. } ) { show_windows_main_window(tray.app_handle()); } }) .build(app)?; let _ = app.manage(WindowsTrayState { open_main_window, status_item, toggle_core_item, restart_core_item, quit_item: quit, locale: Mutex::new(locale), busy: AtomicBool::new(false), }); Ok(()) }