Compare commits

...

2 Commits

9 changed files with 259 additions and 168 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

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

@@ -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,133 @@
use std::borrow::Cow;
use tokio::fs;
use crate::{
config::IVerge,
core::tray::view::{
CacheComponent, IconBytes, IconStyle, ProxyStatus, TrayComponent, TrayIcon, TrayIconCache, TrayIconView,
},
utils::dirs::find_target_icons,
};
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,
}
}
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(),
(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_monochrome {
IconStyle::Monochrome
} else {
IconStyle::Normal
},
Cow::Borrowed(bytes),
)
}
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)
}
}
impl CacheComponent for TrayIcon {
type Cache = TrayIconCache;
type View<'a>
= TrayIconView<'a>
where
Self: 'a;
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,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

@@ -1,24 +1,26 @@
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;
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;
#[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;
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;
@@ -31,107 +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;
#[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())
}
}
icon_compoment: Arc<Mutex<TrayIcon>>,
}
impl Default for Tray {
@@ -143,6 +51,7 @@ impl Default for Tray {
.unwrap()
.allow_burst(NonZeroU32::new(1).unwrap()),
),
icon_compoment: Arc::new(Mutex::new(Default::default())),
}
}
}
@@ -254,7 +163,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 +179,20 @@ 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, "应用正在退出,跳过托盘图标更新");
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 app_handle = handle::Handle::app_handle();
let icon_comp_view = icon_compoment_guard.get();
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 _ = 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(matches!(icon_comp_view.last_icon_style, IconStyle::Monochrome));
let (_is_custom_icon, icon_bytes) = TrayState::get_tray_icon(verge).await;
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
drop(icon_compoment_guard);
Ok(())
}
@@ -391,8 +283,12 @@ impl Tray {
let verge = Config::verge().await.data_arc();
let icon_bytes = TrayState::get_tray_icon(&verge).await.1;
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);
@@ -609,7 +505,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: &str,
) -> Result<ProxyMenuItem> {
// 创建代理主菜单
let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline {

View File

@@ -1 +0,0 @@

View File

@@ -0,0 +1,58 @@
use std::borrow::Cow;
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, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IconStyle {
#[default]
Normal,
Custom,
Monochrome,
}
#[async_trait::async_trait]
pub(crate) trait TrayComponent: CacheComponent {
type Context;
async fn refresh(&mut self, force: bool, ctx: &Self::Context) -> bool;
}
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;
}
#[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 TrayIconView<'a> {
pub(crate) last_icon_style: &'a IconStyle,
pub(crate) last_icon_bytes: &'a IconBytes,
}
#[derive(Default)]
pub(crate) struct TrayIcon(pub TrayIconCache);

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)?