diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bd566566d..c74e6d1f9 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -45,7 +45,7 @@ tauri = { workspace = true, features = [ "image-ico", "image-png", ] } -parking_lot = { workspace = true } +parking_lot = { workspace = true, features = ["send_guard"] } anyhow = { workspace = true } tokio = { workspace = true } compact_str = { workspace = true } @@ -65,7 +65,12 @@ boa_engine = "0.21.0" once_cell = { version = "1.21.3", features = ["parking_lot"] } delay_timer = "0.11.6" percent-encoding = "2.3.2" -reqwest = { version = "0.13.1", features = ["json", "cookies", "rustls", "form"] } +reqwest = { version = "0.13.1", features = [ + "json", + "cookies", + "rustls", + "form", +] } regex = "1.12.2" sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [ "guard", diff --git a/src-tauri/src/core/tray/logic.rs b/src-tauri/src/core/tray/logic.rs index 6a7775f7f..191304b01 100644 --- a/src-tauri/src/core/tray/logic.rs +++ b/src-tauri/src/core/tray/logic.rs @@ -4,42 +4,23 @@ use tokio::fs; use crate::{ config::IVerge, - core::tray::view::{IconBytes, IconStyle, ProxyStatus, TrayState, TrayStateImpl}, + core::tray::view::{ + CacheComponent, IconBytes, IconStyle, ProxyStatus, TrayComponent, TrayIcon, TrayIconCache, TrayIconView, + }, utils::dirs::find_target_icons, }; - -impl TrayState for TrayStateImpl { - async fn parse_icon_from_verge(verge: &IVerge) -> (IconStyle, IconBytes) { - let enable_proxy = verge.enable_system_proxy.unwrap_or(false); - let enable_tun = verge.enable_tun_mode.unwrap_or(false); - let proxy_status = calculate_icon_status(enable_proxy, enable_tun); - - let is_custom = match proxy_status { - ProxyStatus::Idle => verge.common_tray_icon, - ProxyStatus::Proxy => verge.sysproxy_tray_icon, - _ => verge.tun_tray_icon, +impl TrayIcon { + const fn calculate_icon_status(enable_proxy: bool, enable_tun: bool) -> ProxyStatus { + match (enable_proxy, enable_tun) { + (false, false) => ProxyStatus::Idle, + (true, false) => ProxyStatus::Proxy, + (false, true) => ProxyStatus::Tun, + (true, true) => ProxyStatus::ProxyTUN, } - .unwrap_or(false); - if is_custom { - if let Some(bytes) = get_custom_icon_bytes(proxy_status).await { - return (IconStyle::Custom, bytes); - } - } - - let is_monochrome = cfg!(target_os = "macos") && verge.tray_icon.as_deref() == Some("monochrome"); - Self::get_tray_icon(proxy_status, is_monochrome.into()).await } - async fn get_tray_icon(proxy_status: ProxyStatus, style: IconStyle) -> (IconStyle, IconBytes) { - Self::get_builtin_icon(proxy_status, style) - } -} - -impl TrayStateImpl { - fn get_builtin_icon(status: ProxyStatus, style: IconStyle) -> (IconStyle, IconBytes) { - let is_mono = cfg!(target_os = "macos") && style == IconStyle::Monochrome; - - let bytes = match (status, is_mono) { + const fn get_builtin_icon(status: ProxyStatus, is_monochrome: bool) -> (IconStyle, IconBytes) { + let bytes = match (status, is_monochrome) { (ProxyStatus::Idle, true) => include_bytes!("../../../icons/tray-icon-mono.ico").as_slice(), (ProxyStatus::Idle, false) => include_bytes!("../../../icons/tray-icon.ico").as_slice(), @@ -51,7 +32,7 @@ impl TrayStateImpl { }; ( - if is_mono { + if is_monochrome { IconStyle::Monochrome } else { IconStyle::Normal @@ -59,24 +40,94 @@ impl TrayStateImpl { Cow::Borrowed(bytes), ) } -} -fn calculate_icon_status(enable_proxy: bool, enable_tun: bool) -> ProxyStatus { - match (enable_proxy, enable_tun) { - (false, false) => ProxyStatus::Idle, - (true, false) => ProxyStatus::Proxy, - (false, true) => ProxyStatus::TUN, - (true, true) => ProxyStatus::ProxyTUN, + async fn get_custom_icon_bytes(target: ProxyStatus) -> Option { + let tag = match target { + ProxyStatus::Idle => "common", + ProxyStatus::Proxy => "sysproxy", + _ => "tun", + }; + + let path = find_target_icons(tag).ok()??; + fs::read(path).await.ok().map(Cow::Owned) + } + + async fn from_verge(verge: &IVerge, proxy_status: ProxyStatus, is_monochrome: bool) -> (IconStyle, IconBytes) { + let is_custom = match proxy_status { + ProxyStatus::Idle => verge.common_tray_icon, + ProxyStatus::Proxy => verge.sysproxy_tray_icon, + _ => verge.tun_tray_icon, + } + .unwrap_or(false); + if is_custom && let Some(bytes) = Self::get_custom_icon_bytes(proxy_status).await { + return (IconStyle::Custom, bytes); + } + + Self::get_builtin_icon(proxy_status, is_monochrome) } } -async fn get_custom_icon_bytes(target: ProxyStatus) -> Option { - let tag = match target { - ProxyStatus::Idle => "common", - ProxyStatus::Proxy => "sysproxy", - _ => "tun", - }; +impl CacheComponent for TrayIcon { + type Cache = TrayIconCache; + type View<'a> + = TrayIconView<'a> + where + Self: 'a; - let path = find_target_icons(tag).ok()??; - fs::read(path).await.ok().map(Cow::Owned) + fn is_some(&self) -> bool { + !self.0.last_icon_bytes.is_empty() + } + + fn get(&self) -> Self::View<'_> { + TrayIconView { + last_icon_style: &self.0.last_icon_style, + last_icon_bytes: &self.0.last_icon_bytes, + } + } + + fn update(&mut self, t: Self::Cache) { + self.0 = t; + } + + /// We assume the cache is equal when both status and style are equal + /// and ignore the actual icon bytes comparison + fn equals(&self, other: &Self::Cache) -> bool { + self.0.last_proxy_status == other.last_proxy_status && self.0.last_icon_style == other.last_icon_style + } +} + +#[async_trait::async_trait] +impl TrayComponent for TrayIcon { + type Context = IVerge; + + async fn refresh(&mut self, force: bool, verge: &Self::Context) -> bool { + let enable_proxy = verge.enable_system_proxy.unwrap_or(false); + let enable_tun = verge.enable_tun_mode.unwrap_or(false); + let is_monochrome = cfg!(target_os = "macos") && verge.tray_icon.as_deref() == Some("monochrome"); + + let target_status = Self::calculate_icon_status(enable_proxy, enable_tun); + let target_style = if is_monochrome { + IconStyle::Monochrome + } else { + IconStyle::Normal + }; + + let cmpare_cache = TrayIconCache { + last_icon_style: target_style, + last_proxy_status: target_status, + last_icon_bytes: Cow::Borrowed(&[]), + }; + if !force && self.is_some() && self.equals(&cmpare_cache) { + return false; + } + + let (icon_style, icon_bytes) = Self::from_verge(verge, target_status, is_monochrome).await; + self.update(TrayIconCache { + last_icon_style: icon_style, + last_proxy_status: target_status, + last_icon_bytes: icon_bytes, + }); + + true + } } diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index ba4f29aba..dd63a46e6 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -1,13 +1,16 @@ use governor::{DefaultDirectRateLimiter, Quota, RateLimiter}; +use parking_lot::Mutex; use tauri::tray::TrayIconBuilder; use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin; use tauri_plugin_mihomo::models::Proxies; -pub mod data; +// pub mod data; mod logic; mod view; use crate::config::{IProfilePreview, IVerge}; use crate::core::service; -use crate::core::tray::view::{TrayState, TrayStateImpl}; +#[cfg(target_os = "macos")] +use crate::core::tray::view::IconStyle; +use crate::core::tray::view::{CacheComponent, TrayComponent, TrayIcon}; use crate::module::lightweight; use crate::process::AsyncHandler; use crate::singleton; @@ -20,6 +23,7 @@ use smartstring::alias::String; use std::borrow::Cow; use std::collections::HashMap; use std::num::NonZeroU32; +use std::sync::Arc; use std::time::Duration; use tauri::{ AppHandle, Wry, @@ -29,14 +33,13 @@ use tauri::{ mod menu_def; use menu_def::{MenuIds, MenuTexts}; -// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑) - type ProxyMenuItem = (Option>, Vec>>); const TRAY_CLICK_DEBOUNCE_MS: u64 = 1_275; pub struct Tray { limiter: DefaultDirectRateLimiter, + icon_compoment: Arc>, } impl Default for Tray { @@ -48,6 +51,7 @@ impl Default for Tray { .unwrap() .allow_burst(NonZeroU32::new(1).unwrap()), ), + icon_compoment: Arc::new(Mutex::new(Default::default())), } } } @@ -175,11 +179,20 @@ impl Tray { } }; - let (_icon_style, icon_bytes) = TrayStateImpl::parse_icon_from_verge(verge).await; + let mut icon_compoment_guard = self.icon_compoment.lock(); + let is_refresh = icon_compoment_guard.refresh(false, verge).await; + if !is_refresh { + logging!(debug, Type::Tray, "托盘图标未更改,跳过更新"); + return Ok(()); + } - let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); + let icon_comp_view = icon_compoment_guard.get(); + + let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(icon_comp_view.last_icon_bytes)?)); #[cfg(target_os = "macos")] - let _ = tray.set_icon_as_template(_icon_style.into()); + let _ = tray.set_icon_as_template(matches!(icon_comp_view.last_icon_style, IconStyle::Monochrome)); + + drop(icon_compoment_guard); Ok(()) } @@ -270,8 +283,12 @@ impl Tray { let verge = Config::verge().await.data_arc(); - let (_icon_style, icon_bytes) = TrayStateImpl::parse_icon_from_verge(&verge).await; - let icon = tauri::image::Image::from_bytes(&icon_bytes)?; + { + self.icon_compoment.lock().refresh(false, &verge).await; + } + let cache_icon_guard = self.icon_compoment.lock(); + let icon = tauri::image::Image::from_bytes(cache_icon_guard.get().last_icon_bytes)?; + drop(cache_icon_guard); #[cfg(target_os = "linux")] let builder = TrayIconBuilder::with_id("main").icon(icon).icon_as_template(false); @@ -488,7 +505,7 @@ fn create_proxy_menu_item( app_handle: &AppHandle, show_proxy_groups_inline: bool, proxy_submenus: Vec>, - proxies_text: &Cow<'_, str>, + proxies_text: &str, ) -> Result { // 创建代理主菜单 let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline { diff --git a/src-tauri/src/core/tray/view.rs b/src-tauri/src/core/tray/view.rs index 4db7f0194..dd9a087a2 100644 --- a/src-tauri/src/core/tray/view.rs +++ b/src-tauri/src/core/tray/view.rs @@ -1,7 +1,5 @@ use std::borrow::Cow; -use crate::config::IVerge; - pub type IconBytes = Cow<'static, [u8]>; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -12,12 +10,12 @@ pub(crate) enum ProxyStatus { // Proxy Enabled, Not TUN Proxy, // Not Proxy, TUN Enabled - TUN, + Tun, // Proxy Enabled, TUN Enabled ProxyTUN, } -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub(crate) enum IconStyle { #[default] Normal, @@ -25,28 +23,36 @@ pub(crate) enum IconStyle { Monochrome, } -impl From for IconStyle { - fn from(is_monochrome: bool) -> Self { - if is_monochrome { - IconStyle::Monochrome - } else { - IconStyle::Normal - } - } +#[async_trait::async_trait] +pub(crate) trait TrayComponent: CacheComponent { + type Context; + + async fn refresh(&mut self, force: bool, ctx: &Self::Context) -> bool; } -impl Into for IconStyle { - fn into(self) -> bool { - match self { - IconStyle::Monochrome => true, - _ => false, - } - } +pub(crate) trait CacheComponent { + type Cache; + type View<'a> + where + Self: 'a; + + fn is_some(&self) -> bool; + fn get(&self) -> Self::View<'_>; + fn update(&mut self, t: Self::Cache); + fn equals(&self, other: &Self::Cache) -> bool; } -pub(crate) trait TrayState { - async fn parse_icon_from_verge(verge: &IVerge) -> (IconStyle, IconBytes); - async fn get_tray_icon(proxy_status: ProxyStatus, style: IconStyle) -> (IconStyle, IconBytes); +#[derive(Default)] +pub(crate) struct TrayIconCache { + pub(crate) last_icon_style: IconStyle, + pub(crate) last_proxy_status: ProxyStatus, + pub(crate) last_icon_bytes: IconBytes, } -pub(crate) struct TrayStateImpl; +pub(crate) struct TrayIconView<'a> { + pub(crate) last_icon_style: &'a IconStyle, + pub(crate) last_icon_bytes: &'a IconBytes, +} + +#[derive(Default)] +pub(crate) struct TrayIcon(pub TrayIconCache);