refactor(tray): enhance tray icon management and improve caching mechanism

This commit is contained in:
Tunglies
2026-01-03 18:53:20 +08:00
parent 971b8ccadf
commit fc3bb3c14b
4 changed files with 161 additions and 82 deletions

View File

@@ -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",

View File

@@ -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<IconBytes> {
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<IconBytes> {
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
}
}

View File

@@ -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<Submenu<Wry>>, Vec<Box<dyn IsMenuItem<Wry>>>);
const TRAY_CLICK_DEBOUNCE_MS: u64 = 1_275;
pub struct Tray {
limiter: DefaultDirectRateLimiter,
icon_compoment: Arc<Mutex<TrayIcon>>,
}
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<Submenu<Wry>>,
proxies_text: &Cow<'_, str>,
proxies_text: &str,
) -> Result<ProxyMenuItem> {
// 创建代理主菜单
let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline {

View File

@@ -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<bool> 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<bool> 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);