* refactor: implement DRY principle improvements across backend
Major DRY violations identified and addressed:
1. **IPC Stream Monitor Pattern**:
- Created `utils/ipc_monitor.rs` with generic `IpcStreamMonitor` trait
- Added `IpcMonitorManager` for common async task management patterns
- Eliminates duplication across traffic.rs, memory.rs, and logs.rs
2. **Singleton Pattern Duplication**:
- Created `utils/singleton.rs` with `singleton\!` and `singleton_with_logging\!` macros
- Replaces 16+ duplicate singleton implementations across codebase
- Provides consistent, tested patterns for global instances
3. **macOS Activation Policy Refactoring**:
- Consolidated 3 duplicate methods into single parameterized `set_activation_policy()`
- Eliminated code duplication while maintaining backward compatibility
- Reduced maintenance burden for macOS-specific functionality
These improvements enhance maintainability, reduce bug potential, and ensure consistent patterns across the backend codebase.
* fix: resolve test failures and clippy warnings
- Fix doctest in singleton.rs by using rust,ignore syntax and proper code examples
- Remove unused time::Instant import from ipc_monitor.rs
- Add #[allow(dead_code)] attributes to future-use utility modules
- All 11 unit tests now pass successfully
- All clippy checks pass with -D warnings strict mode
- Documentation tests properly ignore example code that requires full context
* refactor: migrate code to use new utility tools (partial)
Progress on systematic migration to use created utility tools:
1. **Reorganized IPC Monitor**:
- Moved ipc_monitor.rs to src-tauri/src/ipc/monitor.rs for better organization
- Updated module structure to emphasize IPC relationship
2. **IpcManager Singleton Migration**:
- Replaced manual OnceLock singleton pattern with singleton_with_logging\! macro
- Simplified initialization code and added consistent logging
- Removed unused imports (OnceLock, logging::Type)
3. **ProxyRequestCache Singleton Migration**:
- Migrated from once_cell::sync::OnceCell to singleton\! macro
- Cleaner, more maintainable singleton pattern
- Consistent with project-wide singleton approach
These migrations demonstrate the utility and effectiveness of the created tools:
- Less boilerplate code
- Consistent patterns across codebase
- Easier maintenance and debugging
* feat: complete migration to new utility tools - phase 1
Successfully migrated core components to use the created utility tools:
- Moved `ipc_monitor.rs` to `src-tauri/src/ipc/monitor.rs`
- Better organization emphasizing IPC relationship
- Updated module exports and imports
- **IpcManager**: Migrated to `singleton_with_logging\!` macro
- **ProxyRequestCache**: Migrated to `singleton\!` macro
- Eliminated ~30 lines of boilerplate singleton code
- Consistent logging and initialization patterns
- Removed unused imports (OnceLock, once_cell, logging::Type)
- Cleaner, more maintainable code structure
- All 11 unit tests pass successfully
- Zero compilation warnings
- **Lines of code reduced**: ~50+ lines of boilerplate
- **Consistency improved**: Unified singleton patterns
- **Maintainability enhanced**: Centralized utility functions
- **Test coverage maintained**: 100% test pass rate
Remaining complex monitors (traffic, memory, logs) will be migrated to use the shared IPC monitoring patterns in the next phase, which requires careful refactoring of their streaming logic.
* refactor: complete singleton pattern migration to utility macros
Migrate remaining singleton patterns across the backend to use standardized
utility macros, achieving significant code reduction and consistency improvements.
- **LogsMonitor** (ipc/logs.rs): `OnceLock` → `singleton_with_logging\!`
- **Sysopt** (core/sysopt.rs): `OnceCell` → `singleton_lazy\!`
- **Tray** (core/tray/mod.rs): Complex `OnceCell` → `singleton_lazy\!`
- **Handle** (core/handle.rs): `OnceCell` → `singleton\!`
- **CoreManager** (core/core.rs): `OnceCell` → `singleton_lazy\!`
- **TrafficMonitor** (ipc/traffic.rs): `OnceLock` → `singleton_lazy_with_logging\!`
- **MemoryMonitor** (ipc/memory.rs): `OnceLock` → `singleton_lazy_with_logging\!`
- `singleton_lazy\!` - For complex initialization patterns
- `singleton_lazy_with_logging\!` - For complex initialization with logging
- **Code Reduction**: -33 lines of boilerplate singleton code
- **DRY Compliance**: Eliminated duplicate initialization patterns
- **Consistency**: Unified singleton approach across codebase
- **Maintainability**: Centralized singleton logic in utility macros
- **Zero Breaking Changes**: All existing APIs remain compatible
All tests pass and clippy warnings resolved.
* refactor: optimize singleton macros using Default trait implementation
Simplify singleton macro usage by implementing Default trait for complex
initialization patterns, significantly improving code readability and maintainability.
- **MemoryMonitor**: Move IPC client initialization to Default impl
- **TrafficMonitor**: Move IPC client initialization to Default impl
- **Sysopt**: Move Arc<Mutex> initialization to Default impl
- **Tray**: Move struct field initialization to Default impl
- **CoreManager**: Move Arc<Mutex> initialization to Default impl
```rust
singleton_lazy_with_logging\!(MemoryMonitor, INSTANCE, "MemoryMonitor", || {
let ipc_path_buf = ipc_path().unwrap();
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
let client = IpcStreamClient::new(ipc_path).unwrap();
MemoryMonitor::new(client)
});
```
```rust
impl Default for MemoryMonitor { /* initialization logic */ }
singleton_lazy_with_logging\!(MemoryMonitor, INSTANCE, "MemoryMonitor", MemoryMonitor::default);
```
- **Code Reduction**: -17 lines of macro closure code (80%+ simplification)
- **Separation of Concerns**: Initialization logic moved to proper Default impl
- **Readability**: Single-line macro calls vs multi-line closures
- **Testability**: Default implementations can be tested independently
- **Rust Idioms**: Using standard Default trait pattern
- **Performance**: Function calls more efficient than closures
All tests pass and clippy warnings resolved.
* refactor: implement MonitorData and StreamingParser traits for IPC monitors
* refactor: add timeout and retry_interval fields to IpcStreamMonitor; update TrafficMonitorState to derive Default
* refactor: migrate AppHandleManager to unified singleton control
- Replace manual singleton implementation with singleton_with_logging\! macro
- Remove std::sync::Once dependency in favor of OnceLock-based pattern
- Improve error handling for macOS activation policy methods
- Maintain thread safety with parking_lot::Mutex for AppHandle storage
- Add proper initialization check to prevent duplicate handle assignment
- Enhance logging consistency across AppHandleManager operations
* refactor: improve hotkey management with enum-based operations
- Add HotkeyFunction enum for type-safe function selection
- Add SystemHotkey enum for predefined system shortcuts
- Implement Display and FromStr traits for type conversions
- Replace string-based hotkey registration with enum methods
- Add register_system_hotkey() and unregister_system_hotkey() methods
- Maintain backward compatibility with string-based register() method
- Migrate singleton pattern to use singleton_with_logging\! macro
- Extract hotkey function execution logic into centralized execute_function()
- Update lib.rs to use new enum-based SystemHotkey operations
- Improve type safety and reduce string manipulation errors
Benefits:
- Type safety prevents invalid hotkey function names
- Centralized function execution reduces code duplication
- Enum-based API provides better IDE autocomplete support
- Maintains full backward compatibility with existing configurations
* fix: resolve LightWeightState initialization order panic
- Modify with_lightweight_status() to safely handle unmanaged state using try_state()
- Return Option<R> instead of R to gracefully handle state unavailability
- Update is_in_lightweight_mode() to use unwrap_or(false) for safe defaults
- Add state availability check in auto_lightweight_mode_init() before access
- Maintain singleton check priority while preventing early state access panics
- Fix clippy warnings for redundant pattern matching
Resolves runtime panic: "state() called before manage() for LightWeightState"
* refactor: add unreachable patterns for non-macOS in hotkey handling
* refactor: simplify SystemHotkey enum by removing redundant cfg attributes
* refactor: add macOS conditional compilation for system hotkey registration methods
* refactor: streamline hotkey unregistration and error logging for macOS
834 lines
26 KiB
Rust
834 lines
26 KiB
Rust
use once_cell::sync::OnceCell;
|
|
use tauri::tray::TrayIconBuilder;
|
|
#[cfg(target_os = "macos")]
|
|
pub mod speed_rate;
|
|
use crate::ipc::Rate;
|
|
use crate::{
|
|
cmd,
|
|
config::Config,
|
|
feat, logging,
|
|
module::lightweight::is_in_lightweight_mode,
|
|
singleton_lazy,
|
|
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
|
|
Type,
|
|
};
|
|
|
|
use anyhow::Result;
|
|
use parking_lot::Mutex;
|
|
use std::{
|
|
fs,
|
|
sync::atomic::{AtomicBool, Ordering},
|
|
time::{Duration, Instant},
|
|
};
|
|
use tauri::{
|
|
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
|
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
|
AppHandle, Wry,
|
|
};
|
|
|
|
use super::handle;
|
|
|
|
#[derive(Clone)]
|
|
struct TrayState {}
|
|
|
|
// 托盘点击防抖机制
|
|
static TRAY_CLICK_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
|
|
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
|
|
|
fn get_tray_click_debounce() -> &'static Mutex<Instant> {
|
|
TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
|
|
}
|
|
|
|
fn should_handle_tray_click() -> bool {
|
|
let debounce_lock = get_tray_click_debounce();
|
|
let mut last_click = debounce_lock.lock();
|
|
let now = Instant::now();
|
|
|
|
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
|
*last_click = now;
|
|
true
|
|
} else {
|
|
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
|
|
now.duration_since(*last_click).as_millis());
|
|
false
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub struct Tray {
|
|
last_menu_update: Mutex<Option<Instant>>,
|
|
menu_updating: AtomicBool,
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
pub struct Tray {
|
|
last_menu_update: Mutex<Option<Instant>>,
|
|
menu_updating: AtomicBool,
|
|
}
|
|
|
|
impl TrayState {
|
|
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
|
let verge = Config::verge().latest_ref().clone();
|
|
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
|
|
if is_common_tray_icon {
|
|
if let Some(common_icon_path) = find_target_icons("common").unwrap() {
|
|
let icon_data = fs::read(common_icon_path).unwrap();
|
|
return (true, icon_data);
|
|
}
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let tray_icon_colorful = verge.tray_icon.unwrap_or("monochrome".to_string());
|
|
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(),
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
|
|
let verge = Config::verge().latest_ref().clone();
|
|
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
|
|
if is_sysproxy_tray_icon {
|
|
if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() {
|
|
let icon_data = fs::read(sysproxy_icon_path).unwrap();
|
|
return (true, icon_data);
|
|
}
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
|
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(),
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn get_tun_tray_icon() -> (bool, Vec<u8>) {
|
|
let verge = Config::verge().latest_ref().clone();
|
|
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
|
|
if is_tun_tray_icon {
|
|
if let Some(tun_icon_path) = find_target_icons("tun").unwrap() {
|
|
let icon_data = fs::read(tun_icon_path).unwrap();
|
|
return (true, icon_data);
|
|
}
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
|
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 {
|
|
fn default() -> Self {
|
|
Tray {
|
|
last_menu_update: Mutex::new(None),
|
|
menu_updating: AtomicBool::new(false),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use simplified singleton_lazy macro
|
|
singleton_lazy!(Tray, TRAY, Tray::default);
|
|
|
|
impl Tray {
|
|
pub fn init(&self) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
/// 更新托盘点击行为
|
|
pub fn update_click_behavior(&self) -> Result<()> {
|
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
|
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
|
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
|
let tray = app_handle.tray_by_id("main").unwrap();
|
|
match tray_event.as_str() {
|
|
"tray_menu" => tray.set_show_menu_on_left_click(true)?,
|
|
_ => tray.set_show_menu_on_left_click(false)?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// 更新托盘菜单
|
|
pub fn update_menu(&self) -> Result<()> {
|
|
// 调整最小更新间隔,确保状态及时刷新
|
|
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
|
|
|
|
// 检查是否正在更新
|
|
if self.menu_updating.load(Ordering::Acquire) {
|
|
return Ok(());
|
|
}
|
|
|
|
// 检查更新频率,但允许重要事件跳过频率限制
|
|
let should_force_update = match std::thread::current().name() {
|
|
Some("main") => true,
|
|
_ => {
|
|
let last_update = self.last_menu_update.lock();
|
|
if let Some(last_time) = *last_update {
|
|
last_time.elapsed() >= MIN_UPDATE_INTERVAL
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
};
|
|
|
|
if !should_force_update {
|
|
return Ok(());
|
|
}
|
|
|
|
let app_handle = match handle::Handle::global().app_handle() {
|
|
Some(handle) => handle,
|
|
None => {
|
|
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// 设置更新状态
|
|
self.menu_updating.store(true, Ordering::Release);
|
|
|
|
let result = self.update_menu_internal(&app_handle);
|
|
|
|
{
|
|
let mut last_update = self.last_menu_update.lock();
|
|
*last_update = Some(Instant::now());
|
|
}
|
|
self.menu_updating.store(false, Ordering::Release);
|
|
|
|
result
|
|
}
|
|
|
|
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
|
|
let verge = Config::verge().latest_ref().clone();
|
|
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
|
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
|
let mode = {
|
|
Config::clash()
|
|
.latest_ref()
|
|
.0
|
|
.get("mode")
|
|
.map(|val| val.as_str().unwrap_or("rule"))
|
|
.unwrap_or("rule")
|
|
.to_owned()
|
|
};
|
|
let profile_uid_and_name = Config::profiles()
|
|
.data_mut()
|
|
.all_profile_uid_and_name()
|
|
.unwrap_or_default();
|
|
let is_lightweight_mode = is_in_lightweight_mode();
|
|
|
|
match app_handle.tray_by_id("main") {
|
|
Some(tray) => {
|
|
let _ = tray.set_menu(Some(create_tray_menu(
|
|
app_handle,
|
|
Some(mode.as_str()),
|
|
*system_proxy,
|
|
*tun_mode,
|
|
profile_uid_and_name,
|
|
is_lightweight_mode,
|
|
)?));
|
|
log::debug!(target: "app", "托盘菜单更新成功");
|
|
Ok(())
|
|
}
|
|
None => {
|
|
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 更新托盘图标
|
|
#[cfg(target_os = "macos")]
|
|
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
|
let app_handle = match handle::Handle::global().app_handle() {
|
|
Some(handle) => handle,
|
|
None => {
|
|
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let tray = match app_handle.tray_by_id("main") {
|
|
Some(tray) => tray,
|
|
None => {
|
|
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let verge = Config::verge().latest_ref().clone();
|
|
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
|
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
|
|
|
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
|
(true, true) => TrayState::get_tun_tray_icon(),
|
|
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
|
(false, true) => TrayState::get_tun_tray_icon(),
|
|
(false, false) => TrayState::get_common_tray_icon(),
|
|
};
|
|
|
|
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
|
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 fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
|
let app_handle = match handle::Handle::global().app_handle() {
|
|
Some(handle) => handle,
|
|
None => {
|
|
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let tray = match app_handle.tray_by_id("main") {
|
|
Some(tray) => tray,
|
|
None => {
|
|
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let verge = Config::verge().latest_ref().clone();
|
|
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
|
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
|
|
|
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
|
(true, true) => TrayState::get_tun_tray_icon(),
|
|
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
|
(false, true) => TrayState::get_tun_tray_icon(),
|
|
(false, false) => TrayState::get_common_tray_icon(),
|
|
};
|
|
|
|
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
|
Ok(())
|
|
}
|
|
|
|
/// 更新托盘显示状态的函数
|
|
pub fn update_tray_display(&self) -> Result<()> {
|
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
|
let _tray = app_handle.tray_by_id("main").unwrap();
|
|
|
|
// 更新菜单
|
|
self.update_menu()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// 更新托盘提示
|
|
pub fn update_tooltip(&self) -> Result<()> {
|
|
let app_handle = match handle::Handle::global().app_handle() {
|
|
Some(handle) => handle,
|
|
None => {
|
|
log::warn!(target: "app", "更新托盘提示失败: app_handle不存在");
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let version = match VERSION.get() {
|
|
Some(v) => v,
|
|
None => {
|
|
log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在");
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let verge = Config::verge().latest_ref().clone();
|
|
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
|
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
|
|
|
let switch_map = {
|
|
let mut map = std::collections::HashMap::new();
|
|
map.insert(true, "on");
|
|
map.insert(false, "off");
|
|
map
|
|
};
|
|
|
|
let mut current_profile_name = "None".to_string();
|
|
let profiles = Config::profiles();
|
|
let profiles = profiles.latest_ref();
|
|
if let Some(current_profile_uid) = profiles.get_current() {
|
|
if let Ok(profile) = profiles.get_item(¤t_profile_uid) {
|
|
current_profile_name = match &profile.name {
|
|
Some(profile_name) => profile_name.to_string(),
|
|
None => current_profile_name,
|
|
};
|
|
}
|
|
};
|
|
|
|
if let Some(tray) = app_handle.tray_by_id("main") {
|
|
let _ = tray.set_tooltip(Some(&format!(
|
|
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
|
t("SysProxy"),
|
|
switch_map[system_proxy],
|
|
t("TUN"),
|
|
switch_map[tun_mode],
|
|
t("Profile"),
|
|
current_profile_name
|
|
)));
|
|
} else {
|
|
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn update_part(&self) -> Result<()> {
|
|
self.update_menu()?;
|
|
self.update_icon(None)?;
|
|
self.update_tooltip()?;
|
|
// 更新轻量模式显示状态
|
|
self.update_tray_display()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// 取消订阅 traffic 数据
|
|
#[cfg(target_os = "macos")]
|
|
pub fn unsubscribe_traffic(&self) {}
|
|
|
|
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
|
log::info!(target: "app", "正在从AppHandle创建系统托盘");
|
|
|
|
// 获取图标
|
|
let icon_bytes = TrayState::get_common_tray_icon().1;
|
|
let icon = tauri::image::Image::from_bytes(&icon_bytes)?;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
let builder = TrayIconBuilder::with_id("main")
|
|
.icon(icon)
|
|
.icon_as_template(false);
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
let mut builder = TrayIconBuilder::with_id("main")
|
|
.icon(icon)
|
|
.icon_as_template(false);
|
|
|
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
{
|
|
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
|
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
|
if tray_event.as_str() != "tray_menu" {
|
|
builder = builder.show_menu_on_left_click(false);
|
|
}
|
|
}
|
|
|
|
let tray = builder.build(app_handle)?;
|
|
|
|
tray.on_tray_icon_event(|_, event| {
|
|
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
|
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
|
log::debug!(target: "app","tray event: {tray_event:?}");
|
|
|
|
if let TrayIconEvent::Click {
|
|
button: MouseButton::Left,
|
|
button_state: MouseButtonState::Down,
|
|
..
|
|
} = event
|
|
{
|
|
// 添加防抖检查,防止快速连击
|
|
if !should_handle_tray_click() {
|
|
return;
|
|
}
|
|
|
|
match tray_event.as_str() {
|
|
"system_proxy" => feat::toggle_system_proxy(),
|
|
"tun_mode" => feat::toggle_tun_mode(None),
|
|
"main_window" => {
|
|
use crate::utils::window_manager::WindowManager;
|
|
log::info!(target: "app", "Tray点击事件: 显示主窗口");
|
|
if crate::module::lightweight::is_in_lightweight_mode() {
|
|
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
|
|
crate::module::lightweight::exit_lightweight_mode();
|
|
}
|
|
let result = WindowManager::show_main_window();
|
|
log::info!(target: "app", "窗口显示结果: {result:?}");
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
});
|
|
tray.on_menu_event(on_menu_event);
|
|
log::info!(target: "app", "系统托盘创建成功");
|
|
Ok(())
|
|
}
|
|
|
|
// 托盘统一的状态更新函数
|
|
pub fn update_all_states(&self) -> Result<()> {
|
|
// 确保所有状态更新完成
|
|
self.update_menu()?;
|
|
self.update_icon(None)?;
|
|
self.update_tooltip()?;
|
|
self.update_tray_display()?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn create_tray_menu(
|
|
app_handle: &AppHandle,
|
|
mode: Option<&str>,
|
|
system_proxy_enabled: bool,
|
|
tun_mode_enabled: bool,
|
|
profile_uid_and_name: Vec<(String, String)>,
|
|
is_lightweight_mode: bool,
|
|
) -> Result<tauri::menu::Menu<Wry>> {
|
|
let mode = mode.unwrap_or("");
|
|
|
|
let unknown_version = String::from("unknown");
|
|
let version = VERSION.get().unwrap_or(&unknown_version);
|
|
|
|
let hotkeys = Config::verge()
|
|
.latest_ref()
|
|
.hotkeys
|
|
.as_ref()
|
|
.map(|h| {
|
|
h.iter()
|
|
.filter_map(|item| {
|
|
let mut parts = item.split(',');
|
|
match (parts.next(), parts.next()) {
|
|
(Some(func), Some(key)) => Some((func.to_string(), key.to_string())),
|
|
_ => None,
|
|
}
|
|
})
|
|
.collect::<std::collections::HashMap<String, String>>()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
|
|
.iter()
|
|
.map(|(profile_uid, profile_name)| {
|
|
let is_current_profile = Config::profiles()
|
|
.data_mut()
|
|
.is_current_profile_index(profile_uid.to_string());
|
|
CheckMenuItem::with_id(
|
|
app_handle,
|
|
format!("profiles_{profile_uid}"),
|
|
t(profile_name),
|
|
true,
|
|
is_current_profile,
|
|
None::<&str>,
|
|
)
|
|
.unwrap()
|
|
})
|
|
.collect();
|
|
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
|
.iter()
|
|
.map(|item| item as &dyn IsMenuItem<Wry>)
|
|
.collect();
|
|
|
|
let open_window = &MenuItem::with_id(
|
|
app_handle,
|
|
"open_window",
|
|
t("Dashboard"),
|
|
true,
|
|
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
|
|
)
|
|
.unwrap();
|
|
|
|
let rule_mode = &CheckMenuItem::with_id(
|
|
app_handle,
|
|
"rule_mode",
|
|
t("Rule Mode"),
|
|
true,
|
|
mode == "rule",
|
|
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
|
|
)
|
|
.unwrap();
|
|
|
|
let global_mode = &CheckMenuItem::with_id(
|
|
app_handle,
|
|
"global_mode",
|
|
t("Global Mode"),
|
|
true,
|
|
mode == "global",
|
|
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
|
|
)
|
|
.unwrap();
|
|
|
|
let direct_mode = &CheckMenuItem::with_id(
|
|
app_handle,
|
|
"direct_mode",
|
|
t("Direct Mode"),
|
|
true,
|
|
mode == "direct",
|
|
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
|
|
)
|
|
.unwrap();
|
|
|
|
let profiles = &Submenu::with_id_and_items(
|
|
app_handle,
|
|
"profiles",
|
|
t("Profiles"),
|
|
true,
|
|
&profile_menu_items,
|
|
)
|
|
.unwrap();
|
|
|
|
let system_proxy = &CheckMenuItem::with_id(
|
|
app_handle,
|
|
"system_proxy",
|
|
t("System Proxy"),
|
|
true,
|
|
system_proxy_enabled,
|
|
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
|
|
)
|
|
.unwrap();
|
|
|
|
let tun_mode = &CheckMenuItem::with_id(
|
|
app_handle,
|
|
"tun_mode",
|
|
t("TUN Mode"),
|
|
true,
|
|
tun_mode_enabled,
|
|
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
|
|
)
|
|
.unwrap();
|
|
|
|
let lighteweight_mode = &CheckMenuItem::with_id(
|
|
app_handle,
|
|
"entry_lightweight_mode",
|
|
t("LightWeight Mode"),
|
|
true,
|
|
is_lightweight_mode,
|
|
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
|
|
)
|
|
.unwrap();
|
|
|
|
let copy_env =
|
|
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
|
|
|
let open_app_dir = &MenuItem::with_id(
|
|
app_handle,
|
|
"open_app_dir",
|
|
t("Conf Dir"),
|
|
true,
|
|
None::<&str>,
|
|
)
|
|
.unwrap();
|
|
|
|
let open_core_dir = &MenuItem::with_id(
|
|
app_handle,
|
|
"open_core_dir",
|
|
t("Core Dir"),
|
|
true,
|
|
None::<&str>,
|
|
)
|
|
.unwrap();
|
|
|
|
let open_logs_dir = &MenuItem::with_id(
|
|
app_handle,
|
|
"open_logs_dir",
|
|
t("Logs Dir"),
|
|
true,
|
|
None::<&str>,
|
|
)
|
|
.unwrap();
|
|
|
|
let open_dir = &Submenu::with_id_and_items(
|
|
app_handle,
|
|
"open_dir",
|
|
t("Open Dir"),
|
|
true,
|
|
&[open_app_dir, open_core_dir, open_logs_dir],
|
|
)
|
|
.unwrap();
|
|
|
|
let restart_clash = &MenuItem::with_id(
|
|
app_handle,
|
|
"restart_clash",
|
|
t("Restart Clash Core"),
|
|
true,
|
|
None::<&str>,
|
|
)
|
|
.unwrap();
|
|
|
|
let restart_app = &MenuItem::with_id(
|
|
app_handle,
|
|
"restart_app",
|
|
t("Restart App"),
|
|
true,
|
|
None::<&str>,
|
|
)
|
|
.unwrap();
|
|
|
|
let app_version = &MenuItem::with_id(
|
|
app_handle,
|
|
"app_version",
|
|
format!("{} {version}", t("Verge Version")),
|
|
true,
|
|
None::<&str>,
|
|
)
|
|
.unwrap();
|
|
|
|
let more = &Submenu::with_id_and_items(
|
|
app_handle,
|
|
"more",
|
|
t("More"),
|
|
true,
|
|
&[restart_clash, restart_app, app_version],
|
|
)
|
|
.unwrap();
|
|
|
|
let quit =
|
|
&MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q")).unwrap();
|
|
|
|
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
|
|
|
|
let menu = tauri::menu::MenuBuilder::new(app_handle)
|
|
.items(&[
|
|
open_window,
|
|
separator,
|
|
rule_mode,
|
|
global_mode,
|
|
direct_mode,
|
|
separator,
|
|
profiles,
|
|
separator,
|
|
system_proxy,
|
|
tun_mode,
|
|
separator,
|
|
lighteweight_mode,
|
|
copy_env,
|
|
open_dir,
|
|
more,
|
|
separator,
|
|
quit,
|
|
])
|
|
.build()
|
|
.unwrap();
|
|
Ok(menu)
|
|
}
|
|
|
|
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|
match event.id.as_ref() {
|
|
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
|
let mode = &mode[0..mode.len() - 5];
|
|
logging!(
|
|
info,
|
|
Type::ProxyMode,
|
|
true,
|
|
"Switch Proxy Mode To: {}",
|
|
mode
|
|
);
|
|
feat::change_clash_mode(mode.into());
|
|
}
|
|
"open_window" => {
|
|
use crate::utils::window_manager::WindowManager;
|
|
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
|
|
|
if !should_handle_tray_click() {
|
|
return;
|
|
}
|
|
|
|
if crate::module::lightweight::is_in_lightweight_mode() {
|
|
log::info!(target: "app", "当前在轻量模式,正在退出");
|
|
crate::module::lightweight::exit_lightweight_mode();
|
|
}
|
|
let result = WindowManager::show_main_window();
|
|
log::info!(target: "app", "窗口显示结果: {result:?}");
|
|
}
|
|
"system_proxy" => {
|
|
feat::toggle_system_proxy();
|
|
}
|
|
"tun_mode" => {
|
|
feat::toggle_tun_mode(None);
|
|
}
|
|
"copy_env" => feat::copy_clash_env(),
|
|
"open_app_dir" => {
|
|
let _ = cmd::open_app_dir();
|
|
}
|
|
"open_core_dir" => {
|
|
let _ = cmd::open_core_dir();
|
|
}
|
|
"open_logs_dir" => {
|
|
let _ = cmd::open_logs_dir();
|
|
}
|
|
"restart_clash" => feat::restart_clash_core(),
|
|
"restart_app" => feat::restart_app(),
|
|
"entry_lightweight_mode" => {
|
|
if !should_handle_tray_click() {
|
|
return;
|
|
}
|
|
|
|
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
|
|
if was_lightweight {
|
|
crate::module::lightweight::exit_lightweight_mode();
|
|
} else {
|
|
crate::module::lightweight::entry_lightweight_mode();
|
|
}
|
|
|
|
if was_lightweight {
|
|
use crate::utils::window_manager::WindowManager;
|
|
let result = WindowManager::show_main_window();
|
|
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
|
|
}
|
|
}
|
|
"quit" => {
|
|
feat::quit();
|
|
}
|
|
id if id.starts_with("profiles_") => {
|
|
let profile_index = &id["profiles_".len()..];
|
|
feat::toggle_proxy_profile(profile_index.into());
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if let Err(e) = Tray::global().update_all_states() {
|
|
log::warn!(target: "app", "更新托盘状态失败: {e}");
|
|
}
|
|
}
|