mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 07:14:40 +08:00
refactor(tray): enhance tray icon management and improve caching mechanism
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user