diff --git a/src-tauri/src/cmd/proxy.rs b/src-tauri/src/cmd/proxy.rs index 1b72627f0..55c8e740f 100644 --- a/src-tauri/src/cmd/proxy.rs +++ b/src-tauri/src/cmd/proxy.rs @@ -1,5 +1,13 @@ +use tauri::Emitter; + use super::CmdResult; -use crate::{ipc::IpcManager, logging, state::proxy::ProxyRequestCache, utils::logging::Type}; +use crate::{ + core::{handle::Handle, tray::Tray}, + ipc::IpcManager, + logging, + state::proxy::ProxyRequestCache, + utils::logging::Type, +}; use std::time::Duration; const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60); @@ -45,3 +53,69 @@ pub async fn get_providers_proxies() -> CmdResult { .await; Ok((*value).clone()) } + +/// 同步托盘和GUI的代理选择状态 +#[tauri::command] +pub async fn sync_tray_proxy_selection() -> CmdResult<()> { + use crate::core::tray::Tray; + + match Tray::global().update_menu().await { + Ok(_) => { + logging!(info, Type::Cmd, "Tray proxy selection synced successfully"); + Ok(()) + } + Err(e) => { + logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}"); + Err(e.to_string()) + } + } +} + +/// 更新代理选择并同步托盘和GUI状态 +#[tauri::command] +pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> { + match IpcManager::global().update_proxy(&group, &proxy).await { + Ok(_) => { + logging!( + info, + Type::Cmd, + "Proxy updated successfully: {} -> {}", + group, + proxy + ); + + let cache = crate::state::proxy::ProxyRequestCache::global(); + let key = crate::state::proxy::ProxyRequestCache::make_key("proxies", "default"); + cache.map.remove(&key); + + if let Err(e) = Tray::global().update_menu().await { + logging!(error, Type::Cmd, "Failed to sync tray menu: {}", e); + } + + if let Some(app_handle) = Handle::global().app_handle() { + let _ = app_handle.emit("verge://force-refresh-proxies", ()); + let _ = app_handle.emit("verge://refresh-proxy-config", ()); + } + + logging!( + info, + Type::Cmd, + "Proxy and sync completed successfully: {} -> {}", + group, + proxy + ); + Ok(()) + } + Err(e) => { + logging!( + error, + Type::Cmd, + "Failed to update proxy: {} -> {}, error: {}", + group, + proxy, + e + ); + Err(e.to_string()) + } + } +} diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 64445b4c6..32e099ee7 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -1,5 +1,6 @@ use once_cell::sync::OnceCell; use tauri::tray::TrayIconBuilder; +use tauri::Emitter; #[cfg(target_os = "macos")] pub mod speed_rate; use crate::ipc::Rate; @@ -7,7 +8,9 @@ use crate::process::AsyncHandler; use crate::{ cmd, config::Config, - feat, logging, + feat, + ipc::IpcManager, + logging, module::lightweight::is_in_lightweight_mode, singleton_lazy, utils::{dirs::find_target_icons, i18n::t}, @@ -281,6 +284,15 @@ impl Tray { .unwrap_or_default(); let is_lightweight_mode = is_in_lightweight_mode(); + // 获取代理节点 + let proxy_nodes_data = match IpcManager::global().get_proxies().await { + Ok(data) => data, + Err(e) => { + log::warn!(target: "app", "获取代理节点数据失败: {}", e); + serde_json::Value::Object(serde_json::Map::new()) + } + }; + match app_handle.tray_by_id("main") { Some(tray) => { let _ = tray.set_menu(Some( @@ -291,6 +303,7 @@ impl Tray { *tun_mode, profile_uid_and_name, is_lightweight_mode, + proxy_nodes_data, ) .await?, )); @@ -557,9 +570,25 @@ async fn create_tray_menu( tun_mode_enabled: bool, profile_uid_and_name: Vec<(String, String)>, is_lightweight_mode: bool, + proxy_nodes_data: serde_json::Value, ) -> Result> { let mode = mode.unwrap_or(""); + // 获取当前配置文件的选中代理组信息 + let current_profile_selected = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + if let Some(current_profile_uid) = profiles.get_current() { + if let Ok(profile) = profiles.get_item(¤t_profile_uid) { + profile.selected.clone().unwrap_or_default() + } else { + Vec::new() + } + } else { + Vec::new() + } + }; + let version = env!("CARGO_PKG_VERSION"); let hotkeys = Config::verge() @@ -606,12 +635,131 @@ async fn create_tray_menu( results.into_iter().collect::, _>>()? }; + // 代理组子菜单 + let proxy_submenus: Vec> = { + let mut submenus = Vec::new(); + + // + if let Some(proxies) = proxy_nodes_data.get("proxies").and_then(|v| v.as_object()) { + for (group_name, group_data) in proxies.iter() { + if let Some(all_proxies) = group_data.get("all").and_then(|v| v.as_array()) { + // 在全局模式下只显示GLOBAL组,在规则模式下显示所有Selector类型的代理组 + let should_show_group = if mode == "global" { + group_name == "GLOBAL" + } else { + group_name != "GLOBAL" + }; + + if !should_show_group { + continue; + } + + let now_proxy = group_data.get("now").and_then(|v| v.as_str()).unwrap_or(""); + + // 每个代理组创建子菜单项 + let mut group_items = Vec::new(); + + for proxy_name in all_proxies.iter() { + if let Some(proxy_str) = proxy_name.as_str() { + let is_selected = proxy_str == now_proxy; + let item_id = format!("proxy_{}_{}", group_name, proxy_str); + + let display_text = { + if let Some(proxy_detail) = proxies.get(proxy_str) { + if let Some(history) = + proxy_detail.get("history").and_then(|h| h.as_array()) + { + if let Some(last_record) = history.last() { + if let Some(delay) = + last_record.get("delay").and_then(|d| d.as_i64()) + { + if delay == -1 { + format!("{} | -ms", proxy_str) + } else if delay >= 10000 { + format!("{} | -1ms", proxy_str) + } else { + format!("{} | {}ms", proxy_str, delay) + } + } else { + format!("{} | -ms ", proxy_str) + } + } else { + format!("{} | -ms ", proxy_str) + } + } else { + format!("{} | -ms", proxy_str) + } + } else { + format!("{} | -ms ", proxy_str) + } + }; + + match CheckMenuItem::with_id( + app_handle, + item_id, + display_text, // 显示包含延迟的节点名 + true, + is_selected, + None::<&str>, + ) { + Ok(item) => group_items.push(item), + Err(e) => log::warn!(target: "app", "创建代理菜单项失败: {}", e), + } + } + } + + // 创建代理组子菜单 + if !group_items.is_empty() { + let group_items_refs: Vec<&dyn IsMenuItem> = group_items + .iter() + .map(|item| item as &dyn IsMenuItem) + .collect(); + + // 判断当前代理组是否为真正在使用中的组 + let is_group_active = if mode == "global" { + group_name == "GLOBAL" && !now_proxy.is_empty() + } else if mode == "direct" { + // 直连模式下 不显示任何勾选 + false + } else { + let is_user_selected = current_profile_selected + .iter() + .any(|selected| selected.name.as_deref() == Some(group_name)); + is_user_selected && !now_proxy.is_empty() + }; + + // 如果组处于活动状态,在组名前添加勾选标记 + let group_display_name = if is_group_active { + format!("✓ {}", group_name) + } else { + group_name.to_string() + }; + + match Submenu::with_id_and_items( + app_handle, + format!("proxy_group_{}", group_name), + group_display_name, // 使用带勾选标记的组名 + true, + &group_items_refs, + ) { + Ok(submenu) => submenus.push(submenu), + Err(e) => log::warn!(target: "app", "创建代理组子菜单失败: {}", e), + } + } + } + } + } + + submenus + }; + // Pre-fetch all localized strings let dashboard_text = t("Dashboard").await; let rule_mode_text = t("Rule Mode").await; let global_mode_text = t("Global Mode").await; let direct_mode_text = t("Direct Mode").await; let profiles_text = t("Profiles").await; + let proxies_text = t("Proxies").await; let system_proxy_text = t("System Proxy").await; let tun_mode_text = t("TUN Mode").await; let lightweight_mode_text = t("LightWeight Mode").await; @@ -675,6 +823,24 @@ async fn create_tray_menu( &profile_menu_items_refs, )?; + // 创建代理主菜单 + let proxies_submenu = if !proxy_submenus.is_empty() { + let proxy_submenu_refs: Vec<&dyn IsMenuItem> = proxy_submenus + .iter() + .map(|submenu| submenu as &dyn IsMenuItem) + .collect(); + + Some(Submenu::with_id_and_items( + app_handle, + "proxies", + proxies_text, + true, + &proxy_submenu_refs, + )?) + } else { + None + }; + let system_proxy = &CheckMenuItem::with_id( app_handle, "system_proxy", @@ -772,26 +938,37 @@ async fn create_tray_menu( let separator = &PredefinedMenuItem::separator(app_handle)?; + // 动态构建菜单项 + let mut menu_items: Vec<&dyn IsMenuItem> = vec![ + open_window, + separator, + rule_mode, + global_mode, + direct_mode, + separator, + profiles, + ]; + + // 如果有代理节点,添加代理节点菜单 + if let Some(ref proxies_menu) = proxies_submenu { + menu_items.push(proxies_menu); + } + + menu_items.extend_from_slice(&[ + separator, + system_proxy as &dyn IsMenuItem, + tun_mode as &dyn IsMenuItem, + separator, + lighteweight_mode as &dyn IsMenuItem, + copy_env as &dyn IsMenuItem, + open_dir as &dyn IsMenuItem, + more as &dyn IsMenuItem, + separator, + quit as &dyn IsMenuItem, + ]); + let menu = tauri::menu::MenuBuilder::new(app_handle) - .items(&[ - open_window, - separator, - rule_mode, - global_mode, - direct_mode, - separator, - profiles, - separator, - system_proxy, - tun_mode, - separator, - lighteweight_mode, - copy_env, - open_dir, - more, - separator, - quit, - ]) + .items(&menu_items) .build()?; Ok(menu) } @@ -865,6 +1042,63 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { let profile_index = &id["profiles_".len()..]; feat::toggle_proxy_profile(profile_index.into()).await; // Await async function } + id if id.starts_with("proxy_") => { + // proxy_{group_name}_{proxy_name} + let parts: Vec<&str> = id.splitn(3, '_').collect(); + + if parts.len() == 3 && parts[0] == "proxy" { + let group_name = parts[1]; + let proxy_name = parts[2]; + + let current_mode = { + Config::clash() + .await + .latest_ref() + .0 + .get("mode") + .map(|val| val.as_str().unwrap_or("rule")) + .unwrap_or("rule") + .to_owned() + }; + + match cmd::proxy::update_proxy_and_sync( + group_name.to_string(), + proxy_name.to_string(), + ) + .await + { + Ok(_) => { + log::info!(target: "app", " {} -> {} (模式: {})", group_name, proxy_name, current_mode); + } + Err(e) => { + log::error!(target: "app", " {} -> {}, 错误: {:?}", group_name, proxy_name, e); + + match IpcManager::global() + .update_proxy(group_name, proxy_name) + .await + { + Ok(_) => { + log::info!(target: "app", " {} -> {}", group_name, proxy_name); + + if let Err(e) = Tray::global().update_menu().await { + log::warn!(target: "app", "托盘菜单更新失败: {e}"); + } + + if let Some(app_handle) = handle::Handle::global().app_handle() + { + let _ = + app_handle.emit("verge://force-refresh-proxies", ()); + let _ = app_handle.emit("verge://refresh-proxy-config", ()); + } + } + Err(e2) => { + log::error!(target: "app", "托盘代理切换回退也失败: {} -> {}, 错误: {}", group_name, proxy_name, e2); + } + } + } + } + } + } _ => {} } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 65d95de70..bc28a3185 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -185,6 +185,8 @@ mod app_init { cmd::get_proxies, cmd::force_refresh_proxies, cmd::get_providers_proxies, + cmd::sync_tray_proxy_selection, + cmd::update_proxy_and_sync, cmd::save_dns_config, cmd::apply_dns_config, cmd::check_dns_config_exists, diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index 23622862a..b0cdf767d 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -29,7 +29,11 @@ import { } from "@mui/icons-material"; import { useNavigate } from "react-router-dom"; import { EnhancedCard } from "@/components/home/enhanced-card"; -import { updateProxy, deleteConnection } from "@/services/cmds"; +import { + updateProxy, + deleteConnection, + syncTrayProxySelection, +} from "@/services/cmds"; import delayManager from "@/services/delay"; import { useVerge } from "@/hooks/use-verge"; import { useAppData } from "@/providers/app-data-provider"; @@ -342,6 +346,13 @@ export const CurrentProxyCard = () => { }); } + // 同步托盘菜单状态 + try { + await syncTrayProxySelection(); + } catch (syncError) { + console.warn("Failed to sync tray proxy selection:", syncError); + } + // 延长刷新延迟时间 setTimeout(() => { refreshProxy(); diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index b2d723e63..015eb2c90 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -7,6 +7,8 @@ import { updateProxy, deleteConnection, getGroupProxyDelays, + syncTrayProxySelection, + updateProxyAndSync, } from "@/services/cmds"; import { forceRefreshProxies } from "@/services/cmds"; import { useProfiles } from "@/hooks/use-profiles"; @@ -341,37 +343,66 @@ export const ProxyGroups = (props: Props) => { if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return; const { name, now } = group; - await updateProxy(name, proxy.name); + console.log(`[ProxyGroups] GUI代理切换: ${name} -> ${proxy.name}`); - await forceRefreshProxies(); + try { + // 1. 保存到selected中 (先保存本地状态) + if (current) { + if (!current.selected) current.selected = []; - onProxies(); + const index = current.selected.findIndex( + (item) => item.name === group.name, + ); - // 断开连接 - if (verge?.auto_close_connection) { - getConnections().then(({ connections }) => { - connections.forEach((conn) => { - if (conn.chains.includes(now!)) { - deleteConnection(conn.id); - } + if (index < 0) { + current.selected.push({ name, now: proxy.name }); + } else { + current.selected[index] = { name, now: proxy.name }; + } + await patchCurrent({ selected: current.selected }); + } + + // 2. 使用统一的同步命令更新代理并同步状态 + await updateProxyAndSync(name, proxy.name); + console.log( + `[ProxyGroups] 代理和状态同步完成: ${name} -> ${proxy.name}`, + ); + + // 3. 刷新前端显示 + onProxies(); + + // 4. 断开连接 (异步处理,不影响UI更新) + if (verge?.auto_close_connection) { + getConnections().then(({ connections }) => { + connections.forEach((conn) => { + if (conn.chains.includes(now!)) { + deleteConnection(conn.id); + } + }); }); - }); + } + } catch (error) { + console.error( + `[ProxyGroups] 代理切换失败: ${name} -> ${proxy.name}`, + error, + ); + // 如果统一命令失败,回退到原来的方式 + try { + await updateProxy(name, proxy.name); + await forceRefreshProxies(); + await syncTrayProxySelection(); + onProxies(); + console.log( + `[ProxyGroups] 代理切换回退成功: ${name} -> ${proxy.name}`, + ); + } catch (fallbackError) { + console.error( + `[ProxyGroups] 代理切换回退也失败: ${name} -> ${proxy.name}`, + fallbackError, + ); + onProxies(); // 至少刷新显示 + } } - - // 保存到selected中 - if (!current) return; - if (!current.selected) current.selected = []; - - const index = current.selected.findIndex( - (item) => item.name === group.name, - ); - - if (index < 0) { - current.selected.push({ name, now: proxy.name }); - } else { - current.selected[index] = { name, now: proxy.name }; - } - await patchCurrent({ selected: current.selected }); }, ); diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index 4962648b1..9e72ecd3f 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -221,16 +221,105 @@ export const AppDataProvider = ({ } }; - window.addEventListener( - "verge://refresh-clash-config", - handleRefreshClash, - ); + // 监听代理配置刷新事件(托盘代理切换等) + const handleRefreshProxy = () => { + const now = Date.now(); + console.log("[AppDataProvider] 代理配置刷新事件"); - return () => { - window.removeEventListener( - "verge://refresh-clash-config", - handleRefreshClash, - ); + if (now - lastUpdateTime > refreshThrottle) { + lastUpdateTime = now; + + setTimeout(() => { + refreshProxy().catch((e) => + console.warn("[AppDataProvider] 代理刷新失败:", e), + ); + }, 100); + } + }; + + // 监听强制代理刷新事件(托盘代理切换立即刷新) + const handleForceRefreshProxies = () => { + console.log("[AppDataProvider] 强制代理刷新事件"); + + // 立即刷新,无延迟,无防抖 + forceRefreshProxies() + .then(() => { + console.log("[AppDataProvider] 强制刷新代理缓存完成"); + // 强制刷新完成后,立即刷新前端显示 + return refreshProxy(); + }) + .then(() => { + console.log("[AppDataProvider] 前端代理数据刷新完成"); + }) + .catch((e) => { + console.warn("[AppDataProvider] 强制代理刷新失败:", e); + // 如果强制刷新失败,尝试普通刷新 + refreshProxy().catch((e2) => + console.warn("[AppDataProvider] 普通代理刷新也失败:", e2), + ); + }); + }; + + // 使用 Tauri 事件监听器替代 window 事件监听器 + const setupTauriListeners = async () => { + try { + const unlistenClash = await listen( + "verge://refresh-clash-config", + handleRefreshClash, + ); + const unlistenProxy = await listen( + "verge://refresh-proxy-config", + handleRefreshProxy, + ); + const unlistenForceRefresh = await listen( + "verge://force-refresh-proxies", + handleForceRefreshProxies, + ); + + return () => { + unlistenClash(); + unlistenProxy(); + unlistenForceRefresh(); + }; + } catch (error) { + console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error); + + // 降级到 window 事件监听器 + window.addEventListener( + "verge://refresh-clash-config", + handleRefreshClash, + ); + window.addEventListener( + "verge://refresh-proxy-config", + handleRefreshProxy, + ); + window.addEventListener( + "verge://force-refresh-proxies", + handleForceRefreshProxies, + ); + + return () => { + window.removeEventListener( + "verge://refresh-clash-config", + handleRefreshClash, + ); + window.removeEventListener( + "verge://refresh-proxy-config", + handleRefreshProxy, + ); + window.removeEventListener( + "verge://force-refresh-proxies", + handleForceRefreshProxies, + ); + }; + } + }; + + const cleanupTauriListeners = setupTauriListeners(); + + return async () => { + const cleanup = await cleanupTauriListeners; + cleanup(); }; } catch (error) { console.error("[AppDataProvider] 事件监听器设置失败:", error); diff --git a/src/services/cmds.ts b/src/services/cmds.ts index cfc0efb42..18e0c552c 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -143,6 +143,14 @@ export async function updateProxy(group: string, proxy: string) { // console.log(`[API] updateProxy 耗时: ${duration}ms`); } +export async function syncTrayProxySelection() { + return invoke("sync_tray_proxy_selection"); +} + +export async function updateProxyAndSync(group: string, proxy: string) { + return invoke("update_proxy_and_sync", { group, proxy }); +} + export async function getProxies(): Promise<{ global: IProxyGroupItem; direct: IProxyItem;