mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-02 12:52:13 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf35539ad8 | ||
|
|
d1e3f09f17 |
@@ -0,0 +1,237 @@
|
|||||||
|
//! Live handles into the attached tray menu and the reconcile decision:
|
||||||
|
//! state-only changes mutate items in place, a proxy-only structure change
|
||||||
|
//! swaps the group submenus, anything else rebuilds.
|
||||||
|
|
||||||
|
use crate::Type;
|
||||||
|
use crate::config::{IProfiles, IVerge};
|
||||||
|
use clash_verge_logging::logging_error;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use smartstring::alias::String;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tauri::menu::{CheckMenuItem, IsMenuItem, Submenu};
|
||||||
|
use tauri::tray::TrayIcon;
|
||||||
|
use tauri::{AppHandle, Wry};
|
||||||
|
use tauri_plugin_mihomo::models::Proxies;
|
||||||
|
|
||||||
|
use super::{TrayMenuOptions, build_tray_menu, create_subcreate_proxy_menu_item};
|
||||||
|
|
||||||
|
pub(super) struct ProxyNodeItem {
|
||||||
|
pub(super) name: String,
|
||||||
|
pub(super) item: CheckMenuItem<Wry>,
|
||||||
|
pub(super) text: String,
|
||||||
|
pub(super) checked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct ProxyGroupItems {
|
||||||
|
pub(super) name: String,
|
||||||
|
pub(super) nodes: Vec<ProxyNodeItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct TrayMenuState {
|
||||||
|
pub(super) mode_items: [CheckMenuItem<Wry>; 3],
|
||||||
|
pub(super) system_proxy_item: CheckMenuItem<Wry>,
|
||||||
|
pub(super) tun_item: CheckMenuItem<Wry>,
|
||||||
|
pub(super) lightweight_item: CheckMenuItem<Wry>,
|
||||||
|
pub(super) profile_items: Vec<(String, CheckMenuItem<Wry>)>,
|
||||||
|
pub(super) proxies_submenu: Option<Submenu<Wry>>,
|
||||||
|
pub(super) proxy_groups: Vec<ProxyGroupItems>,
|
||||||
|
pub(super) signature: String,
|
||||||
|
pub(super) proxy_signature: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn proxy_delay_text(proxy_nodes_data: &Proxies, proxy_str: &str) -> String {
|
||||||
|
proxy_nodes_data
|
||||||
|
.proxies
|
||||||
|
.get(proxy_str)
|
||||||
|
.and_then(|h| h.history.last())
|
||||||
|
.map(|h| match h.delay {
|
||||||
|
0 => "-ms".into(),
|
||||||
|
delay if delay >= 10000 => "-ms".into(),
|
||||||
|
_ => format!("{}ms", h.delay).into(),
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "-ms".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Topology inputs that a rebuild cannot reconcile by mutating items.
|
||||||
|
pub(super) fn menu_signature(verge: &IVerge, profiles: &IProfiles, mode: &str) -> String {
|
||||||
|
let hotkeys = verge.hotkeys.as_ref().map(|h| h.join(",")).unwrap_or_default();
|
||||||
|
let profiles = profiles
|
||||||
|
.profiles_preview()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|profile| format!("{}:{}", profile.uid, profile.name))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";");
|
||||||
|
format!(
|
||||||
|
"{}|{}|{mode}|{}|{hotkeys}|{profiles}",
|
||||||
|
verge.tray_proxy_groups_display_mode.as_deref().unwrap_or("default"),
|
||||||
|
verge.tray_inline_outbound_modes.unwrap_or(false),
|
||||||
|
verge.language.as_deref().unwrap_or(""),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group/node identity plus ordering, canonicalized so it is stable across fetches.
|
||||||
|
pub(super) fn proxy_structure_signature(
|
||||||
|
proxy_mode: &str,
|
||||||
|
order_map: Option<&HashMap<String, usize>>,
|
||||||
|
proxy_nodes_data: &Proxies,
|
||||||
|
) -> String {
|
||||||
|
let mut parts: Vec<std::string::String> = proxy_nodes_data
|
||||||
|
.proxies
|
||||||
|
.iter()
|
||||||
|
.filter(|(name, data)| {
|
||||||
|
let global = name.as_str() == "GLOBAL";
|
||||||
|
(match proxy_mode {
|
||||||
|
"global" => global,
|
||||||
|
_ => !global,
|
||||||
|
}) && !data.hidden.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.filter_map(|(name, data)| {
|
||||||
|
data.all.as_ref().map(|all| {
|
||||||
|
let order = order_map
|
||||||
|
.and_then(|map| map.get(name.as_str()))
|
||||||
|
.map_or_else(|| "-".to_owned(), |index| index.to_string());
|
||||||
|
format!(
|
||||||
|
"{}@{order}:{}",
|
||||||
|
name,
|
||||||
|
all.iter().map(|node| node.as_str()).collect::<Vec<_>>().join(",")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
parts.sort();
|
||||||
|
parts.join(";").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct ReconcileContext<'a> {
|
||||||
|
pub(super) app_handle: &'a AppHandle<Wry>,
|
||||||
|
pub(super) tray: &'a TrayIcon<Wry>,
|
||||||
|
pub(super) options: TrayMenuOptions<'a>,
|
||||||
|
pub(super) verge: &'a IVerge,
|
||||||
|
pub(super) profiles: &'a IProfiles,
|
||||||
|
pub(super) fetched_proxies: Option<&'a Proxies>,
|
||||||
|
pub(super) order_map: Option<&'a HashMap<String, usize>>,
|
||||||
|
pub(super) menu_signature: &'a str,
|
||||||
|
pub(super) proxy_signature: Option<&'a str>,
|
||||||
|
pub(super) fetched: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reconcile(state_store: &Mutex<Option<TrayMenuState>>, ctx: ReconcileContext<'_>) -> anyhow::Result<()> {
|
||||||
|
let tun_mode_available = crate::core::runstate::RUN_STATE.state().tun_capable();
|
||||||
|
{
|
||||||
|
let mut attached = state_store.lock();
|
||||||
|
if let Some(state) = attached.as_mut().filter(|state| state.signature == ctx.menu_signature) {
|
||||||
|
let in_place = !ctx.fetched || state.proxy_signature.as_deref() == ctx.proxy_signature;
|
||||||
|
let swapped = !in_place
|
||||||
|
&& ctx.fetched_proxies.is_some_and(|data| {
|
||||||
|
state.swap_proxy_section(
|
||||||
|
ctx.app_handle,
|
||||||
|
ctx.options.mode.unwrap_or(""),
|
||||||
|
ctx.order_map,
|
||||||
|
data,
|
||||||
|
ctx.proxy_signature,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if in_place || swapped {
|
||||||
|
state.apply_static(&ctx.options, ctx.profiles, tun_mode_available);
|
||||||
|
if in_place && let Some(data) = ctx.fetched_proxies {
|
||||||
|
state.apply_proxy(data);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (menu, mut new_state) = build_tray_menu(
|
||||||
|
ctx.app_handle,
|
||||||
|
ctx.options,
|
||||||
|
ctx.verge,
|
||||||
|
ctx.profiles,
|
||||||
|
ctx.order_map,
|
||||||
|
ctx.fetched_proxies,
|
||||||
|
)?;
|
||||||
|
new_state.signature = ctx.menu_signature.into();
|
||||||
|
new_state.proxy_signature = if ctx.fetched {
|
||||||
|
ctx.proxy_signature.map(Into::into)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
logging_error!(Type::Tray, ctx.tray.set_menu(Some(menu)));
|
||||||
|
*state_store.lock() = Some(new_state);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrayMenuState {
|
||||||
|
fn apply_static(&self, options: &TrayMenuOptions<'_>, profiles: &IProfiles, tun_mode_available: bool) {
|
||||||
|
let mode = options.mode.unwrap_or("");
|
||||||
|
for (item, name) in self.mode_items.iter().zip(["rule", "global", "direct"]) {
|
||||||
|
logging_error!(Type::Tray, item.set_checked(mode == name));
|
||||||
|
}
|
||||||
|
logging_error!(
|
||||||
|
Type::Tray,
|
||||||
|
self.system_proxy_item.set_checked(options.system_proxy_enabled)
|
||||||
|
);
|
||||||
|
logging_error!(Type::Tray, self.tun_item.set_enabled(tun_mode_available));
|
||||||
|
logging_error!(Type::Tray, self.tun_item.set_checked(options.tun_mode_enabled));
|
||||||
|
logging_error!(
|
||||||
|
Type::Tray,
|
||||||
|
self.lightweight_item.set_checked(options.is_lightweight_mode)
|
||||||
|
);
|
||||||
|
let current = profiles.current.as_deref();
|
||||||
|
for (uid, item) in &self.profile_items {
|
||||||
|
logging_error!(Type::Tray, item.set_checked(current == Some(uid.as_str())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_proxy(&mut self, proxy_nodes_data: &Proxies) {
|
||||||
|
for group in self.proxy_groups.iter_mut() {
|
||||||
|
let Some(group_data) = proxy_nodes_data.proxies.get(group.name.as_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let now = group_data.now.as_deref().unwrap_or_default();
|
||||||
|
for node in group.nodes.iter_mut() {
|
||||||
|
let text: String = format!(
|
||||||
|
"{} | {}",
|
||||||
|
node.name,
|
||||||
|
proxy_delay_text(proxy_nodes_data, node.name.as_str())
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
if node.text != text {
|
||||||
|
logging_error!(Type::Tray, node.item.set_text(&text));
|
||||||
|
node.text = text;
|
||||||
|
}
|
||||||
|
let checked = node.name == now;
|
||||||
|
if node.checked != checked {
|
||||||
|
logging_error!(Type::Tray, node.item.set_checked(checked));
|
||||||
|
node.checked = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn swap_proxy_section(
|
||||||
|
&mut self,
|
||||||
|
app_handle: &AppHandle<Wry>,
|
||||||
|
proxy_mode: &str,
|
||||||
|
order_map: Option<&HashMap<String, usize>>,
|
||||||
|
proxy_nodes_data: &Proxies,
|
||||||
|
proxy_signature: Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(proxies_submenu) = self.proxies_submenu.clone() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let (group_menus, group_state) =
|
||||||
|
create_subcreate_proxy_menu_item(app_handle, proxy_mode, order_map, proxy_nodes_data);
|
||||||
|
if group_menus.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
while proxies_submenu.remove_at(0).is_ok_and(|item| item.is_some()) {}
|
||||||
|
let group_menus_refs: Vec<&dyn IsMenuItem<Wry>> =
|
||||||
|
group_menus.iter().map(|menu| menu as &dyn IsMenuItem<Wry>).collect();
|
||||||
|
logging_error!(Type::Tray, proxies_submenu.append_items(&group_menus_refs));
|
||||||
|
self.proxy_groups = group_state;
|
||||||
|
self.proxy_signature = proxy_signature.map(Into::into);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
+208
-161
@@ -1,4 +1,4 @@
|
|||||||
use crate::config::{IProfilePreview, IVerge};
|
use crate::config::{IProfilePreview, IProfiles, IVerge};
|
||||||
use crate::core::lightweight;
|
use crate::core::lightweight;
|
||||||
use crate::core::tray::menu_def::TrayAction;
|
use crate::core::tray::menu_def::TrayAction;
|
||||||
use crate::process::AsyncHandler;
|
use crate::process::AsyncHandler;
|
||||||
@@ -13,6 +13,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter};
|
use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter};
|
||||||
use clash_verge_logging::logging_error;
|
use clash_verge_logging::logging_error;
|
||||||
|
use parking_lot::Mutex;
|
||||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||||
use tauri_plugin_mihomo::models::Proxies;
|
use tauri_plugin_mihomo::models::Proxies;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
@@ -29,9 +30,14 @@ use tauri::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod menu_def;
|
mod menu_def;
|
||||||
|
mod menu_state;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod speed_task;
|
mod speed_task;
|
||||||
use menu_def::{MenuIds, MenuTexts};
|
use menu_def::{MenuIds, MenuTexts};
|
||||||
|
use menu_state::{
|
||||||
|
ProxyGroupItems, ProxyNodeItem, ReconcileContext, TrayMenuState, menu_signature, proxy_delay_text,
|
||||||
|
proxy_structure_signature, reconcile,
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑)
|
// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑)
|
||||||
|
|
||||||
@@ -41,7 +47,10 @@ const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
|||||||
pub const TRAY_ID: &str = "clash-verge-rev-tray";
|
pub const TRAY_ID: &str = "clash-verge-rev-tray";
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct TrayMenuOptions {
|
struct TrayMenuOptions<'a> {
|
||||||
|
mode: Option<&'a str>,
|
||||||
|
system_proxy_enabled: bool,
|
||||||
|
tun_mode_enabled: bool,
|
||||||
is_lightweight_mode: bool,
|
is_lightweight_mode: bool,
|
||||||
include_proxy_groups: bool,
|
include_proxy_groups: bool,
|
||||||
}
|
}
|
||||||
@@ -59,6 +68,7 @@ pub struct Tray {
|
|||||||
limiter: SystemLimiter,
|
limiter: SystemLimiter,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
speed_controller: speed_task::TraySpeedController,
|
speed_controller: speed_task::TraySpeedController,
|
||||||
|
menu_state: Mutex<Option<TrayMenuState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrayState {
|
impl TrayState {
|
||||||
@@ -132,6 +142,7 @@ impl Default for Tray {
|
|||||||
limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock),
|
limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
speed_controller: speed_task::TraySpeedController::new(),
|
speed_controller: speed_task::TraySpeedController::new(),
|
||||||
|
menu_state: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,9 +212,8 @@ impl Tray {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let verge = Config::verge().await.latest_arc();
|
let verge = Config::verge().await.latest_arc();
|
||||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
let system_proxy = verge.enable_system_proxy.unwrap_or(false);
|
||||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
let tun_mode = verge.enable_tun_mode.unwrap_or(false);
|
||||||
let tun_mode_available = crate::core::runstate::RUN_STATE.state().tun_capable();
|
|
||||||
let mode = {
|
let mode = {
|
||||||
Config::clash()
|
Config::clash()
|
||||||
.await
|
.await
|
||||||
@@ -214,29 +224,81 @@ impl Tray {
|
|||||||
.unwrap_or("rule")
|
.unwrap_or("rule")
|
||||||
.to_owned()
|
.to_owned()
|
||||||
};
|
};
|
||||||
let profiles_config = Config::profiles().await;
|
|
||||||
let profiles_arc = profiles_config.latest_arc();
|
|
||||||
let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default();
|
|
||||||
let is_lightweight_mode = is_in_lightweight_mode();
|
let is_lightweight_mode = is_in_lightweight_mode();
|
||||||
|
|
||||||
logging_error!(
|
let fetch_proxy_groups =
|
||||||
Type::Tray,
|
include_proxy_groups && verge.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
||||||
tray.set_menu(Some(
|
|
||||||
create_tray_menu(
|
// TODO: should update tray menu again when it was timeout error
|
||||||
app_handle,
|
let (proxy_nodes_data, runtime_proxy_groups_order) = if fetch_proxy_groups {
|
||||||
Some(mode.as_str()),
|
let proxy_nodes_data =
|
||||||
*system_proxy,
|
tokio::time::timeout(Duration::from_millis(1000), handle::Handle::mihomo().get_proxies())
|
||||||
*tun_mode,
|
.await
|
||||||
tun_mode_available,
|
.map_or(None, |res| res.ok());
|
||||||
profiles_preview,
|
|
||||||
TrayMenuOptions {
|
let runtime = Config::runtime().await.latest_arc();
|
||||||
is_lightweight_mode,
|
let runtime_proxy_groups_order = runtime.config.as_ref().map(|config| {
|
||||||
include_proxy_groups,
|
config
|
||||||
|
.get("proxy-groups")
|
||||||
|
.and_then(|groups| groups.as_sequence())
|
||||||
|
.map(|groups| {
|
||||||
|
groups
|
||||||
|
.iter()
|
||||||
|
.filter_map(|group| group.get("name"))
|
||||||
|
.filter_map(|name| name.as_str())
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, name)| (name.into(), index))
|
||||||
|
.collect::<HashMap<String, usize>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
|
||||||
|
(proxy_nodes_data, runtime_proxy_groups_order)
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let verge = if fetch_proxy_groups {
|
||||||
|
Config::verge().await.latest_arc()
|
||||||
|
} else {
|
||||||
|
verge
|
||||||
|
};
|
||||||
|
let profiles = Config::profiles().await.latest_arc();
|
||||||
|
|
||||||
|
let menu_signature = menu_signature(&verge, &profiles, &mode);
|
||||||
|
let proxy_signature = proxy_nodes_data
|
||||||
|
.as_ref()
|
||||||
|
.map(|data| proxy_structure_signature(&mode, runtime_proxy_groups_order.as_ref(), data));
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
let dispatch_handle = app_handle.clone();
|
||||||
|
app_handle
|
||||||
|
.run_on_main_thread(move || {
|
||||||
|
let _ = tx.send(reconcile(
|
||||||
|
&Self::global().menu_state,
|
||||||
|
ReconcileContext {
|
||||||
|
app_handle: &dispatch_handle,
|
||||||
|
tray: &tray,
|
||||||
|
options: TrayMenuOptions {
|
||||||
|
mode: Some(mode.as_str()),
|
||||||
|
system_proxy_enabled: system_proxy,
|
||||||
|
tun_mode_enabled: tun_mode,
|
||||||
|
is_lightweight_mode,
|
||||||
|
include_proxy_groups,
|
||||||
|
},
|
||||||
|
verge: &verge,
|
||||||
|
profiles: &profiles,
|
||||||
|
fetched_proxies: proxy_nodes_data.as_ref(),
|
||||||
|
order_map: runtime_proxy_groups_order.as_ref(),
|
||||||
|
menu_signature: &menu_signature,
|
||||||
|
proxy_signature: proxy_signature.as_deref(),
|
||||||
|
fetched: fetch_proxy_groups,
|
||||||
},
|
},
|
||||||
)
|
));
|
||||||
.await?,
|
})
|
||||||
))
|
.map_err(|e| anyhow::anyhow!("Failed to dispatch tray menu update: {e}"))?;
|
||||||
);
|
rx.await
|
||||||
|
.map_err(|_| anyhow::anyhow!("Tray menu update returned no result"))??;
|
||||||
|
|
||||||
logging!(debug, Type::Tray, "托盘菜单更新成功");
|
logging!(debug, Type::Tray, "托盘菜单更新成功");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -440,7 +502,7 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<&str, &str> {
|
|||||||
fn create_profile_menu_item(
|
fn create_profile_menu_item(
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
profiles_preview: Vec<IProfilePreview<'_>>,
|
profiles_preview: Vec<IProfilePreview<'_>>,
|
||||||
) -> Result<Vec<CheckMenuItem<Wry>>> {
|
) -> Result<Vec<(String, CheckMenuItem<Wry>)>> {
|
||||||
profiles_preview
|
profiles_preview
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|profile| {
|
.map(|profile| {
|
||||||
@@ -452,6 +514,7 @@ fn create_profile_menu_item(
|
|||||||
profile.is_current,
|
profile.is_current,
|
||||||
None::<&str>,
|
None::<&str>,
|
||||||
)
|
)
|
||||||
|
.map(|item| (profile.uid.clone(), item))
|
||||||
.map_err(|e| e.into())
|
.map_err(|e| e.into())
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -460,93 +523,96 @@ fn create_profile_menu_item(
|
|||||||
fn create_subcreate_proxy_menu_item(
|
fn create_subcreate_proxy_menu_item(
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
proxy_mode: &str,
|
proxy_mode: &str,
|
||||||
proxy_group_order_map: Option<HashMap<String, usize>>,
|
proxy_group_order_map: Option<&HashMap<String, usize>>,
|
||||||
proxy_nodes_data: Option<Proxies>,
|
proxy_nodes_data: &Proxies,
|
||||||
) -> Vec<Submenu<Wry>> {
|
) -> (Vec<Submenu<Wry>>, Vec<ProxyGroupItems>) {
|
||||||
let proxy_submenus: Vec<Submenu<Wry>> = {
|
let mut submenus: Vec<(String, usize, Submenu<Wry>, Vec<ProxyNodeItem>)> = Vec::new();
|
||||||
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
|
|
||||||
|
|
||||||
// TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息
|
// TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息
|
||||||
if let Some(proxy_nodes_data) = proxy_nodes_data {
|
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
|
||||||
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
|
let should_show = match proxy_mode {
|
||||||
let should_show = match proxy_mode {
|
"global" => group_name == "GLOBAL",
|
||||||
"global" => group_name == "GLOBAL",
|
_ => group_name != "GLOBAL",
|
||||||
_ => group_name != "GLOBAL",
|
} && !group_data.hidden.unwrap_or_default();
|
||||||
} && !group_data.hidden.unwrap_or_default();
|
|
||||||
|
|
||||||
if !should_show {
|
if !should_show {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(all_proxies) = group_data.all.as_ref() else {
|
let Some(all_proxies) = group_data.all.as_ref() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let now_proxy = group_data.now.as_deref().unwrap_or_default();
|
let now_proxy = group_data.now.as_deref().unwrap_or_default();
|
||||||
|
|
||||||
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
|
let mut group_items: Vec<CheckMenuItem<Wry>> = Vec::with_capacity(all_proxies.len());
|
||||||
.iter()
|
let mut nodes: Vec<ProxyNodeItem> = Vec::with_capacity(all_proxies.len());
|
||||||
.filter_map(|proxy_str| {
|
for proxy_str in all_proxies {
|
||||||
let is_selected = *proxy_str == now_proxy;
|
let is_selected = proxy_str == now_proxy;
|
||||||
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
|
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
|
||||||
|
let display_text = format!(
|
||||||
let delay_text = proxy_nodes_data
|
"{} | {}",
|
||||||
.proxies
|
proxy_str,
|
||||||
.get(proxy_str)
|
proxy_delay_text(proxy_nodes_data, proxy_str.as_str())
|
||||||
.and_then(|h| h.history.last())
|
);
|
||||||
.map(|h| match h.delay {
|
|
||||||
0 => "-ms".into(),
|
match CheckMenuItem::with_id(app_handle, item_id, &display_text, true, is_selected, None::<&str>) {
|
||||||
delay if delay >= 10000 => "-ms".into(),
|
Ok(item) => {
|
||||||
_ => format!("{}ms", h.delay),
|
group_items.push(item.clone());
|
||||||
})
|
nodes.push(ProxyNodeItem {
|
||||||
.unwrap_or_else(|| "-ms".into());
|
name: proxy_str.clone().into(),
|
||||||
|
item,
|
||||||
let display_text = format!("{} | {}", proxy_str, delay_text);
|
text: display_text.as_str().into(),
|
||||||
|
checked: is_selected,
|
||||||
CheckMenuItem::with_id(app_handle, item_id, display_text, true, is_selected, None::<&str>)
|
});
|
||||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e))
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if group_items.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let group_display_name = group_name.to_string();
|
|
||||||
|
|
||||||
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> =
|
|
||||||
group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>).collect();
|
|
||||||
|
|
||||||
if let Ok(submenu) = Submenu::with_id_and_items(
|
|
||||||
app_handle,
|
|
||||||
format!("proxy_group_{}", group_name),
|
|
||||||
group_display_name,
|
|
||||||
true,
|
|
||||||
&group_items_refs,
|
|
||||||
) {
|
|
||||||
let insertion_index = submenus.len();
|
|
||||||
submenus.push((group_name.into(), insertion_index, submenu));
|
|
||||||
} else {
|
|
||||||
logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name);
|
|
||||||
}
|
}
|
||||||
|
Err(e) => logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(order_map) = proxy_group_order_map.as_ref() {
|
if group_items.is_empty() {
|
||||||
submenus.sort_by(|(name_a, original_index_a, _), (name_b, original_index_b, _)| {
|
continue;
|
||||||
match (order_map.get(name_a), order_map.get(name_b)) {
|
|
||||||
(Some(index_a), Some(index_b)) => index_a.cmp(index_b),
|
|
||||||
(Some(_), None) => std::cmp::Ordering::Less,
|
|
||||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
|
||||||
(None, None) => original_index_a.cmp(original_index_b),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submenus.into_iter().map(|(_, _, submenu)| submenu).collect()
|
let group_display_name = group_name.to_string();
|
||||||
};
|
|
||||||
proxy_submenus
|
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> =
|
||||||
|
group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>).collect();
|
||||||
|
|
||||||
|
if let Ok(submenu) = Submenu::with_id_and_items(
|
||||||
|
app_handle,
|
||||||
|
format!("proxy_group_{}", group_name),
|
||||||
|
group_display_name,
|
||||||
|
true,
|
||||||
|
&group_items_refs,
|
||||||
|
) {
|
||||||
|
let insertion_index = submenus.len();
|
||||||
|
submenus.push((group_name.into(), insertion_index, submenu, nodes));
|
||||||
|
} else {
|
||||||
|
logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(order_map) = proxy_group_order_map {
|
||||||
|
submenus.sort_by(|(name_a, original_index_a, _, _), (name_b, original_index_b, _, _)| {
|
||||||
|
match (order_map.get(name_a), order_map.get(name_b)) {
|
||||||
|
(Some(index_a), Some(index_b)) => index_a.cmp(index_b),
|
||||||
|
(Some(_), None) => std::cmp::Ordering::Less,
|
||||||
|
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||||
|
(None, None) => original_index_a.cmp(original_index_b),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut groups = Vec::with_capacity(submenus.len());
|
||||||
|
let group_menus = submenus
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, _, submenu, nodes)| {
|
||||||
|
groups.push(ProxyGroupItems { name, nodes });
|
||||||
|
submenu
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
(group_menus, groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_proxy_menu_item(
|
fn create_proxy_menu_item(
|
||||||
@@ -585,53 +651,16 @@ fn create_proxy_menu_item(
|
|||||||
Ok((proxies_submenu, inline_proxy_items))
|
Ok((proxies_submenu, inline_proxy_items))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_tray_menu(
|
fn build_tray_menu(
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
mode: Option<&str>,
|
options: TrayMenuOptions<'_>,
|
||||||
system_proxy_enabled: bool,
|
verge_settings: &IVerge,
|
||||||
tun_mode_enabled: bool,
|
profiles_config: &IProfiles,
|
||||||
tun_mode_available: bool,
|
proxy_group_order_map: Option<&HashMap<String, usize>>,
|
||||||
profiles_preview: Vec<IProfilePreview<'_>>,
|
proxy_nodes_data: Option<&Proxies>,
|
||||||
options: TrayMenuOptions,
|
) -> Result<(tauri::menu::Menu<Wry>, TrayMenuState)> {
|
||||||
) -> Result<tauri::menu::Menu<Wry>> {
|
let current_proxy_mode = options.mode.unwrap_or("");
|
||||||
let current_proxy_mode = mode.unwrap_or("");
|
let tun_mode_available = crate::core::runstate::RUN_STATE.state().tun_capable();
|
||||||
|
|
||||||
let mut verge_settings = Config::verge().await.latest_arc();
|
|
||||||
let fetch_proxy_groups =
|
|
||||||
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
|
||||||
|
|
||||||
// TODO: should update tray menu again when it was timeout error
|
|
||||||
let (proxy_nodes_data, runtime_proxy_groups_order) = if fetch_proxy_groups {
|
|
||||||
let proxy_nodes_data =
|
|
||||||
tokio::time::timeout(Duration::from_millis(1000), handle::Handle::mihomo().get_proxies())
|
|
||||||
.await
|
|
||||||
.map_or(None, |res| res.ok());
|
|
||||||
|
|
||||||
let runtime = Config::runtime().await.latest_arc();
|
|
||||||
let runtime_proxy_groups_order = runtime.config.as_ref().map(|config| {
|
|
||||||
config
|
|
||||||
.get("proxy-groups")
|
|
||||||
.and_then(|groups| groups.as_sequence())
|
|
||||||
.map(|groups| {
|
|
||||||
groups
|
|
||||||
.iter()
|
|
||||||
.filter_map(|group| group.get("name"))
|
|
||||||
.filter_map(|name| name.as_str())
|
|
||||||
.enumerate()
|
|
||||||
.map(|(index, name)| (name.into(), index))
|
|
||||||
.collect::<HashMap<String, usize>>()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
});
|
|
||||||
|
|
||||||
(proxy_nodes_data, runtime_proxy_groups_order)
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if fetch_proxy_groups {
|
|
||||||
verge_settings = Config::verge().await.latest_arc();
|
|
||||||
}
|
|
||||||
|
|
||||||
let tray_proxy_groups_display_mode = verge_settings
|
let tray_proxy_groups_display_mode = verge_settings
|
||||||
.tray_proxy_groups_display_mode
|
.tray_proxy_groups_display_mode
|
||||||
@@ -639,20 +668,19 @@ async fn create_tray_menu(
|
|||||||
.unwrap_or("default");
|
.unwrap_or("default");
|
||||||
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable";
|
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable";
|
||||||
|
|
||||||
let proxy_group_order_map = runtime_proxy_groups_order;
|
|
||||||
|
|
||||||
let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false);
|
let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false);
|
||||||
|
|
||||||
let version = env!("CARGO_PKG_VERSION");
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
let hotkeys = create_hotkeys(&verge_settings.hotkeys);
|
let hotkeys = create_hotkeys(&verge_settings.hotkeys);
|
||||||
|
|
||||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = create_profile_menu_item(app_handle, profiles_preview)?;
|
let profiles_preview = profiles_config.profiles_preview().unwrap_or_default();
|
||||||
|
let profile_menu_items: Vec<(String, CheckMenuItem<Wry>)> = create_profile_menu_item(app_handle, profiles_preview)?;
|
||||||
|
|
||||||
let texts = MenuTexts::new();
|
let texts = MenuTexts::new();
|
||||||
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
.map(|(_, item)| item as &dyn IsMenuItem<Wry>)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let open_window = &MenuItem::with_id(
|
let open_window = &MenuItem::with_id(
|
||||||
@@ -720,12 +748,20 @@ async fn create_tray_menu(
|
|||||||
&profile_menu_items_refs,
|
&profile_menu_items_refs,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let (proxies_menu, inline_proxy_items) = if include_proxy_groups {
|
let mut proxy_groups: Vec<ProxyGroupItems> = Vec::new();
|
||||||
let proxy_sub_menus =
|
let mut proxies_submenu_handle: Option<Submenu<Wry>> = None;
|
||||||
|
let (proxies_menu, inline_proxy_items) = if include_proxy_groups && let Some(proxy_nodes_data) = proxy_nodes_data {
|
||||||
|
let (proxy_sub_menus, groups) =
|
||||||
create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data);
|
create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data);
|
||||||
|
proxy_groups = groups;
|
||||||
|
|
||||||
match tray_proxy_groups_display_mode {
|
match tray_proxy_groups_display_mode {
|
||||||
"default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?,
|
"default" => {
|
||||||
|
let (submenu, inline_items) =
|
||||||
|
create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?;
|
||||||
|
proxies_submenu_handle = submenu.clone();
|
||||||
|
(submenu, inline_items)
|
||||||
|
}
|
||||||
"inline" => create_proxy_menu_item(app_handle, true, proxy_sub_menus, &texts.proxies)?,
|
"inline" => create_proxy_menu_item(app_handle, true, proxy_sub_menus, &texts.proxies)?,
|
||||||
_ => (None, Vec::new()),
|
_ => (None, Vec::new()),
|
||||||
}
|
}
|
||||||
@@ -738,7 +774,7 @@ async fn create_tray_menu(
|
|||||||
MenuIds::SYSTEM_PROXY,
|
MenuIds::SYSTEM_PROXY,
|
||||||
&texts.system_proxy,
|
&texts.system_proxy,
|
||||||
true,
|
true,
|
||||||
system_proxy_enabled,
|
options.system_proxy_enabled,
|
||||||
hotkeys.get("toggle_system_proxy").copied(),
|
hotkeys.get("toggle_system_proxy").copied(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -747,7 +783,7 @@ async fn create_tray_menu(
|
|||||||
MenuIds::TUN_MODE,
|
MenuIds::TUN_MODE,
|
||||||
&texts.tun_mode,
|
&texts.tun_mode,
|
||||||
tun_mode_available,
|
tun_mode_available,
|
||||||
tun_mode_enabled,
|
options.tun_mode_enabled,
|
||||||
hotkeys.get("toggle_tun_mode").copied(),
|
hotkeys.get("toggle_tun_mode").copied(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -866,7 +902,18 @@ async fn create_tray_menu(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
|
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
|
||||||
Ok(menu)
|
let state = TrayMenuState {
|
||||||
|
mode_items: [rule_mode.clone(), global_mode.clone(), direct_mode.clone()],
|
||||||
|
system_proxy_item: system_proxy.clone(),
|
||||||
|
tun_item: tun_mode.clone(),
|
||||||
|
lightweight_item: lightweight_mode.clone(),
|
||||||
|
profile_items: profile_menu_items,
|
||||||
|
proxies_submenu: proxies_submenu_handle,
|
||||||
|
proxy_groups,
|
||||||
|
signature: String::new(),
|
||||||
|
proxy_signature: None,
|
||||||
|
};
|
||||||
|
Ok((menu, state))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) {
|
fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) {
|
||||||
|
|||||||
Reference in New Issue
Block a user