refactor(tray): reorganize tray module structure and improve icon handling

This commit is contained in:
Tunglies
2026-01-03 08:21:08 +08:00
parent 349be20a6c
commit 971b8ccadf
8 changed files with 179 additions and 167 deletions

31
Cargo.lock generated
View File

@@ -1423,7 +1423,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -3653,12 +3653,12 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.1",
"socket2 0.5.10",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry 0.6.1",
"windows-registry",
]
[[package]]
@@ -5268,7 +5268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]
@@ -6077,7 +6077,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.1",
"socket2 0.5.10",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -6115,7 +6115,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.1",
"socket2 0.5.10",
"tracing",
"windows-sys 0.60.2",
]
@@ -7967,7 +7967,7 @@ dependencies = [
"thiserror 2.0.17",
"tracing",
"url",
"windows-registry 0.5.3",
"windows-registry",
"windows-result 0.3.4",
]
@@ -8079,8 +8079,8 @@ dependencies = [
[[package]]
name = "tauri-plugin-mihomo"
version = "0.1.2"
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#88bf64a17f2c25f306ad22629a8688d094e1cf02"
version = "0.1.3"
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#65500f248533c0700a65f0f081e4bcadda4bff35"
dependencies = [
"base64 0.22.1",
"futures-util",
@@ -9729,7 +9729,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]
@@ -9909,17 +9909,6 @@ dependencies = [
"windows-strings 0.4.2",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"

View File

@@ -0,0 +1,20 @@
use tauri_plugin_mihomo::models::Proxies;
use crate::core::handle;
#[derive(Debug)]
struct MihomoProxies {
proxies: Proxies,
epoch: u64,
}
impl MihomoProxies {
async fn refresh(&mut self) -> anyhow::Result<()> {
let proxies = handle::Handle::mihomo().await.get_proxies().await?;
if proxies != self.proxies {
self.proxies = proxies;
self.epoch += 1;
}
Ok(())
}
}

View File

@@ -0,0 +1,82 @@
use std::borrow::Cow;
use tokio::fs;
use crate::{
config::IVerge,
core::tray::view::{IconBytes, IconStyle, ProxyStatus, TrayState, TrayStateImpl},
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,
}
.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) {
(ProxyStatus::Idle, true) => include_bytes!("../../../icons/tray-icon-mono.ico").as_slice(),
(ProxyStatus::Idle, false) => include_bytes!("../../../icons/tray-icon.ico").as_slice(),
(ProxyStatus::Proxy, true) => include_bytes!("../../../icons/tray-icon-sys-mono-new.ico").as_slice(),
(ProxyStatus::Proxy, false) => include_bytes!("../../../icons/tray-icon-sys.ico").as_slice(),
(_, true) => include_bytes!("../../../icons/tray-icon-tun-mono-new.ico").as_slice(),
(_, false) => include_bytes!("../../../icons/tray-icon-tun.ico").as_slice(),
};
(
if is_mono {
IconStyle::Monochrome
} else {
IconStyle::Normal
},
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)
}

View File

@@ -1,21 +1,11 @@
use clash_verge_i18n::t;
use std::{borrow::Cow, sync::Arc};
fn to_arc_str<S>(value: S) -> Arc<str>
where
S: Into<Cow<'static, str>>,
{
match value.into() {
Cow::Borrowed(s) => Arc::from(s),
Cow::Owned(s) => Arc::from(s.into_boxed_str()),
}
}
use std::borrow::Cow;
macro_rules! define_menu {
($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => {
#[derive(Debug)]
pub struct MenuTexts {
$(pub $field: Arc<str>,)+
$(pub $field: Cow<'static, str>,)+
}
pub struct MenuIds;
@@ -23,7 +13,7 @@ macro_rules! define_menu {
impl MenuTexts {
pub fn new() -> Self {
Self {
$($field: to_arc_str(t!($text)),)+
$($field: t!($text),)+
}
}
}

View File

@@ -2,26 +2,24 @@ use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use tauri::tray::TrayIconBuilder;
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
use tauri_plugin_mihomo::models::Proxies;
use tokio::fs;
#[cfg(target_os = "macos")]
pub mod speed_rate;
pub mod data;
mod logic;
mod view;
use crate::config::{IProfilePreview, IVerge};
use crate::core::service;
use crate::core::tray::view::{TrayState, TrayStateImpl};
use crate::module::lightweight;
use crate::process::AsyncHandler;
use crate::singleton;
use crate::utils::window_manager::WindowManager;
use crate::{
Type, cmd, config::Config, feat, logging, module::lightweight::is_in_lightweight_mode,
utils::dirs::find_target_icons,
};
use crate::{Type, cmd, config::Config, feat, logging, module::lightweight::is_in_lightweight_mode};
use super::handle;
use anyhow::Result;
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,
@@ -37,103 +35,10 @@ type ProxyMenuItem = (Option<Submenu<Wry>>, Vec<Box<dyn IsMenuItem<Wry>>>);
const TRAY_CLICK_DEBOUNCE_MS: u64 = 1_275;
#[derive(Clone)]
struct TrayState {}
pub struct Tray {
limiter: DefaultDirectRateLimiter,
}
impl TrayState {
async fn get_tray_icon(verge: &IVerge) -> (bool, Vec<u8>) {
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
match (*system_mode, *tun_mode) {
(true, true) => Self::get_tun_tray_icon(verge).await,
(true, false) => Self::get_sysproxy_tray_icon(verge).await,
(false, true) => Self::get_tun_tray_icon(verge).await,
(false, false) => Self::get_common_tray_icon(verge).await,
}
}
async fn get_common_tray_icon(verge: &IVerge) -> (bool, Vec<u8>) {
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon
&& let Ok(Some(common_icon_path)) = find_target_icons("common")
&& let Ok(icon_data) = fs::read(common_icon_path).await
{
return (true, icon_data);
}
#[cfg(target_os = "macos")]
{
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or_else(|| "monochrome".into());
if tray_icon_colorful == "monochrome" {
(false, include_bytes!("../../../icons/tray-icon-mono.ico").to_vec())
} else {
(false, include_bytes!("../../../icons/tray-icon.ico").to_vec())
}
}
#[cfg(not(target_os = "macos"))]
{
(false, include_bytes!("../../../icons/tray-icon.ico").to_vec())
}
}
async fn get_sysproxy_tray_icon(verge: &IVerge) -> (bool, Vec<u8>) {
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
&& let Ok(icon_data) = fs::read(sysproxy_icon_path).await
{
return (true, icon_data);
}
#[cfg(target_os = "macos")]
{
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or_else(|| "monochrome".into());
if tray_icon_colorful == "monochrome" {
(
false,
include_bytes!("../../../icons/tray-icon-sys-mono-new.ico").to_vec(),
)
} else {
(false, include_bytes!("../../../icons/tray-icon-sys.ico").to_vec())
}
}
#[cfg(not(target_os = "macos"))]
{
(false, include_bytes!("../../../icons/tray-icon-sys.ico").to_vec())
}
}
async fn get_tun_tray_icon(verge: &IVerge) -> (bool, Vec<u8>) {
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun")
&& let Ok(icon_data) = fs::read(tun_icon_path).await
{
return (true, icon_data);
}
#[cfg(target_os = "macos")]
{
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or_else(|| "monochrome".into());
if tray_icon_colorful == "monochrome" {
(
false,
include_bytes!("../../../icons/tray-icon-tun-mono-new.ico").to_vec(),
)
} else {
(false, include_bytes!("../../../icons/tray-icon-tun.ico").to_vec())
}
}
#[cfg(not(target_os = "macos"))]
{
(false, include_bytes!("../../../icons/tray-icon-tun.ico").to_vec())
}
}
}
impl Default for Tray {
#[allow(clippy::unwrap_used)]
fn default() -> Self {
@@ -254,7 +159,6 @@ impl Tray {
}
/// 更新托盘图标
#[cfg(target_os = "macos")]
pub async fn update_icon(&self, verge: &IVerge) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
@@ -271,36 +175,11 @@ impl Tray {
}
};
let (_is_custom_icon, icon_bytes) = TrayState::get_tray_icon(verge).await;
let colorful = verge.tray_icon.clone().unwrap_or_else(|| "monochrome".into());
let is_colorful = colorful == "colorful";
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
let _ = tray.set_icon_as_template(!is_colorful);
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub async fn update_icon(&self, verge: &IVerge) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
logging!(warn, Type::Tray, "Failed to update tray icon: tray not found");
return Ok(());
}
};
let (_is_custom_icon, icon_bytes) = TrayState::get_tray_icon(verge).await;
let (_icon_style, icon_bytes) = TrayStateImpl::parse_icon_from_verge(verge).await;
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
#[cfg(target_os = "macos")]
let _ = tray.set_icon_as_template(_icon_style.into());
Ok(())
}
@@ -391,7 +270,7 @@ impl Tray {
let verge = Config::verge().await.data_arc();
let icon_bytes = TrayState::get_tray_icon(&verge).await.1;
let (_icon_style, icon_bytes) = TrayStateImpl::parse_icon_from_verge(&verge).await;
let icon = tauri::image::Image::from_bytes(&icon_bytes)?;
#[cfg(target_os = "linux")]
@@ -609,7 +488,7 @@ fn create_proxy_menu_item(
app_handle: &AppHandle,
show_proxy_groups_inline: bool,
proxy_submenus: Vec<Submenu<Wry>>,
proxies_text: &Arc<str>,
proxies_text: &Cow<'_, str>,
) -> Result<ProxyMenuItem> {
// 创建代理主菜单
let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline {

View File

@@ -1 +0,0 @@

View File

@@ -0,0 +1,52 @@
use std::borrow::Cow;
use crate::config::IVerge;
pub type IconBytes = Cow<'static, [u8]>;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ProxyStatus {
#[default]
// Not Proxy, Not TUN
Idle,
// Proxy Enabled, Not TUN
Proxy,
// Not Proxy, TUN Enabled
TUN,
// Proxy Enabled, TUN Enabled
ProxyTUN,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) enum IconStyle {
#[default]
Normal,
Custom,
Monochrome,
}
impl From<bool> for IconStyle {
fn from(is_monochrome: bool) -> Self {
if is_monochrome {
IconStyle::Monochrome
} else {
IconStyle::Normal
}
}
}
impl Into<bool> for IconStyle {
fn into(self) -> bool {
match self {
IconStyle::Monochrome => true,
_ => false,
}
}
}
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);
}
pub(crate) struct TrayStateImpl;

View File

@@ -90,6 +90,7 @@ pub fn app_icons_dir() -> Result<PathBuf> {
Ok(app_home_dir()?.join("icons"))
}
// TODO find target icon when needed
pub fn find_target_icons(target: &str) -> Result<Option<String>> {
let icons_dir = app_icons_dir()?;
let icon_path = fs::read_dir(&icons_dir)?