mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-26 14:12:10 +08:00
refactor: invock mihomo api by use tauri-plugin-mihomo
This commit is contained in:
+3
-2
@@ -11,7 +11,7 @@
|
||||
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsc --noEmit && vite build",
|
||||
"web:build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tsc --noEmit && vite build",
|
||||
"web:serve": "vite preview",
|
||||
"prebuild": "node scripts/prebuild.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
@@ -71,7 +71,8 @@
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"swr": "^2.3.6",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.8"
|
||||
"zustand": "^5.0.8",
|
||||
"tauri-plugin-mihomo-api": "file:crates/tauri-plugin-mihomo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
|
||||
Generated
+10
@@ -128,6 +128,9 @@ importers:
|
||||
swr:
|
||||
specifier: ^2.3.6
|
||||
version: 2.3.6(react@19.1.1)
|
||||
tauri-plugin-mihomo-api:
|
||||
specifier: file:crates/tauri-plugin-mihomo
|
||||
version: file:crates/tauri-plugin-mihomo
|
||||
types-pac:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
@@ -3786,6 +3789,9 @@ packages:
|
||||
resolution: {integrity: sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tauri-plugin-mihomo-api@file:crates/tauri-plugin-mihomo:
|
||||
resolution: {directory: crates/tauri-plugin-mihomo, type: directory}
|
||||
|
||||
terser@5.44.0:
|
||||
resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8244,6 +8250,10 @@ snapshots:
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
tauri-plugin-mihomo-api@file:crates/tauri-plugin-mihomo:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.8.0
|
||||
|
||||
terser@5.44.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.6
|
||||
|
||||
Generated
+97
@@ -1147,6 +1147,7 @@ dependencies = [
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-mihomo",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
@@ -1654,6 +1655,12 @@ dependencies = [
|
||||
"parking_lot_core 0.9.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.2"
|
||||
@@ -7303,6 +7310,30 @@ dependencies = [
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-mihomo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"log",
|
||||
"pin-project",
|
||||
"rand 0.9.2",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.16",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"ts-rs",
|
||||
"urlencoding",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.3.1"
|
||||
@@ -7537,6 +7568,15 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminfo"
|
||||
version = "0.7.5"
|
||||
@@ -7821,6 +7861,18 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.16"
|
||||
@@ -8215,6 +8267,45 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "11.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be"
|
||||
dependencies = [
|
||||
"thiserror 2.0.16",
|
||||
"ts-rs-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "11.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"thiserror 2.0.16",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
@@ -8337,6 +8428,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.3.0"
|
||||
|
||||
@@ -86,6 +86,7 @@ cfg-if = "1.0.3"
|
||||
nu-ansi-term = { version = "0.50.1", optional = true }
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
tauri-plugin-devtools = { version = "2.0.1" }
|
||||
tauri-plugin-mihomo = {path = "../crates/tauri-plugin-mihomo"}
|
||||
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }]
|
||||
}
|
||||
},
|
||||
"mihomo:default"
|
||||
]
|
||||
}
|
||||
|
||||
+321
-316
@@ -1,21 +1,15 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
cache::CacheProxy,
|
||||
config::Config,
|
||||
core::{CoreManager, handle},
|
||||
};
|
||||
use crate::{
|
||||
config::*,
|
||||
feat,
|
||||
ipc::{self, IpcManager},
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
wrap_err,
|
||||
core::{self, CoreManager, RunningMode, handle, logger},
|
||||
};
|
||||
use crate::{config::*, feat, logging, utils::logging::Type, wrap_err};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use std::time::Duration;
|
||||
// use std::time::Duration;
|
||||
|
||||
const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
// const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
/// 复制Clash环境变量
|
||||
#[tauri::command]
|
||||
@@ -113,18 +107,18 @@ pub async fn restart_core() -> CmdResult {
|
||||
}
|
||||
|
||||
/// 获取代理延迟
|
||||
#[tauri::command]
|
||||
pub async fn clash_api_get_proxy_delay(
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(
|
||||
IpcManager::global()
|
||||
.test_proxy_delay(&name, url, timeout)
|
||||
.await
|
||||
)
|
||||
}
|
||||
// #[tauri::command]
|
||||
// pub async fn clash_api_get_proxy_delay(
|
||||
// name: String,
|
||||
// url: Option<String>,
|
||||
// timeout: i32,
|
||||
// ) -> CmdResult<serde_json::Value> {
|
||||
// wrap_err!(
|
||||
// IpcManager::global()
|
||||
// .test_proxy_delay(&name, url, timeout)
|
||||
// .await
|
||||
// )
|
||||
// }
|
||||
|
||||
/// 测试URL延迟
|
||||
#[tauri::command]
|
||||
@@ -307,317 +301,328 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取Clash版本信息
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_version().await)
|
||||
}
|
||||
// 获取Clash版本信息
|
||||
// #[tauri::command]
|
||||
// pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
|
||||
// wrap_err!(IpcManager::global().get_version().await)
|
||||
// }
|
||||
|
||||
/// 获取Clash配置
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
|
||||
let manager = IpcManager::global();
|
||||
let cache = CacheProxy::global();
|
||||
let key = CacheProxy::make_key("clash_config", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
|
||||
manager.get_config().await.unwrap_or_else(|e| {
|
||||
logging!(error, Type::Cmd, "Failed to fetch clash config: {e}");
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
})
|
||||
})
|
||||
.await;
|
||||
Ok((*value).clone())
|
||||
}
|
||||
// 获取Clash配置
|
||||
// #[tauri::command]
|
||||
// pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
|
||||
// let manager = IpcManager::global();
|
||||
// let cache = CacheProxy::global();
|
||||
// let key = CacheProxy::make_key("clash_config", "default");
|
||||
// let value = cache
|
||||
// .get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
|
||||
// manager.get_config().await.unwrap_or_else(|e| {
|
||||
// logging!(error, Type::Cmd, "Failed to fetch clash config: {e}");
|
||||
// serde_json::Value::Object(serde_json::Map::new())
|
||||
// })
|
||||
// })
|
||||
// .await;
|
||||
// Ok((*value).clone())
|
||||
// }
|
||||
|
||||
/// 强制刷新Clash配置缓存
|
||||
#[tauri::command]
|
||||
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
|
||||
let cache = CacheProxy::global();
|
||||
let key = CacheProxy::make_key("clash_config", "default");
|
||||
cache.map.remove(&key);
|
||||
get_clash_config().await
|
||||
}
|
||||
// 强制刷新Clash配置缓存
|
||||
// #[tauri::command]
|
||||
// pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
|
||||
// let cache = CacheProxy::global();
|
||||
// let key = CacheProxy::make_key("clash_config", "default");
|
||||
// cache.map.remove(&key);
|
||||
// get_clash_config().await
|
||||
// }
|
||||
|
||||
/// 更新地理数据
|
||||
#[tauri::command]
|
||||
pub async fn update_geo_data() -> CmdResult {
|
||||
wrap_err!(IpcManager::global().update_geo_data().await)
|
||||
}
|
||||
// 更新地理数据
|
||||
// #[tauri::command]
|
||||
// pub async fn update_geo_data() -> CmdResult {
|
||||
// wrap_err!(IpcManager::global().update_geo_data().await)
|
||||
// }
|
||||
|
||||
/// 升级Clash核心
|
||||
#[tauri::command]
|
||||
pub async fn upgrade_clash_core() -> CmdResult {
|
||||
wrap_err!(IpcManager::global().upgrade_core().await)
|
||||
}
|
||||
// 升级Clash核心
|
||||
// #[tauri::command]
|
||||
// pub async fn upgrade_clash_core() -> CmdResult {
|
||||
// wrap_err!(IpcManager::global().upgrade_core().await)
|
||||
// }
|
||||
|
||||
/// 获取规则
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_rules() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_rules().await)
|
||||
}
|
||||
// 获取规则
|
||||
// #[tauri::command]
|
||||
// pub async fn get_clash_rules() -> CmdResult<serde_json::Value> {
|
||||
// wrap_err!(IpcManager::global().get_rules().await)
|
||||
// }
|
||||
|
||||
/// 更新代理选择
|
||||
#[tauri::command]
|
||||
pub async fn update_proxy_choice(group: String, proxy: String) -> CmdResult {
|
||||
wrap_err!(IpcManager::global().update_proxy(&group, &proxy).await)
|
||||
}
|
||||
// 更新代理选择
|
||||
// #[tauri::command]
|
||||
// pub async fn update_proxy_choice(group: String, proxy: String) -> CmdResult {
|
||||
// wrap_err!(IpcManager::global().update_proxy(&group, &proxy).await)
|
||||
// }
|
||||
|
||||
/// 获取代理提供者
|
||||
#[tauri::command]
|
||||
pub async fn get_proxy_providers() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_providers_proxies().await)
|
||||
}
|
||||
// 获取代理提供者
|
||||
// #[tauri::command]
|
||||
// pub async fn get_proxy_providers() -> CmdResult<serde_json::Value> {
|
||||
// wrap_err!(IpcManager::global().get_providers_proxies().await)
|
||||
// }
|
||||
|
||||
/// 获取规则提供者
|
||||
#[tauri::command]
|
||||
pub async fn get_rule_providers() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_rule_providers().await)
|
||||
}
|
||||
// 获取规则提供者
|
||||
// #[tauri::command]
|
||||
// pub async fn get_rule_providers() -> CmdResult<serde_json::Value> {
|
||||
// wrap_err!(IpcManager::global().get_rule_providers().await)
|
||||
// }
|
||||
|
||||
/// 代理提供者健康检查
|
||||
#[tauri::command]
|
||||
pub async fn proxy_provider_health_check(name: String) -> CmdResult {
|
||||
wrap_err!(
|
||||
IpcManager::global()
|
||||
.proxy_provider_health_check(&name)
|
||||
.await
|
||||
)
|
||||
}
|
||||
// 代理提供者健康检查
|
||||
// #[tauri::command]
|
||||
// pub async fn proxy_provider_health_check(name: String) -> CmdResult {
|
||||
// wrap_err!(
|
||||
// IpcManager::global()
|
||||
// .proxy_provider_health_check(&name)
|
||||
// .await
|
||||
// )
|
||||
// }
|
||||
|
||||
/// 更新代理提供者
|
||||
#[tauri::command]
|
||||
pub async fn update_proxy_provider(name: String) -> CmdResult {
|
||||
wrap_err!(IpcManager::global().update_proxy_provider(&name).await)
|
||||
}
|
||||
// 更新代理提供者
|
||||
// #[tauri::command]
|
||||
// pub async fn update_proxy_provider(name: String) -> CmdResult {
|
||||
// wrap_err!(IpcManager::global().update_proxy_provider(&name).await)
|
||||
// }
|
||||
|
||||
/// 更新规则提供者
|
||||
#[tauri::command]
|
||||
pub async fn update_rule_provider(name: String) -> CmdResult {
|
||||
wrap_err!(IpcManager::global().update_rule_provider(&name).await)
|
||||
}
|
||||
// 更新规则提供者
|
||||
// #[tauri::command]
|
||||
// pub async fn update_rule_provider(name: String) -> CmdResult {
|
||||
// wrap_err!(IpcManager::global().update_rule_provider(&name).await)
|
||||
// }
|
||||
|
||||
/// 获取连接
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_connections() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_connections().await)
|
||||
}
|
||||
// 获取连接
|
||||
// #[tauri::command]
|
||||
// pub async fn get_clash_connections() -> CmdResult<serde_json::Value> {
|
||||
// wrap_err!(IpcManager::global().get_connections().await)
|
||||
// }
|
||||
|
||||
/// 删除连接
|
||||
#[tauri::command]
|
||||
pub async fn delete_clash_connection(id: String) -> CmdResult {
|
||||
wrap_err!(IpcManager::global().delete_connection(&id).await)
|
||||
}
|
||||
// 删除连接
|
||||
// #[tauri::command]
|
||||
// pub async fn delete_clash_connection(id: String) -> CmdResult {
|
||||
// wrap_err!(IpcManager::global().delete_connection(&id).await)
|
||||
// }
|
||||
|
||||
/// 关闭所有连接
|
||||
#[tauri::command]
|
||||
pub async fn close_all_clash_connections() -> CmdResult {
|
||||
wrap_err!(IpcManager::global().close_all_connections().await)
|
||||
}
|
||||
// 关闭所有连接
|
||||
// #[tauri::command]
|
||||
// pub async fn close_all_clash_connections() -> CmdResult {
|
||||
// wrap_err!(IpcManager::global().close_all_connections().await)
|
||||
// }
|
||||
|
||||
/// 获取流量数据 (使用新的IPC流式监控)
|
||||
#[tauri::command]
|
||||
pub async fn get_traffic_data() -> CmdResult<serde_json::Value> {
|
||||
let traffic = crate::ipc::get_current_traffic().await;
|
||||
let result = serde_json::json!({
|
||||
"up": traffic.total_up,
|
||||
"down": traffic.total_down,
|
||||
"up_rate": traffic.up_rate,
|
||||
"down_rate": traffic.down_rate,
|
||||
"last_updated": traffic.last_updated.elapsed().as_secs()
|
||||
});
|
||||
Ok(result)
|
||||
}
|
||||
// 获取流量数据 (使用新的IPC流式监控)
|
||||
// #[tauri::command]
|
||||
// pub async fn get_traffic_data() -> CmdResult<serde_json::Value> {
|
||||
// let traffic = crate::ipc::get_current_traffic().await;
|
||||
// let result = serde_json::json!({
|
||||
// "up": traffic.total_up,
|
||||
// "down": traffic.total_down,
|
||||
// "up_rate": traffic.up_rate,
|
||||
// "down_rate": traffic.down_rate,
|
||||
// "last_updated": traffic.last_updated.elapsed().as_secs()
|
||||
// });
|
||||
// Ok(result)
|
||||
// }
|
||||
|
||||
// 获取内存数据 (使用新的IPC流式监控)
|
||||
// #[tauri::command]
|
||||
// pub async fn get_memory_data() -> CmdResult<serde_json::Value> {
|
||||
// let memory = crate::ipc::get_current_memory().await;
|
||||
// let usage_percent = if memory.oslimit > 0 {
|
||||
// (memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||
// } else {
|
||||
// 0.0
|
||||
// };
|
||||
// let result = serde_json::json!({
|
||||
// "inuse": memory.inuse,
|
||||
// "oslimit": memory.oslimit,
|
||||
// "usage_percent": usage_percent,
|
||||
// "last_updated": memory.last_updated.elapsed().as_secs()
|
||||
// });
|
||||
// Ok(result)
|
||||
// }
|
||||
|
||||
// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留)
|
||||
// #[tauri::command]
|
||||
// pub async fn start_traffic_service() -> CmdResult {
|
||||
// logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)");
|
||||
// // 新的IPC监控在首次访问时自动启动
|
||||
// // 触发一次访问以确保监控器已初始化
|
||||
// let _ = crate::ipc::get_current_traffic().await;
|
||||
// let _ = crate::ipc::get_current_memory().await;
|
||||
// logging!(info, Type::Ipc, "IPC流式监控已激活");
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留)
|
||||
// #[tauri::command]
|
||||
// pub async fn stop_traffic_service() -> CmdResult {
|
||||
// logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)");
|
||||
// // 新的IPC监控是持久的,无需显式停止
|
||||
// logging!(info, Type::Ipc, "IPC流式监控继续运行");
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
// 获取格式化的流量数据 (包含单位,便于前端显示)
|
||||
// #[tauri::command]
|
||||
// pub async fn get_formatted_traffic_data() -> CmdResult<serde_json::Value> {
|
||||
// logging!(trace, Type::Ipc, "获取格式化流量数据");
|
||||
// let (up_rate, down_rate, total_up, total_down, is_fresh) =
|
||||
// crate::ipc::get_formatted_traffic().await;
|
||||
// let result = serde_json::json!({
|
||||
// "up_rate_formatted": up_rate,
|
||||
// "down_rate_formatted": down_rate,
|
||||
// "total_up_formatted": total_up,
|
||||
// "total_down_formatted": total_down,
|
||||
// "is_fresh": is_fresh
|
||||
// });
|
||||
// logging!(
|
||||
// debug,
|
||||
// Type::Ipc,
|
||||
// "格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"
|
||||
// );
|
||||
// Ok(result)
|
||||
// }
|
||||
|
||||
// 获取格式化的内存数据 (包含单位,便于前端显示)
|
||||
// #[tauri::command]
|
||||
// pub async fn get_formatted_memory_data() -> CmdResult<serde_json::Value> {
|
||||
// logging!(info, Type::Ipc, "获取格式化内存数据");
|
||||
// let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await;
|
||||
// let result = serde_json::json!({
|
||||
// "inuse_formatted": inuse,
|
||||
// "oslimit_formatted": oslimit,
|
||||
// "usage_percent": usage_percent,
|
||||
// "is_fresh": is_fresh
|
||||
// });
|
||||
// logging!(
|
||||
// debug,
|
||||
// Type::Ipc,
|
||||
// "格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"
|
||||
// );
|
||||
// Ok(result)
|
||||
// }
|
||||
|
||||
// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态)
|
||||
// #[tauri::command]
|
||||
// pub async fn get_system_monitor_overview() -> CmdResult<serde_json::Value> {
|
||||
// logging!(debug, Type::Ipc, "获取系统监控概览");
|
||||
|
||||
// // 并发获取流量和内存数据
|
||||
// let (traffic, memory) = tokio::join!(
|
||||
// crate::ipc::get_current_traffic(),
|
||||
// crate::ipc::get_current_memory()
|
||||
// );
|
||||
|
||||
// let (traffic_formatted, memory_formatted) = tokio::join!(
|
||||
// crate::ipc::get_formatted_traffic(),
|
||||
// crate::ipc::get_formatted_memory()
|
||||
// );
|
||||
|
||||
// let traffic_is_fresh = traffic.last_updated.elapsed().as_secs() < 5;
|
||||
// let memory_is_fresh = memory.last_updated.elapsed().as_secs() < 10;
|
||||
|
||||
// let result = serde_json::json!({
|
||||
// "traffic": {
|
||||
// "raw": {
|
||||
// "up": traffic.total_up,
|
||||
// "down": traffic.total_down,
|
||||
// "up_rate": traffic.up_rate,
|
||||
// "down_rate": traffic.down_rate
|
||||
// },
|
||||
// "formatted": {
|
||||
// "up_rate": traffic_formatted.0,
|
||||
// "down_rate": traffic_formatted.1,
|
||||
// "total_up": traffic_formatted.2,
|
||||
// "total_down": traffic_formatted.3
|
||||
// },
|
||||
// "is_fresh": traffic_is_fresh
|
||||
// },
|
||||
// "memory": {
|
||||
// "raw": {
|
||||
// "inuse": memory.inuse,
|
||||
// "oslimit": memory.oslimit,
|
||||
// "usage_percent": if memory.oslimit > 0 {
|
||||
// (memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||
// } else {
|
||||
// 0.0
|
||||
// }
|
||||
// },
|
||||
// "formatted": {
|
||||
// "inuse": memory_formatted.0,
|
||||
// "oslimit": memory_formatted.1,
|
||||
// "usage_percent": memory_formatted.2
|
||||
// },
|
||||
// "is_fresh": memory_is_fresh
|
||||
// },
|
||||
// "overall_status": if traffic_is_fresh && memory_is_fresh { "healthy" } else { "stale" }
|
||||
// });
|
||||
|
||||
// Ok(result)
|
||||
// }
|
||||
|
||||
// 获取代理组延迟
|
||||
// #[tauri::command]
|
||||
// pub async fn get_group_proxy_delays(
|
||||
// group_name: String,
|
||||
// url: Option<String>,
|
||||
// timeout: Option<i32>,
|
||||
// ) -> CmdResult<serde_json::Value> {
|
||||
// wrap_err!(
|
||||
// IpcManager::global()
|
||||
// .get_group_proxy_delays(&group_name, url, timeout.unwrap_or(10000))
|
||||
// .await
|
||||
// )
|
||||
// }
|
||||
|
||||
// 检查调试是否启用
|
||||
// #[tauri::command]
|
||||
// pub async fn is_clash_debug_enabled() -> CmdResult<bool> {
|
||||
// match IpcManager::global().is_debug_enabled().await {
|
||||
// Ok(enabled) => Ok(enabled),
|
||||
// Err(_) => Ok(false),
|
||||
// }
|
||||
// }
|
||||
|
||||
// 垃圾回收
|
||||
// #[tauri::command]
|
||||
// pub async fn clash_gc() -> CmdResult {
|
||||
// wrap_err!(IpcManager::global().gc().await)
|
||||
// }
|
||||
|
||||
// 获取日志 (使用新的流式实现)
|
||||
// #[tauri::command]
|
||||
// pub async fn get_clash_logs() -> CmdResult<serde_json::Value> {
|
||||
// Ok(ipc::get_logs_json().await)
|
||||
// }
|
||||
|
||||
/// 获取内存数据 (使用新的IPC流式监控)
|
||||
#[tauri::command]
|
||||
pub async fn get_memory_data() -> CmdResult<serde_json::Value> {
|
||||
let memory = crate::ipc::get_current_memory().await;
|
||||
let usage_percent = if memory.oslimit > 0 {
|
||||
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
pub async fn get_clash_logs() -> CmdResult<VecDeque<String>> {
|
||||
let logs = match core::CoreManager::global().get_running_mode() {
|
||||
// TODO: 服务模式下日志获取接口
|
||||
RunningMode::Service => VecDeque::new(),
|
||||
RunningMode::Sidecar => logger::Logger::global().get_logs().clone(),
|
||||
_ => VecDeque::new(),
|
||||
};
|
||||
let result = serde_json::json!({
|
||||
"inuse": memory.inuse,
|
||||
"oslimit": memory.oslimit,
|
||||
"usage_percent": usage_percent,
|
||||
"last_updated": memory.last_updated.elapsed().as_secs()
|
||||
});
|
||||
Ok(result)
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
/// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留)
|
||||
#[tauri::command]
|
||||
pub async fn start_traffic_service() -> CmdResult {
|
||||
logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)");
|
||||
// 新的IPC监控在首次访问时自动启动
|
||||
// 触发一次访问以确保监控器已初始化
|
||||
let _ = crate::ipc::get_current_traffic().await;
|
||||
let _ = crate::ipc::get_current_memory().await;
|
||||
logging!(info, Type::Ipc, "IPC流式监控已激活");
|
||||
Ok(())
|
||||
}
|
||||
// 启动日志监控
|
||||
// #[tauri::command]
|
||||
// pub async fn start_logs_monitoring(level: Option<String>) -> CmdResult {
|
||||
// ipc::start_logs_monitoring(level).await;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
/// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留)
|
||||
#[tauri::command]
|
||||
pub async fn stop_traffic_service() -> CmdResult {
|
||||
logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)");
|
||||
// 新的IPC监控是持久的,无需显式停止
|
||||
logging!(info, Type::Ipc, "IPC流式监控继续运行");
|
||||
Ok(())
|
||||
}
|
||||
// 停止日志监控
|
||||
// #[tauri::command]
|
||||
// pub async fn stop_logs_monitoring() -> CmdResult {
|
||||
// ipc::stop_logs_monitoring().await;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
/// 获取格式化的流量数据 (包含单位,便于前端显示)
|
||||
#[tauri::command]
|
||||
pub async fn get_formatted_traffic_data() -> CmdResult<serde_json::Value> {
|
||||
logging!(trace, Type::Ipc, "获取格式化流量数据");
|
||||
let (up_rate, down_rate, total_up, total_down, is_fresh) =
|
||||
crate::ipc::get_formatted_traffic().await;
|
||||
let result = serde_json::json!({
|
||||
"up_rate_formatted": up_rate,
|
||||
"down_rate_formatted": down_rate,
|
||||
"total_up_formatted": total_up,
|
||||
"total_down_formatted": total_down,
|
||||
"is_fresh": is_fresh
|
||||
});
|
||||
logging!(
|
||||
debug,
|
||||
Type::Ipc,
|
||||
"格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 获取格式化的内存数据 (包含单位,便于前端显示)
|
||||
#[tauri::command]
|
||||
pub async fn get_formatted_memory_data() -> CmdResult<serde_json::Value> {
|
||||
logging!(info, Type::Ipc, "获取格式化内存数据");
|
||||
let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await;
|
||||
let result = serde_json::json!({
|
||||
"inuse_formatted": inuse,
|
||||
"oslimit_formatted": oslimit,
|
||||
"usage_percent": usage_percent,
|
||||
"is_fresh": is_fresh
|
||||
});
|
||||
logging!(
|
||||
debug,
|
||||
Type::Ipc,
|
||||
"格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态)
|
||||
#[tauri::command]
|
||||
pub async fn get_system_monitor_overview() -> CmdResult<serde_json::Value> {
|
||||
logging!(debug, Type::Ipc, "获取系统监控概览");
|
||||
|
||||
// 并发获取流量和内存数据
|
||||
let (traffic, memory) = tokio::join!(
|
||||
crate::ipc::get_current_traffic(),
|
||||
crate::ipc::get_current_memory()
|
||||
);
|
||||
|
||||
let (traffic_formatted, memory_formatted) = tokio::join!(
|
||||
crate::ipc::get_formatted_traffic(),
|
||||
crate::ipc::get_formatted_memory()
|
||||
);
|
||||
|
||||
let traffic_is_fresh = traffic.last_updated.elapsed().as_secs() < 5;
|
||||
let memory_is_fresh = memory.last_updated.elapsed().as_secs() < 10;
|
||||
|
||||
let result = serde_json::json!({
|
||||
"traffic": {
|
||||
"raw": {
|
||||
"up": traffic.total_up,
|
||||
"down": traffic.total_down,
|
||||
"up_rate": traffic.up_rate,
|
||||
"down_rate": traffic.down_rate
|
||||
},
|
||||
"formatted": {
|
||||
"up_rate": traffic_formatted.0,
|
||||
"down_rate": traffic_formatted.1,
|
||||
"total_up": traffic_formatted.2,
|
||||
"total_down": traffic_formatted.3
|
||||
},
|
||||
"is_fresh": traffic_is_fresh
|
||||
},
|
||||
"memory": {
|
||||
"raw": {
|
||||
"inuse": memory.inuse,
|
||||
"oslimit": memory.oslimit,
|
||||
"usage_percent": if memory.oslimit > 0 {
|
||||
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
},
|
||||
"formatted": {
|
||||
"inuse": memory_formatted.0,
|
||||
"oslimit": memory_formatted.1,
|
||||
"usage_percent": memory_formatted.2
|
||||
},
|
||||
"is_fresh": memory_is_fresh
|
||||
},
|
||||
"overall_status": if traffic_is_fresh && memory_is_fresh { "healthy" } else { "stale" }
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 获取代理组延迟
|
||||
#[tauri::command]
|
||||
pub async fn get_group_proxy_delays(
|
||||
group_name: String,
|
||||
url: Option<String>,
|
||||
timeout: Option<i32>,
|
||||
) -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(
|
||||
IpcManager::global()
|
||||
.get_group_proxy_delays(&group_name, url, timeout.unwrap_or(10000))
|
||||
.await
|
||||
)
|
||||
}
|
||||
|
||||
/// 检查调试是否启用
|
||||
#[tauri::command]
|
||||
pub async fn is_clash_debug_enabled() -> CmdResult<bool> {
|
||||
match IpcManager::global().is_debug_enabled().await {
|
||||
Ok(enabled) => Ok(enabled),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// 垃圾回收
|
||||
#[tauri::command]
|
||||
pub async fn clash_gc() -> CmdResult {
|
||||
wrap_err!(IpcManager::global().gc().await)
|
||||
}
|
||||
|
||||
/// 获取日志 (使用新的流式实现)
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_logs() -> CmdResult<serde_json::Value> {
|
||||
Ok(ipc::get_logs_json().await)
|
||||
}
|
||||
|
||||
/// 启动日志监控
|
||||
#[tauri::command]
|
||||
pub async fn start_logs_monitoring(level: Option<String>) -> CmdResult {
|
||||
ipc::start_logs_monitoring(level).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止日志监控
|
||||
#[tauri::command]
|
||||
pub async fn stop_logs_monitoring() -> CmdResult {
|
||||
ipc::stop_logs_monitoring().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 清除日志
|
||||
#[tauri::command]
|
||||
pub async fn clear_logs() -> CmdResult {
|
||||
ipc::clear_logs().await;
|
||||
Ok(())
|
||||
}
|
||||
// 清除日志
|
||||
// #[tauri::command]
|
||||
// pub async fn clear_logs() -> CmdResult {
|
||||
// ipc::clear_logs().await;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
@@ -481,11 +481,11 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||
log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
|
||||
}
|
||||
});
|
||||
// crate::process::AsyncHandler::spawn(|| async move {
|
||||
// if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||
// log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
|
||||
// }
|
||||
// });
|
||||
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||
|
||||
+52
-47
@@ -3,57 +3,60 @@ use tauri::Emitter;
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
cache::CacheProxy,
|
||||
core::{handle::Handle, tray::Tray},
|
||||
ipc::IpcManager,
|
||||
core::{
|
||||
handle::{self, Handle},
|
||||
tray::Tray,
|
||||
},
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use std::time::Duration;
|
||||
// use std::time::Duration;
|
||||
|
||||
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
// const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
// const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
let cache = CacheProxy::global();
|
||||
let key = CacheProxy::make_key("proxies", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
|
||||
let manager = IpcManager::global();
|
||||
manager.get_proxies().await.unwrap_or_else(|e| {
|
||||
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
})
|
||||
})
|
||||
.await;
|
||||
Ok((*value).clone())
|
||||
}
|
||||
// #[tauri::command]
|
||||
// pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
// let cache = CacheProxy::global();
|
||||
// let key = CacheProxy::make_key("proxies", "default");
|
||||
// let value = cache
|
||||
// .get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
|
||||
// let manager = IpcManager::global();
|
||||
// manager.get_proxies().await.unwrap_or_else(|e| {
|
||||
// logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
|
||||
// serde_json::Value::Object(serde_json::Map::new())
|
||||
// })
|
||||
// })
|
||||
// .await;
|
||||
// Ok((*value).clone())
|
||||
// }
|
||||
|
||||
/// 强制刷新代理缓存用于profile切换
|
||||
#[tauri::command]
|
||||
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
let cache = CacheProxy::global();
|
||||
let key = CacheProxy::make_key("proxies", "default");
|
||||
cache.map.remove(&key);
|
||||
get_proxies().await
|
||||
}
|
||||
// #[tauri::command]
|
||||
// pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
// let cache = CacheProxy::global();
|
||||
// let key = CacheProxy::make_key("proxies", "default");
|
||||
// cache.map.remove(&key);
|
||||
// get_proxies().await
|
||||
// }
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
let cache = CacheProxy::global();
|
||||
let key = CacheProxy::make_key("providers", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
|
||||
let manager = IpcManager::global();
|
||||
manager.get_providers_proxies().await.unwrap_or_else(|e| {
|
||||
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
})
|
||||
})
|
||||
.await;
|
||||
Ok((*value).clone())
|
||||
}
|
||||
// #[tauri::command]
|
||||
// pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
// let cache = CacheProxy::global();
|
||||
// let key = CacheProxy::make_key("providers", "default");
|
||||
// let value = cache
|
||||
// .get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
|
||||
// let manager = IpcManager::global();
|
||||
// manager.get_providers_proxies().await.unwrap_or_else(|e| {
|
||||
// logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
|
||||
// serde_json::Value::Object(serde_json::Map::new())
|
||||
// })
|
||||
// })
|
||||
// .await;
|
||||
// Ok((*value).clone())
|
||||
// }
|
||||
|
||||
// TODO: 前端通过 emit 发送更新事件, tray 监听更新事件
|
||||
/// 同步托盘和GUI的代理选择状态
|
||||
#[tauri::command]
|
||||
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
||||
@@ -74,7 +77,11 @@ pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
||||
/// 更新代理选择并同步托盘和GUI状态
|
||||
#[tauri::command]
|
||||
pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> {
|
||||
match IpcManager::global().update_proxy(&group, &proxy).await {
|
||||
match handle::Handle::mihomo()
|
||||
.await
|
||||
.select_node_for_group(&group, &proxy)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// println!("Proxy updated successfully: {} -> {}", group,proxy);
|
||||
logging!(
|
||||
@@ -93,10 +100,8 @@ pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()
|
||||
logging!(error, Type::Cmd, "Failed to sync tray menu: {}", e);
|
||||
}
|
||||
|
||||
if let Some(app_handle) = Handle::global().app_handle() {
|
||||
let _ = app_handle.emit("verge://force-refresh-proxies", ());
|
||||
let _ = app_handle.emit("verge://refresh-proxy-config", ());
|
||||
}
|
||||
let app_handle = Handle::app_handle();
|
||||
let _ = app_handle.emit("verge://refresh-proxy-config", ());
|
||||
|
||||
logging!(
|
||||
info,
|
||||
|
||||
@@ -28,9 +28,7 @@ pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
let sysinfo = PlatformSpecification::new_sync();
|
||||
let info = format!("{sysinfo:?}");
|
||||
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or("Failed to get app handle")?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let cliboard = app_handle.clipboard();
|
||||
if cliboard.write_text(info).is_err() {
|
||||
logging!(error, Type::System, "Failed to write to clipboard");
|
||||
|
||||
+15
-12
@@ -1,11 +1,11 @@
|
||||
use crate::AsyncHandler;
|
||||
use crate::core::logger::Logger;
|
||||
use crate::{
|
||||
config::*,
|
||||
core::{
|
||||
handle,
|
||||
service::{self, SERVICE_MANAGER, ServiceStatus},
|
||||
},
|
||||
ipc::IpcManager,
|
||||
logging, logging_error, singleton_lazy,
|
||||
utils::{
|
||||
dirs,
|
||||
@@ -250,11 +250,7 @@ impl CoreManager {
|
||||
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
|
||||
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
|
||||
let msg = "Failed to get app handle";
|
||||
logging!(error, Type::Core, true, "{}", msg);
|
||||
anyhow::anyhow!(msg)
|
||||
})?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let app_dir_str = dirs::path_to_str(&app_dir)?;
|
||||
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
|
||||
@@ -414,7 +410,11 @@ impl CoreManager {
|
||||
logging_error!(Type::Core, true, "{}", msg);
|
||||
msg
|
||||
});
|
||||
match IpcManager::global().put_configs_force(run_path_str?).await {
|
||||
match handle::Handle::mihomo()
|
||||
.await
|
||||
.reload_config(true, run_path_str?)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
Config::runtime().await.apply();
|
||||
logging!(info, Type::Core, true, "Configuration updated successfully");
|
||||
@@ -733,9 +733,7 @@ impl CoreManager {
|
||||
logging!(info, Type::Core, true, "Running core by sidecar");
|
||||
|
||||
let config_file = &Config::generate_file(ConfigType::Run).await?;
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
|
||||
@@ -774,12 +772,16 @@ impl CoreManager {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
tauri_plugin_shell::process::CommandEvent::Stdout(line) => {
|
||||
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
|
||||
let line = String::from_utf8_lossy(&line);
|
||||
Logger::global().append_log(line.to_string());
|
||||
if let Err(e) = writeln!(log_file, "{}", line) {
|
||||
eprintln!("[Sidecar] write stdout failed: {e}");
|
||||
}
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
|
||||
let _ = writeln!(log_file, "[stderr] {}", String::from_utf8_lossy(&line));
|
||||
let line = String::from_utf8_lossy(&line);
|
||||
Logger::global().append_log(line.to_string());
|
||||
let _ = writeln!(log_file, "[stderr] {}", line);
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
|
||||
let _ = writeln!(log_file, "[terminated] {:?}", term);
|
||||
@@ -900,6 +902,7 @@ impl CoreManager {
|
||||
|
||||
/// 停止核心运行
|
||||
pub async fn stop_core(&self) -> Result<()> {
|
||||
Logger::global().clear_logs();
|
||||
match self.get_running_mode() {
|
||||
RunningMode::Service => self.stop_core_by_service().await,
|
||||
RunningMode::Sidecar => self.stop_core_by_sidecar(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::singleton;
|
||||
use crate::{APP_HANDLE, singleton};
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
sync::{
|
||||
@@ -10,6 +10,8 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||
use tauri_plugin_mihomo::{Mihomo, MihomoExt};
|
||||
use tokio::sync::{RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
@@ -107,7 +109,7 @@ impl NotificationSystem {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(window) = handle.get_window() {
|
||||
if let Some(window) = Handle::get_window() {
|
||||
*system.last_emit_time.write() = Instant::now();
|
||||
|
||||
let (event_name_str, payload_result) = match event {
|
||||
@@ -249,7 +251,6 @@ impl NotificationSystem {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Handle {
|
||||
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||
pub is_exiting: Arc<RwLock<bool>>,
|
||||
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||
startup_completed: Arc<RwLock<bool>>,
|
||||
@@ -259,7 +260,6 @@ pub struct Handle {
|
||||
impl Default for Handle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
app_handle: Arc::new(RwLock::new(None)),
|
||||
is_exiting: Arc::new(RwLock::new(false)),
|
||||
startup_errors: Arc::new(RwLock::new(Vec::new())),
|
||||
startup_completed: Arc::new(RwLock::new(false)),
|
||||
@@ -276,12 +276,7 @@ impl Handle {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn init(&self, app_handle: AppHandle) {
|
||||
{
|
||||
let mut handle = self.app_handle.write();
|
||||
*handle = Some(app_handle);
|
||||
}
|
||||
|
||||
pub fn init(&self) {
|
||||
let mut system_opt = self.notification_system.write();
|
||||
if let Some(system) = system_opt.as_mut() {
|
||||
system.start();
|
||||
@@ -289,12 +284,21 @@ impl Handle {
|
||||
}
|
||||
|
||||
/// 获取 AppHandle
|
||||
pub fn app_handle(&self) -> Option<AppHandle> {
|
||||
self.app_handle.read().clone()
|
||||
pub fn app_handle() -> &'static AppHandle {
|
||||
APP_HANDLE.get().expect("failed to get global app handle")
|
||||
}
|
||||
|
||||
pub fn get_window(&self) -> Option<WebviewWindow> {
|
||||
let app_handle = self.app_handle()?;
|
||||
pub async fn mihomo() -> RwLockReadGuard<'static, Mihomo> {
|
||||
Self::app_handle().mihomo().read().await
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn mihomo_mut() -> RwLockWriteGuard<'static, Mihomo> {
|
||||
Self::app_handle().mihomo().write().await
|
||||
}
|
||||
|
||||
pub fn get_window() -> Option<WebviewWindow> {
|
||||
let app_handle = Self::app_handle();
|
||||
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
|
||||
if window.is_none() {
|
||||
log::debug!(target:"app", "main window not found");
|
||||
|
||||
@@ -200,9 +200,7 @@ impl Hotkey {
|
||||
hotkey: &str,
|
||||
function: HotkeyFunction,
|
||||
) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let manager = app_handle.global_shortcut();
|
||||
|
||||
logging!(
|
||||
@@ -375,9 +373,7 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let manager = app_handle.global_shortcut();
|
||||
manager.unregister_all()?;
|
||||
Ok(())
|
||||
@@ -390,9 +386,7 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
pub fn unregister(&self, hotkey: &str) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let manager = app_handle.global_shortcut();
|
||||
manager.unregister(hotkey)?;
|
||||
logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey);
|
||||
@@ -468,17 +462,7 @@ impl Hotkey {
|
||||
|
||||
impl Drop for Hotkey {
|
||||
fn drop(&mut self) {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
"Failed to get app handle during hotkey cleanup"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||
logging!(
|
||||
error,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::{RwLock, RwLockReadGuard};
|
||||
|
||||
const LOGS_QUEUE_LEN: usize = 100;
|
||||
|
||||
pub struct Logger {
|
||||
logs: Arc<RwLock<VecDeque<String>>>,
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
pub fn global() -> &'static Logger {
|
||||
static LOGGER: OnceCell<Logger> = OnceCell::new();
|
||||
|
||||
LOGGER.get_or_init(|| Logger {
|
||||
logs: Arc::new(RwLock::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_logs(&self) -> RwLockReadGuard<'_, VecDeque<String>> {
|
||||
self.logs.read()
|
||||
}
|
||||
|
||||
pub fn append_log(&self, text: String) {
|
||||
let mut logs = self.logs.write();
|
||||
if logs.len() > LOGS_QUEUE_LEN {
|
||||
logs.pop_front();
|
||||
}
|
||||
logs.push_back(text);
|
||||
}
|
||||
|
||||
pub fn clear_logs(&self) {
|
||||
let mut logs = self.logs.write();
|
||||
logs.clear();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod core;
|
||||
pub mod event_driven_proxy;
|
||||
pub mod handle;
|
||||
pub mod hotkey;
|
||||
pub mod logger;
|
||||
pub mod service;
|
||||
pub mod service_ipc;
|
||||
pub mod sysopt;
|
||||
|
||||
@@ -293,10 +293,7 @@ impl Sysopt {
|
||||
|
||||
/// 尝试使用原来的自启动方法
|
||||
fn try_original_autostart_method(&self, is_enable: bool) {
|
||||
let Some(app_handle) = Handle::global().app_handle() else {
|
||||
log::error!(target: "app", "App handle not available for autostart");
|
||||
return;
|
||||
};
|
||||
let app_handle = Handle::app_handle();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
|
||||
if is_enable {
|
||||
@@ -323,9 +320,7 @@ impl Sysopt {
|
||||
}
|
||||
|
||||
// 回退到原来的方法
|
||||
let app_handle = Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
|
||||
let app_handle = Handle::app_handle();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
|
||||
match autostart_manager.is_enabled() {
|
||||
|
||||
+99
-139
@@ -3,16 +3,13 @@ use tauri::Emitter;
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::ipc::Rate;
|
||||
use crate::module::lightweight;
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
use crate::{
|
||||
Type, cmd,
|
||||
config::Config,
|
||||
feat,
|
||||
ipc::IpcManager,
|
||||
logging,
|
||||
feat, logging,
|
||||
module::lightweight::is_in_lightweight_mode,
|
||||
singleton_lazy,
|
||||
utils::{dirs::find_target_icons, i18n::t},
|
||||
@@ -54,7 +51,7 @@ fn should_handle_tray_click() -> bool {
|
||||
*last_click = now;
|
||||
true
|
||||
} else {
|
||||
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
|
||||
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
|
||||
now.duration_since(*last_click).as_millis());
|
||||
false
|
||||
}
|
||||
@@ -189,18 +186,14 @@ singleton_lazy!(Tray, TRAY, Tray::default);
|
||||
|
||||
impl Tray {
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray initialization"))?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
self.create_tray_from_handle(&app_handle).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘点击行为
|
||||
pub async fn update_click_behavior(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
let tray = app_handle
|
||||
@@ -240,13 +233,7 @@ impl Tray {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
// 设置更新状态
|
||||
self.menu_updating.store(true, Ordering::Release);
|
||||
@@ -309,7 +296,7 @@ impl Tray {
|
||||
/// 更新托盘图标
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
let app_handle = match handle::Handle::app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
||||
@@ -345,14 +332,8 @@ impl Tray {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub async 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(());
|
||||
}
|
||||
};
|
||||
pub async fn update_icon(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
let tray = match app_handle.tray_by_id("main") {
|
||||
Some(tray) => tray,
|
||||
@@ -379,9 +360,7 @@ impl Tray {
|
||||
|
||||
/// 更新托盘显示状态的函数
|
||||
pub async fn update_tray_display(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let _tray = app_handle
|
||||
.tray_by_id("main")
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
|
||||
@@ -394,13 +373,7 @@ impl Tray {
|
||||
|
||||
/// 更新托盘提示
|
||||
pub async 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 app_handle = handle::Handle::app_handle();
|
||||
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
@@ -454,7 +427,7 @@ impl Tray {
|
||||
// self.update_menu().await?;
|
||||
// 更新轻量模式显示状态
|
||||
self.update_tray_display().await?;
|
||||
self.update_icon(None).await?;
|
||||
self.update_icon().await?;
|
||||
self.update_tooltip().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -540,7 +513,7 @@ impl Tray {
|
||||
// 确保所有状态更新完成
|
||||
self.update_tray_display().await?;
|
||||
// self.update_menu().await?;
|
||||
self.update_icon(None).await?;
|
||||
self.update_icon().await?;
|
||||
self.update_tooltip().await?;
|
||||
|
||||
Ok(())
|
||||
@@ -568,14 +541,7 @@ async fn create_tray_menu(
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let proxy_nodes_data = cmd::get_proxies().await.unwrap_or_else(|e| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"Failed to fetch proxies for tray menu: {e}"
|
||||
);
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
});
|
||||
let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await?;
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -628,102 +594,96 @@ async fn create_tray_menu(
|
||||
let mut submenus = Vec::new();
|
||||
let mut group_name_submenus_hash = HashMap::new();
|
||||
|
||||
if let Some(proxies) = proxy_nodes_data.get("proxies").and_then(|v| v.as_object()) {
|
||||
for (group_name, group_data) in proxies.iter() {
|
||||
// Filter groups based on mode
|
||||
let should_show = match mode {
|
||||
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
|
||||
// Filter groups based on mode
|
||||
let should_show = match mode {
|
||||
"global" => group_name == "GLOBAL",
|
||||
_ => group_name != "GLOBAL",
|
||||
} &&
|
||||
// Check if the group is hidden
|
||||
!group_data.get("hidden").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
!group_data.hidden.unwrap_or_default();
|
||||
|
||||
if !should_show {
|
||||
continue;
|
||||
if !should_show {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(all_proxies) = group_data.all.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let now_proxy = group_data.now.as_deref().unwrap_or_default();
|
||||
|
||||
// Create proxy items
|
||||
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
|
||||
.iter()
|
||||
.filter_map(|proxy_str| {
|
||||
let is_selected = *proxy_str == now_proxy;
|
||||
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
|
||||
|
||||
// Get delay for display
|
||||
let delay_text = proxy_nodes_data
|
||||
.proxies
|
||||
.get(proxy_str)
|
||||
.and_then(|h| h.history.last())
|
||||
.map(|h| match h.delay {
|
||||
0 => "-ms".to_string(),
|
||||
delay if delay >= 10000 => "-ms".to_string(),
|
||||
_ => format!("{}ms", h.delay),
|
||||
})
|
||||
.unwrap_or_else(|| "-ms".to_string());
|
||||
|
||||
let display_text = format!("{} | {}", proxy_str, delay_text);
|
||||
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
item_id,
|
||||
display_text,
|
||||
true,
|
||||
is_selected,
|
||||
None::<&str>,
|
||||
)
|
||||
.map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if group_items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine if group is active
|
||||
let is_group_active = match mode {
|
||||
"global" => group_name == "GLOBAL" && !now_proxy.is_empty(),
|
||||
"direct" => false,
|
||||
_ => {
|
||||
current_profile_selected
|
||||
.iter()
|
||||
.any(|s| s.name.as_deref() == Some(&group_name))
|
||||
&& !now_proxy.is_empty()
|
||||
}
|
||||
};
|
||||
|
||||
let Some(all_proxies) = group_data.get("all").and_then(|v| v.as_array()) else {
|
||||
continue;
|
||||
};
|
||||
let group_display_name = if is_group_active {
|
||||
format!("✓ {}", group_name)
|
||||
} else {
|
||||
group_name.to_string()
|
||||
};
|
||||
|
||||
let now_proxy = group_data.get("now").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> = group_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
|
||||
// Create proxy items
|
||||
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
|
||||
.iter()
|
||||
.filter_map(|proxy_name| proxy_name.as_str())
|
||||
.filter_map(|proxy_str| {
|
||||
let is_selected = proxy_str == now_proxy;
|
||||
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
|
||||
|
||||
// Get delay for display
|
||||
let delay_text = proxies
|
||||
.get(proxy_str)
|
||||
.and_then(|p| p.get("history"))
|
||||
.and_then(|h| h.as_array())
|
||||
.and_then(|h| h.last())
|
||||
.and_then(|r| r.get("delay"))
|
||||
.and_then(|d| d.as_i64())
|
||||
.map(|delay| match delay {
|
||||
-1 => "-ms".to_string(),
|
||||
delay if delay >= 10000 => "-ms".to_string(),
|
||||
_ => format!("{}ms", delay),
|
||||
})
|
||||
.unwrap_or_else(|| "-ms".to_string());
|
||||
|
||||
let display_text = format!("{} | {}", proxy_str, delay_text);
|
||||
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
item_id,
|
||||
display_text,
|
||||
true,
|
||||
is_selected,
|
||||
None::<&str>,
|
||||
)
|
||||
.map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if group_items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine if group is active
|
||||
let is_group_active = match mode {
|
||||
"global" => group_name == "GLOBAL" && !now_proxy.is_empty(),
|
||||
"direct" => false,
|
||||
_ => {
|
||||
current_profile_selected
|
||||
.iter()
|
||||
.any(|s| s.name.as_deref() == Some(group_name))
|
||||
&& !now_proxy.is_empty()
|
||||
}
|
||||
};
|
||||
|
||||
let group_display_name = if is_group_active {
|
||||
format!("✓ {}", group_name)
|
||||
} else {
|
||||
group_name.to_string()
|
||||
};
|
||||
|
||||
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> = group_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
|
||||
if let Ok(submenu) = Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group_name),
|
||||
group_display_name,
|
||||
true,
|
||||
&group_items_refs,
|
||||
) {
|
||||
group_name_submenus_hash.insert(group_name.to_string(), submenu);
|
||||
} else {
|
||||
log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name);
|
||||
}
|
||||
if let Ok(submenu) = Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group_name),
|
||||
group_display_name,
|
||||
true,
|
||||
&group_items_refs,
|
||||
) {
|
||||
group_name_submenus_hash.insert(group_name.to_string(), submenu);
|
||||
} else {
|
||||
log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1069,16 +1029,16 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e);
|
||||
|
||||
// Fallback to IPC update
|
||||
if (IpcManager::global()
|
||||
.update_proxy(group_name, proxy_name)
|
||||
if (handle::Handle::mihomo()
|
||||
.await
|
||||
.select_node_for_group(group_name, proxy_name)
|
||||
.await)
|
||||
.is_ok()
|
||||
{
|
||||
log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name);
|
||||
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
let _ = app_handle.emit("verge://force-refresh-proxies", ());
|
||||
}
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let _ = app_handle.emit("verge://force-refresh-proxies", ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-25
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{CoreManager, handle, tray},
|
||||
ipc::IpcManager,
|
||||
logging_error,
|
||||
process::AsyncHandler,
|
||||
utils::{logging::Type, resolve},
|
||||
@@ -27,30 +26,18 @@ pub async fn restart_app() {
|
||||
// logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||
resolve::resolve_reset_async().await;
|
||||
|
||||
handle::Handle::global()
|
||||
.app_handle()
|
||||
.map(|app_handle| {
|
||||
app_handle.restart();
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
logging_error!(
|
||||
Type::System,
|
||||
false,
|
||||
"{}",
|
||||
"Failed to get app handle for restart"
|
||||
);
|
||||
});
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle.restart();
|
||||
}
|
||||
|
||||
fn after_change_clash_mode() {
|
||||
AsyncHandler::spawn(move || async {
|
||||
match IpcManager::global().get_connections().await {
|
||||
let mihomo = handle::Handle::mihomo().await;
|
||||
match mihomo.get_connections().await {
|
||||
Ok(connections) => {
|
||||
if let Some(connections_array) = connections["connections"].as_array() {
|
||||
if let Some(connections_array) = connections.connections {
|
||||
for connection in connections_array {
|
||||
if let Some(id) = connection["id"].as_str() {
|
||||
let _ = IpcManager::global().delete_connection(id).await;
|
||||
}
|
||||
let _ = mihomo.close_connection(&connection.id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +57,11 @@ pub async fn change_clash_mode(mode: String) {
|
||||
"mode": mode
|
||||
});
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
match IpcManager::global().patch_configs(json_value).await {
|
||||
match handle::Handle::mihomo()
|
||||
.await
|
||||
.patch_base_config(&json_value)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// 更新订阅
|
||||
Config::clash().await.data_mut().patch_config(mapping);
|
||||
@@ -80,11 +71,7 @@ pub async fn change_clash_mode(mode: String) {
|
||||
if clash_data.save_config().await.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
true,
|
||||
tray::Tray::global().update_icon(None).await
|
||||
);
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon().await);
|
||||
}
|
||||
|
||||
let is_auto_close_connection = Config::verge()
|
||||
|
||||
@@ -23,11 +23,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
} else {
|
||||
if patch.get("mode").is_some() {
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
true,
|
||||
tray::Tray::global().update_icon(None).await
|
||||
);
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon().await);
|
||||
}
|
||||
Config::runtime().await.draft_mut().patch_config(patch);
|
||||
CoreManager::global().update_config().await?;
|
||||
@@ -211,7 +207,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
tray::Tray::global().update_menu().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
|
||||
tray::Tray::global().update_icon(None).await?;
|
||||
tray::Tray::global().update_icon().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
|
||||
tray::Tray::global().update_tooltip().await?;
|
||||
|
||||
@@ -143,15 +143,15 @@ pub async fn update_profile(
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
if let Err(err) = cmd::proxy::force_refresh_proxies().await {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[订阅更新] 代理组刷新失败: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
// if let Err(err) = cmd::proxy::force_refresh_proxies().await {
|
||||
// logging!(
|
||||
// error,
|
||||
// Type::Config,
|
||||
// true,
|
||||
// "[订阅更新] 代理组刷新失败: {}",
|
||||
// err
|
||||
// );
|
||||
// }
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle,
|
||||
ipc::IpcManager,
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use std::env;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
@@ -26,7 +23,7 @@ pub async fn toggle_system_proxy() {
|
||||
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
|
||||
if enable
|
||||
&& auto_close_connection
|
||||
&& let Err(err) = IpcManager::global().close_all_connections().await
|
||||
&& let Err(err) = handle::Handle::mihomo().await.close_all_connections().await
|
||||
{
|
||||
log::error!(target: "app", "Failed to close all connections: {err}");
|
||||
}
|
||||
@@ -78,14 +75,7 @@ pub async fn copy_clash_env() {
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string()),
|
||||
};
|
||||
|
||||
let Some(app_handle) = handle::Handle::global().app_handle() else {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"Failed to get app handle for proxy operation"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let port = {
|
||||
Config::verge()
|
||||
.await
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::utils::window_manager::WindowManager;
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{CoreManager, handle, sysopt},
|
||||
ipc::IpcManager,
|
||||
logging,
|
||||
module::lightweight,
|
||||
utils::logging::Type,
|
||||
@@ -25,18 +24,11 @@ pub async fn quit() {
|
||||
logging!(debug, Type::System, true, "启动退出流程");
|
||||
|
||||
// 获取应用句柄并设置退出标志
|
||||
let Some(app_handle) = handle::Handle::global().app_handle() else {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"Failed to get app handle for quit operation"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
// 优先关闭窗口,提供立即反馈
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let _ = window.hide();
|
||||
log::info!(target: "app", "窗口已隐藏");
|
||||
}
|
||||
@@ -70,7 +62,9 @@ async fn clean_async() -> bool {
|
||||
let disable_tun = serde_json::json!({"tun": {"enable": false}});
|
||||
match timeout(
|
||||
Duration::from_secs(3),
|
||||
IpcManager::global().patch_configs(disable_tun),
|
||||
handle::Handle::mihomo()
|
||||
.await
|
||||
.patch_base_config(&disable_tun),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -216,7 +210,7 @@ pub async fn hide() {
|
||||
add_light_weight_timer().await;
|
||||
}
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window()
|
||||
if let Some(window) = handle::Handle::get_window()
|
||||
&& window.is_visible().unwrap_or(false)
|
||||
{
|
||||
let _ = window.hide();
|
||||
|
||||
+54
-44
@@ -7,28 +7,30 @@ pub mod config;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod feat;
|
||||
mod ipc;
|
||||
// mod ipc;
|
||||
mod module;
|
||||
mod process;
|
||||
mod utils;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
use crate::{
|
||||
core::handle,
|
||||
core::hotkey,
|
||||
process::AsyncHandler,
|
||||
utils::{resolve, server},
|
||||
};
|
||||
use config::Config;
|
||||
use tauri::AppHandle;
|
||||
use once_cell::sync::OnceCell;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::Manager;
|
||||
use tauri::{AppHandle, Manager};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tokio::time::{Duration, timeout};
|
||||
use utils::logging::Type;
|
||||
|
||||
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
|
||||
|
||||
/// Application initialization helper functions
|
||||
mod app_init {
|
||||
use super::*;
|
||||
@@ -41,7 +43,7 @@ mod app_init {
|
||||
Ok(result) => {
|
||||
if result.is_err() {
|
||||
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
if let Some(app_handle) = APP_HANDLE.get() {
|
||||
app_handle.exit(0);
|
||||
} else {
|
||||
std::process::exit(0);
|
||||
@@ -75,7 +77,13 @@ mod app_init {
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_http::init());
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(
|
||||
tauri_plugin_mihomo::Builder::new()
|
||||
.protocol(tauri_plugin_mihomo::models::Protocol::LocalSocket)
|
||||
.socket_path(crate::config::IClashTemp::guard_external_controller_ipc())
|
||||
.build(),
|
||||
);
|
||||
|
||||
// Devtools plugin only in debug mode with feature tauri-dev
|
||||
// to avoid duplicated registering of logger since the devtools plugin also registers a logger
|
||||
@@ -184,9 +192,9 @@ mod app_init {
|
||||
cmd::update_proxy_chain_config_in_runtime,
|
||||
cmd::invoke_uwp_tool,
|
||||
cmd::copy_clash_env,
|
||||
cmd::get_proxies,
|
||||
cmd::force_refresh_proxies,
|
||||
cmd::get_providers_proxies,
|
||||
// cmd::get_proxies,
|
||||
// cmd::force_refresh_proxies,
|
||||
// cmd::get_providers_proxies,
|
||||
cmd::sync_tray_proxy_selection,
|
||||
cmd::update_proxy_and_sync,
|
||||
cmd::save_dns_config,
|
||||
@@ -194,36 +202,36 @@ mod app_init {
|
||||
cmd::check_dns_config_exists,
|
||||
cmd::get_dns_config_content,
|
||||
cmd::validate_dns_config,
|
||||
cmd::get_clash_version,
|
||||
cmd::get_clash_config,
|
||||
cmd::force_refresh_clash_config,
|
||||
cmd::update_geo_data,
|
||||
cmd::upgrade_clash_core,
|
||||
cmd::get_clash_rules,
|
||||
cmd::update_proxy_choice,
|
||||
cmd::get_proxy_providers,
|
||||
cmd::get_rule_providers,
|
||||
cmd::proxy_provider_health_check,
|
||||
cmd::update_proxy_provider,
|
||||
cmd::update_rule_provider,
|
||||
cmd::get_clash_connections,
|
||||
cmd::delete_clash_connection,
|
||||
cmd::close_all_clash_connections,
|
||||
cmd::get_group_proxy_delays,
|
||||
cmd::is_clash_debug_enabled,
|
||||
cmd::clash_gc,
|
||||
// cmd::get_clash_version,
|
||||
// cmd::get_clash_config,
|
||||
// cmd::force_refresh_clash_config,
|
||||
// cmd::update_geo_data,
|
||||
// cmd::upgrade_clash_core,
|
||||
// cmd::get_clash_rules,
|
||||
// cmd::update_proxy_choice,
|
||||
// cmd::get_proxy_providers,
|
||||
// cmd::get_rule_providers,
|
||||
// cmd::proxy_provider_health_check,
|
||||
// cmd::update_proxy_provider,
|
||||
// cmd::update_rule_provider,
|
||||
// cmd::get_clash_connections,
|
||||
// cmd::delete_clash_connection,
|
||||
// cmd::close_all_clash_connections,
|
||||
// cmd::get_group_proxy_delays,
|
||||
// cmd::is_clash_debug_enabled,
|
||||
// cmd::clash_gc,
|
||||
// Logging and monitoring
|
||||
cmd::get_clash_logs,
|
||||
cmd::start_logs_monitoring,
|
||||
cmd::stop_logs_monitoring,
|
||||
cmd::clear_logs,
|
||||
cmd::get_traffic_data,
|
||||
cmd::get_memory_data,
|
||||
cmd::get_formatted_traffic_data,
|
||||
cmd::get_formatted_memory_data,
|
||||
cmd::get_system_monitor_overview,
|
||||
cmd::start_traffic_service,
|
||||
cmd::stop_traffic_service,
|
||||
// cmd::start_logs_monitoring,
|
||||
// cmd::stop_logs_monitoring,
|
||||
// cmd::clear_logs,
|
||||
// cmd::get_traffic_data,
|
||||
// cmd::get_memory_data,
|
||||
// cmd::get_formatted_traffic_data,
|
||||
// cmd::get_formatted_memory_data,
|
||||
// cmd::get_system_monitor_overview,
|
||||
// cmd::start_traffic_service,
|
||||
// cmd::stop_traffic_service,
|
||||
// Verge configuration
|
||||
cmd::get_verge_config,
|
||||
cmd::patch_verge_config,
|
||||
@@ -252,7 +260,7 @@ mod app_init {
|
||||
cmd::script_validate_notice,
|
||||
cmd::validate_script_file,
|
||||
// Clash API
|
||||
cmd::clash_api_get_proxy_delay,
|
||||
// cmd::clash_api_get_proxy_delay,
|
||||
// Backup and WebDAV
|
||||
cmd::create_webdav_backup,
|
||||
cmd::save_webdav_config,
|
||||
@@ -306,6 +314,10 @@ pub fn run() {
|
||||
.setup(|app| {
|
||||
logging!(info, Type::Setup, true, "开始应用初始化...");
|
||||
|
||||
APP_HANDLE
|
||||
.set(app.app_handle().clone())
|
||||
.expect("failed to set global app handle");
|
||||
|
||||
// Setup autostart plugin
|
||||
if let Err(e) = app_init::setup_autostart(app) {
|
||||
logging!(error, Type::Setup, true, "Failed to setup autostart: {}", e);
|
||||
@@ -333,11 +345,9 @@ pub fn run() {
|
||||
);
|
||||
}
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
||||
|
||||
resolve::resolve_setup_handle(app_handle);
|
||||
resolve::resolve_setup_handle();
|
||||
resolve::resolve_setup_async();
|
||||
resolve::resolve_setup_sync();
|
||||
|
||||
@@ -353,13 +363,13 @@ pub fn run() {
|
||||
use super::*;
|
||||
|
||||
/// Handle application ready/resumed events
|
||||
pub fn handle_ready_resumed(app_handle: &AppHandle) {
|
||||
pub fn handle_ready_resumed(_app_handle: &AppHandle) {
|
||||
logging!(info, Type::System, true, "应用就绪或恢复");
|
||||
handle::Handle::global().init(app_handle.clone());
|
||||
handle::Handle::global().init();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if let Some(window) = _app_handle.get_webview_window("main") {
|
||||
logging!(info, Type::Window, true, "设置macOS窗口标题");
|
||||
let _ = window.set_title("Clash Verge");
|
||||
}
|
||||
@@ -410,7 +420,7 @@ pub fn run() {
|
||||
log::info!(target: "app", "closing window...");
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = api {
|
||||
api.prevent_close();
|
||||
if let Some(window) = core::handle::Handle::global().get_window() {
|
||||
if let Some(window) = core::handle::Handle::get_window() {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
|
||||
|
||||
@@ -219,7 +219,7 @@ pub async fn add_light_weight_timer() {
|
||||
}
|
||||
|
||||
fn setup_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let handler = window.listen("tauri://close-requested", move |_event| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
if let Err(e) = setup_light_weight_timer().await {
|
||||
@@ -239,7 +239,7 @@ fn setup_window_close_listener() {
|
||||
}
|
||||
|
||||
fn cancel_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
|
||||
if handler != 0 {
|
||||
window.unlisten(handler);
|
||||
@@ -249,7 +249,7 @@ fn cancel_window_close_listener() {
|
||||
}
|
||||
|
||||
fn setup_webview_focus_listener() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let handler = window.listen("tauri://focus", move |_event| {
|
||||
log_err!(cancel_light_weight_timer());
|
||||
logging!(
|
||||
@@ -264,7 +264,7 @@ fn setup_webview_focus_listener() {
|
||||
}
|
||||
|
||||
fn cancel_webview_focus_listener() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
|
||||
if handler != 0 {
|
||||
window.unlisten(handler);
|
||||
|
||||
@@ -38,17 +38,7 @@ impl PlatformSpecification {
|
||||
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
|
||||
let system_arch = System::cpu_arch();
|
||||
|
||||
let Some(handler) = handle::Handle::global().app_handle() else {
|
||||
return Self {
|
||||
system_name,
|
||||
system_version,
|
||||
system_kernel_version,
|
||||
system_arch,
|
||||
verge_version: "unknown".into(),
|
||||
running_mode: "NotRunning".to_string(),
|
||||
is_admin: false,
|
||||
};
|
||||
};
|
||||
let handler = handle::Handle::app_handle();
|
||||
let verge_version = handler.package_info().version.to_string();
|
||||
|
||||
// 使用默认值避免在同步上下文中执行异步操作
|
||||
|
||||
@@ -51,53 +51,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
// 避免在Handle未初始化时崩溃
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "app_handle not initialized, using default path");
|
||||
// 使用可执行文件目录作为备用
|
||||
let exe_path = tauri::utils::platform::current_exe()?;
|
||||
let exe_dir = exe_path
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to get executable directory"))?;
|
||||
|
||||
// 使用系统临时目录 + 应用ID
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") {
|
||||
let path = PathBuf::from(local_app_data).join(APP_ID);
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
let path = PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Application Support")
|
||||
.join(APP_ID);
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
let path = PathBuf::from(home)
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join(APP_ID);
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法获取系统目录,则回退到可执行文件目录
|
||||
let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID);
|
||||
log::warn!(target: "app", "Using fallback data directory: {fallback_dir:?}");
|
||||
return Ok(fallback_dir);
|
||||
}
|
||||
};
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
match app_handle.path().data_dir() {
|
||||
Ok(dir) => Ok(dir.join(APP_ID)),
|
||||
@@ -111,18 +65,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
/// get the resources dir
|
||||
pub fn app_resources_dir() -> Result<PathBuf> {
|
||||
// 避免在Handle未初始化时崩溃
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "app_handle not initialized in app_resources_dir, using fallback");
|
||||
// 使用可执行文件目录作为备用
|
||||
let exe_dir = tauri::utils::platform::current_exe()?
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to get executable directory"))?
|
||||
.to_path_buf();
|
||||
return Ok(exe_dir.join("resources"));
|
||||
}
|
||||
};
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
match app_handle.path().resource_dir() {
|
||||
Ok(dir) => Ok(dir.join("resources")),
|
||||
|
||||
@@ -494,15 +494,7 @@ pub fn init_scheme() -> Result<()> {
|
||||
}
|
||||
|
||||
pub async fn startup_script() -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"app_handle not available for startup script execution"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let script_path = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pub async fn set_public_dns(dns_server: String) {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
let app_handle = match handle::Handle::app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::error!(target: "app", "app_handle not available for DNS configuration");
|
||||
@@ -50,7 +50,7 @@ pub async fn set_public_dns(dns_server: String) {
|
||||
pub async fn restore_public_dns() {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
let app_handle = match handle::Handle::app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::error!(target: "app", "app_handle not available for DNS restoration");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
@@ -18,8 +17,8 @@ pub mod ui;
|
||||
pub mod window;
|
||||
pub mod window_script;
|
||||
|
||||
pub fn resolve_setup_handle(app_handle: AppHandle) {
|
||||
init_handle(app_handle);
|
||||
pub fn resolve_setup_handle() {
|
||||
init_handle();
|
||||
}
|
||||
|
||||
pub fn resolve_setup_sync() {
|
||||
@@ -118,9 +117,9 @@ pub async fn resolve_reset_async() {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_handle(app_handle: AppHandle) {
|
||||
pub fn init_handle() {
|
||||
logging!(info, Type::Setup, true, "Initializing app handle...");
|
||||
handle::Handle::global().init(app_handle);
|
||||
handle::Handle::global().init();
|
||||
}
|
||||
|
||||
pub(super) fn init_scheme() {
|
||||
|
||||
@@ -2,7 +2,7 @@ use tauri::WebviewWindow;
|
||||
|
||||
use crate::{
|
||||
core::handle,
|
||||
logging, logging_error,
|
||||
logging_error,
|
||||
utils::{
|
||||
logging::Type,
|
||||
resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
|
||||
@@ -18,18 +18,10 @@ const MINIMAL_HEIGHT: f64 = 520.0;
|
||||
|
||||
/// 构建新的 WebView 窗口
|
||||
pub fn build_new_window() -> Result<WebviewWindow, String> {
|
||||
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Window,
|
||||
true,
|
||||
"无法获取app_handle,窗口创建失败"
|
||||
);
|
||||
"无法获取app_handle".to_string()
|
||||
})?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
app_handle,
|
||||
"main", /* the unique window label */
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ fn should_handle_window_operation() -> bool {
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(*last_operation);
|
||||
|
||||
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
|
||||
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
|
||||
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||
|
||||
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
|
||||
@@ -76,7 +76,7 @@ fn should_handle_window_operation() -> bool {
|
||||
log::info!(target: "app", "[防抖] 窗口操作被允许执行");
|
||||
true
|
||||
} else {
|
||||
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
|
||||
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
|
||||
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||
false
|
||||
}
|
||||
@@ -117,9 +117,8 @@ impl WindowManager {
|
||||
|
||||
/// 获取主窗口实例
|
||||
pub fn get_main_window() -> Option<WebviewWindow<Wry>> {
|
||||
handle::Handle::global()
|
||||
.app_handle()
|
||||
.and_then(|app| app.get_webview_window("main"))
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle.get_webview_window("main")
|
||||
}
|
||||
|
||||
/// 智能显示主窗口
|
||||
|
||||
@@ -49,8 +49,11 @@
|
||||
"security": {
|
||||
"capabilities": ["desktop-capability", "migrated"],
|
||||
"assetProtocol": {
|
||||
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
|
||||
"enable": true
|
||||
"enable": true,
|
||||
"scope": {
|
||||
"allow": ["**"],
|
||||
"requireLiteralLeadingDot": false
|
||||
}
|
||||
},
|
||||
"csp": null
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import dayjs from "dayjs";
|
||||
import { t } from "i18next";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { deleteConnection } from "@/services/cmds";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { closeConnections } from "tauri-plugin-mihomo-api";
|
||||
|
||||
export interface ConnectionDetailRef {
|
||||
open: (detail: IConnectionsItem) => void;
|
||||
@@ -99,7 +99,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
|
||||
];
|
||||
|
||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||
const onDelete = useLockFn(async () => closeConnections(data.id));
|
||||
|
||||
return (
|
||||
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { deleteConnection } from "@/services/cmds";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { closeConnections } from "tauri-plugin-mihomo-api";
|
||||
|
||||
const Tag = styled("span")(({ theme }) => ({
|
||||
fontSize: "10px",
|
||||
@@ -34,7 +34,7 @@ export const ConnectionItem = (props: Props) => {
|
||||
|
||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
||||
|
||||
const onDelete = useLockFn(async () => deleteConnection(id));
|
||||
const onDelete = useLockFn(async () => closeConnections(id));
|
||||
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,8 +10,8 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { closeAllConnections } from "@/services/cmds";
|
||||
import { patchClashMode } from "@/services/cmds";
|
||||
import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
||||
|
||||
export const ClashModeCard = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -13,25 +13,23 @@ import {
|
||||
useTheme,
|
||||
PaletteColor,
|
||||
Grid,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { useRef, useCallback, memo, useMemo } from "react";
|
||||
import { useRef, memo, useMemo } from "react";
|
||||
import { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
|
||||
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { isDebugEnabled, gc } from "@/services/cmds";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
import {
|
||||
EnhancedCanvasTrafficGraph,
|
||||
type EnhancedCanvasTrafficGraphRef,
|
||||
} from "./enhanced-canvas-traffic-graph";
|
||||
import { useConnectionData } from "@/hooks/use-connection-data";
|
||||
import { useTrafficData } from "@/hooks/use-traffic-data";
|
||||
import { useMemoryData } from "@/hooks/use-memory-data";
|
||||
|
||||
interface StatCardProps {
|
||||
icon: ReactNode;
|
||||
@@ -149,51 +147,33 @@ export const EnhancedTrafficStats = () => {
|
||||
const trafficRef = useRef<EnhancedCanvasTrafficGraphRef>(null);
|
||||
const pageVisible = useVisibility();
|
||||
|
||||
// 使用AppDataProvider
|
||||
const { connections } = useAppData();
|
||||
const {
|
||||
response: { data: traffic },
|
||||
} = useTrafficData();
|
||||
|
||||
// 使用增强版的统一流量数据Hook
|
||||
const { traffic, memory, isLoading, isDataFresh, hasValidData } =
|
||||
useTrafficDataEnhanced();
|
||||
const {
|
||||
response: { data: memory },
|
||||
} = useMemoryData();
|
||||
|
||||
const {
|
||||
response: { data: connections },
|
||||
} = useConnectionData();
|
||||
|
||||
// 是否显示流量图表
|
||||
const trafficGraph = verge?.traffic_graph ?? true;
|
||||
|
||||
// 检查是否支持调试
|
||||
// TODO: merge this hook with layout-traffic.tsx
|
||||
const { data: isDebug } = useSWR(
|
||||
`clash-verge-rev-internal://isDebugEnabled`,
|
||||
() => isDebugEnabled(),
|
||||
{
|
||||
// default value before is fetched
|
||||
fallbackData: false,
|
||||
},
|
||||
);
|
||||
|
||||
// Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点
|
||||
|
||||
// 执行垃圾回收
|
||||
const handleGarbageCollection = useCallback(async () => {
|
||||
if (isDebug) {
|
||||
try {
|
||||
await gc();
|
||||
console.log("[Debug] 垃圾回收已执行");
|
||||
} catch (err) {
|
||||
console.error("[Debug] 垃圾回收失败:", err);
|
||||
}
|
||||
}
|
||||
}, [isDebug]);
|
||||
|
||||
// 使用useMemo计算解析后的流量数据
|
||||
const parsedData = useMemo(() => {
|
||||
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
|
||||
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
|
||||
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
|
||||
const [up, upUnit] = parseTraffic(traffic?.up || 0);
|
||||
const [down, downUnit] = parseTraffic(traffic?.down || 0);
|
||||
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
|
||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(
|
||||
connections.uploadTotal,
|
||||
connections?.uploadTotal,
|
||||
);
|
||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(
|
||||
connections.downloadTotal,
|
||||
connections?.downloadTotal,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -207,7 +187,7 @@ export const EnhancedTrafficStats = () => {
|
||||
uploadTotalUnit,
|
||||
downloadTotal,
|
||||
downloadTotalUnit,
|
||||
connectionsCount: connections.count,
|
||||
connectionsCount: connections?.connections.length,
|
||||
};
|
||||
}, [traffic, memory, connections]);
|
||||
|
||||
@@ -229,33 +209,10 @@ export const EnhancedTrafficStats = () => {
|
||||
>
|
||||
<div style={{ height: "100%", position: "relative" }}>
|
||||
<EnhancedCanvasTrafficGraph ref={trafficRef} />
|
||||
{isDebug && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "2px",
|
||||
left: "2px",
|
||||
zIndex: 10,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
color: "white",
|
||||
fontSize: "8px",
|
||||
padding: "2px 4px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
DEBUG: {trafficRef.current ? "图表已初始化" : "图表未初始化"}
|
||||
<br />
|
||||
状态: {isDataFresh ? "active" : "inactive"}
|
||||
<br />
|
||||
数据新鲜度: {traffic?.is_fresh ? "Fresh" : "Stale"}
|
||||
<br />
|
||||
{new Date().toISOString().slice(11, 19)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
|
||||
}, [trafficGraph, pageVisible, theme.palette.divider]);
|
||||
|
||||
// 使用useMemo计算统计卡片配置
|
||||
const statCards = useMemo(
|
||||
@@ -301,10 +258,10 @@ export const EnhancedTrafficStats = () => {
|
||||
value: parsedData.inuse,
|
||||
unit: parsedData.inuseUnit,
|
||||
color: "error" as const,
|
||||
onClick: isDebug ? handleGarbageCollection : undefined,
|
||||
onClick: undefined,
|
||||
},
|
||||
],
|
||||
[t, parsedData, isDebug, handleGarbageCollection],
|
||||
[t, parsedData],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -321,28 +278,11 @@ export const EnhancedTrafficStats = () => {
|
||||
</Grid>
|
||||
)}
|
||||
{/* 统计卡片区域 */}
|
||||
{statCards.map((card, index) => (
|
||||
<Grid key={index} size={4}>
|
||||
{statCards.map((card, _index) => (
|
||||
<Grid key={card.title} size={4}>
|
||||
<CompactStatCard {...card} />
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
{/* 数据状态指示器(调试用)*/}
|
||||
{isDebug && (
|
||||
<Grid size={12}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 1,
|
||||
bgcolor: "action.hover",
|
||||
borderRadius: 1,
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
数据状态: {isDataFresh ? "新鲜" : "过期"} | 有效数据:{" "}
|
||||
{hasValidData ? "是" : "否"} | 加载中: {isLoading ? "是" : "否"}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</TrafficErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -6,34 +6,19 @@ import {
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
|
||||
import { useTrafficData } from "@/hooks/use-traffic-data";
|
||||
import { useMemoryData } from "@/hooks/use-memory-data";
|
||||
|
||||
// setup the traffic
|
||||
export const LayoutTraffic = () => {
|
||||
const { data: isDebug } = useSWR(
|
||||
"clash-verge-rev-internal://isDebugEnabled",
|
||||
() => isDebugEnabled(),
|
||||
{
|
||||
// default value before is fetched
|
||||
fallbackData: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDebug) {
|
||||
console.debug("[Traffic][LayoutTraffic] 组件正在渲染");
|
||||
}
|
||||
const { t } = useTranslation();
|
||||
const { clashInfo } = useClashInfo();
|
||||
const { verge } = useVerge();
|
||||
|
||||
// whether hide traffic graph
|
||||
@@ -42,31 +27,19 @@ export const LayoutTraffic = () => {
|
||||
const trafficRef = useRef<TrafficRef>(null);
|
||||
const pageVisible = useVisibility();
|
||||
|
||||
// 使用增强版的统一流量数据Hook
|
||||
const { traffic, memory } = useTrafficDataEnhanced();
|
||||
|
||||
// 启动流量服务
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
"[Traffic][LayoutTraffic] useEffect 触发,clashInfo:",
|
||||
clashInfo,
|
||||
"pageVisible:",
|
||||
pageVisible,
|
||||
);
|
||||
|
||||
// 简化条件,只要组件挂载就尝试启动服务
|
||||
console.log("[Traffic][LayoutTraffic] 开始启动流量服务");
|
||||
startTrafficService().catch((error) => {
|
||||
console.error("[Traffic][LayoutTraffic] 启动流量服务失败:", error);
|
||||
});
|
||||
}, []); // 移除依赖,只在组件挂载时启动一次
|
||||
const {
|
||||
response: { data: traffic },
|
||||
} = useTrafficData();
|
||||
const {
|
||||
response: { data: memory },
|
||||
} = useMemoryData();
|
||||
|
||||
// 监听数据变化,为图表添加数据点
|
||||
useEffect(() => {
|
||||
if (traffic?.raw && trafficRef.current) {
|
||||
if (trafficRef.current) {
|
||||
trafficRef.current.appendData({
|
||||
up: traffic.raw.up_rate || 0,
|
||||
down: traffic.raw.down_rate || 0,
|
||||
up: traffic?.up || 0,
|
||||
down: traffic?.down || 0,
|
||||
});
|
||||
}
|
||||
}, [traffic]);
|
||||
@@ -75,9 +48,9 @@ export const LayoutTraffic = () => {
|
||||
const displayMemory = verge?.enable_memory_usage ?? true;
|
||||
|
||||
// 使用parseTraffic统一处理转换,保持与首页一致的显示格式
|
||||
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
|
||||
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
|
||||
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
|
||||
const [up, upUnit] = parseTraffic(traffic?.up || 0);
|
||||
const [down, downUnit] = parseTraffic(traffic?.down || 0);
|
||||
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
|
||||
|
||||
const boxStyle: any = {
|
||||
display: "flex",
|
||||
@@ -114,18 +87,16 @@ export const LayoutTraffic = () => {
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={0.75}>
|
||||
<Box
|
||||
title={`${t("Upload Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
|
||||
title={`${t("Upload Speed")}`}
|
||||
{...boxStyle}
|
||||
sx={{
|
||||
...boxStyle.sx,
|
||||
opacity: traffic?.is_fresh ? 1 : 0.6,
|
||||
// opacity: traffic?.is_fresh ? 1 : 0.6,
|
||||
}}
|
||||
>
|
||||
<ArrowUpwardRounded
|
||||
{...iconStyle}
|
||||
color={
|
||||
(traffic?.raw?.up_rate || 0) > 0 ? "secondary" : "disabled"
|
||||
}
|
||||
color={(traffic?.up || 0) > 0 ? "secondary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle} color="secondary">
|
||||
{up}
|
||||
@@ -134,18 +105,16 @@ export const LayoutTraffic = () => {
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
title={`${t("Download Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
|
||||
title={`${t("Download Speed")}`}
|
||||
{...boxStyle}
|
||||
sx={{
|
||||
...boxStyle.sx,
|
||||
opacity: traffic?.is_fresh ? 1 : 0.6,
|
||||
// opacity: traffic?.is_fresh ? 1 : 0.6,
|
||||
}}
|
||||
>
|
||||
<ArrowDownwardRounded
|
||||
{...iconStyle}
|
||||
color={
|
||||
(traffic?.raw?.down_rate || 0) > 0 ? "primary" : "disabled"
|
||||
}
|
||||
color={(traffic?.down || 0) > 0 ? "primary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle} color="primary">
|
||||
{down}
|
||||
@@ -155,15 +124,15 @@ export const LayoutTraffic = () => {
|
||||
|
||||
{displayMemory && (
|
||||
<Box
|
||||
title={`${t(isDebug ? "Memory Cleanup" : "Memory Usage")} ${memory?.is_fresh ? "" : "(Stale)"} ${"usage_percent" in (memory?.formatted || {}) && memory.formatted.usage_percent ? `(${memory.formatted.usage_percent.toFixed(1)}%)` : ""}`}
|
||||
title={`${t("Memory Usage")} `}
|
||||
{...boxStyle}
|
||||
sx={{
|
||||
cursor: isDebug ? "pointer" : "auto",
|
||||
opacity: memory?.is_fresh ? 1 : 0.6,
|
||||
cursor: "auto",
|
||||
// opacity: memory?.is_fresh ? 1 : 0.6,
|
||||
}}
|
||||
color={isDebug ? "success.main" : "disabled"}
|
||||
color={"disabled"}
|
||||
onClick={async () => {
|
||||
isDebug && (await gc());
|
||||
// isDebug && (await gc());
|
||||
}}
|
||||
>
|
||||
<MemoryRounded {...iconStyle} />
|
||||
|
||||
@@ -22,9 +22,9 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { proxyProviderUpdate } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { updateProxyProvider } from "tauri-plugin-mihomo-api";
|
||||
|
||||
// 定义代理提供者类型
|
||||
interface ProxyProviderItem {
|
||||
@@ -74,7 +74,7 @@ export const ProviderButton = () => {
|
||||
// 设置更新状态
|
||||
setUpdating((prev) => ({ ...prev, [name]: true }));
|
||||
|
||||
await proxyProviderUpdate(name);
|
||||
await updateProxyProvider(name);
|
||||
|
||||
// 刷新数据
|
||||
await refreshProxy();
|
||||
@@ -115,7 +115,7 @@ export const ProviderButton = () => {
|
||||
// 改为串行逐个更新所有provider
|
||||
for (const name of allProviders) {
|
||||
try {
|
||||
await proxyProviderUpdate(name);
|
||||
await updateProxyProvider(name);
|
||||
// 每个更新完成后更新状态
|
||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||
} catch (err) {
|
||||
@@ -177,161 +177,164 @@ export const ProviderButton = () => {
|
||||
|
||||
<DialogContent>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(proxyProviders || {}).map(([key, item]) => {
|
||||
const provider = item as ProxyProviderItem;
|
||||
const time = dayjs(provider.updatedAt);
|
||||
const isUpdating = updating[key];
|
||||
{Object.entries(proxyProviders || {})
|
||||
.sort()
|
||||
.map(([key, item]) => {
|
||||
const provider = item as ProxyProviderItem;
|
||||
const time = dayjs(provider.updatedAt);
|
||||
const isUpdating = updating[key];
|
||||
|
||||
// 订阅信息
|
||||
const sub = provider.subscriptionInfo;
|
||||
const hasSubInfo = !!sub;
|
||||
const upload = sub?.Upload || 0;
|
||||
const download = sub?.Download || 0;
|
||||
const total = sub?.Total || 0;
|
||||
const expire = sub?.Expire || 0;
|
||||
// 订阅信息
|
||||
const sub = provider.subscriptionInfo;
|
||||
const hasSubInfo = !!sub;
|
||||
const upload = sub?.Upload || 0;
|
||||
const download = sub?.Download || 0;
|
||||
const total = sub?.Total || 0;
|
||||
const expire = sub?.Expire || 0;
|
||||
|
||||
// 流量使用进度
|
||||
const progress =
|
||||
total > 0
|
||||
? Math.min(
|
||||
Math.round(((download + upload) * 100) / total) + 1,
|
||||
100,
|
||||
)
|
||||
: 0;
|
||||
// 流量使用进度
|
||||
const progress =
|
||||
total > 0
|
||||
? Math.min(
|
||||
Math.round(((download + upload) * 100) / total) + 1,
|
||||
100,
|
||||
)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
sx={[
|
||||
{
|
||||
p: 0,
|
||||
mb: "8px",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
({ palette: { mode, primary } }) => {
|
||||
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
||||
const hoverColor =
|
||||
mode === "light"
|
||||
? alpha(primary.main, 0.1)
|
||||
: alpha(primary.main, 0.2);
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
sx={[
|
||||
{
|
||||
p: 0,
|
||||
mb: "8px",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
({ palette: { mode, primary } }) => {
|
||||
const bgcolor =
|
||||
mode === "light" ? "#ffffff" : "#24252f";
|
||||
const hoverColor =
|
||||
mode === "light"
|
||||
? alpha(primary.main, 0.1)
|
||||
: alpha(primary.main, 0.2);
|
||||
|
||||
return {
|
||||
backgroundColor: bgcolor,
|
||||
"&:hover": {
|
||||
backgroundColor: hoverColor,
|
||||
},
|
||||
};
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ px: 2, py: 1 }}
|
||||
primary={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
noWrap
|
||||
title={key}
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||
<TypeBox component="span">
|
||||
{provider.proxies.length}
|
||||
</TypeBox>
|
||||
<TypeBox component="span">
|
||||
{provider.vehicleType}
|
||||
</TypeBox>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
>
|
||||
<small>{t("Update At")}: </small>
|
||||
{time.fromNow()}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
{/* 订阅信息 */}
|
||||
{hasSubInfo && (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span title={t("Used / Total") as string}>
|
||||
{parseTraffic(upload + download)} /{" "}
|
||||
{parseTraffic(total)}
|
||||
</span>
|
||||
<span title={t("Expire Time") as string}>
|
||||
{parseExpire(expire)}
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
{/* 进度条 */}
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
opacity: total > 0 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
return {
|
||||
backgroundColor: bgcolor,
|
||||
"&:hover": {
|
||||
backgroundColor: hoverColor,
|
||||
},
|
||||
};
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
updateProvider(key);
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
<ListItemText
|
||||
sx={{ px: 2, py: 1 }}
|
||||
primary={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
noWrap
|
||||
title={key}
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||
<TypeBox component="span">
|
||||
{provider.proxies.length}
|
||||
</TypeBox>
|
||||
<TypeBox component="span">
|
||||
{provider.vehicleType}
|
||||
</TypeBox>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
>
|
||||
<small>{t("Update At")}: </small>
|
||||
{time.fromNow()}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
{/* 订阅信息 */}
|
||||
{hasSubInfo && (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span title={t("Used / Total") as string}>
|
||||
{parseTraffic(upload + download)} /{" "}
|
||||
{parseTraffic(total)}
|
||||
</span>
|
||||
<span title={t("Expire Time") as string}>
|
||||
{parseExpire(expire)}
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
{/* 进度条 */}
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
opacity: total > 0 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box
|
||||
sx={{
|
||||
animation: isUpdating
|
||||
? "spin 1s linear infinite"
|
||||
: "none",
|
||||
"@keyframes spin": {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" },
|
||||
},
|
||||
width: 40,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
title={t("Update Provider") as string}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
updateProvider(key);
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
sx={{
|
||||
animation: isUpdating
|
||||
? "spin 1s linear infinite"
|
||||
: "none",
|
||||
"@keyframes spin": {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" },
|
||||
},
|
||||
}}
|
||||
title={t("Update Provider") as string}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
|
||||
@@ -37,11 +37,11 @@ import useSWR from "swr";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import {
|
||||
closeAllConnections,
|
||||
getProxies,
|
||||
calcuProxies,
|
||||
updateProxyAndSync,
|
||||
updateProxyChainConfigInRuntime,
|
||||
} from "@/services/cmds";
|
||||
import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
||||
|
||||
interface ProxyChainItem {
|
||||
id: string;
|
||||
@@ -204,7 +204,7 @@ export const ProxyChain = ({
|
||||
// 获取当前代理信息以检查连接状态
|
||||
const { data: currentProxies, mutate: mutateProxies } = useSWR(
|
||||
"getProxies",
|
||||
getProxies,
|
||||
calcuProxies,
|
||||
{
|
||||
revalidateOnFocus: true,
|
||||
revalidateIfStale: true,
|
||||
|
||||
@@ -3,29 +3,21 @@ import {
|
||||
Snackbar,
|
||||
Alert,
|
||||
Chip,
|
||||
Stack,
|
||||
Typography,
|
||||
IconButton,
|
||||
Collapse,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import { ArchiveOutlined, ExpandMoreRounded } from "@mui/icons-material";
|
||||
import { ExpandMoreRounded } from "@mui/icons-material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useRef, useState, useEffect, useCallback } from "react";
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
||||
|
||||
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import {
|
||||
providerHealthCheck,
|
||||
getGroupProxyDelays,
|
||||
updateProxyChainConfigInRuntime,
|
||||
} from "@/services/cmds";
|
||||
import { updateProxyChainConfigInRuntime } from "@/services/cmds";
|
||||
import delayManager from "@/services/delay";
|
||||
|
||||
import { BaseEmpty } from "../base";
|
||||
@@ -34,6 +26,7 @@ import { ScrollTopButton } from "../layout/scroll-top-button";
|
||||
import { ProxyChain } from "./proxy-chain";
|
||||
import { ProxyRender } from "./proxy-render";
|
||||
import { useRenderList } from "./use-render-list";
|
||||
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
|
||||
|
||||
interface Props {
|
||||
mode: string;
|
||||
@@ -154,15 +147,14 @@ export const ProxyGroups = (props: Props) => {
|
||||
|
||||
// 添加和清理滚动事件监听器
|
||||
useEffect(() => {
|
||||
const currentScroller = scrollerRef.current;
|
||||
if (currentScroller) {
|
||||
currentScroller.addEventListener("scroll", handleScroll, {
|
||||
passive: true,
|
||||
});
|
||||
return () => {
|
||||
currentScroller.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}
|
||||
if (!scrollerRef.current) return;
|
||||
scrollerRef.current.addEventListener("scroll", handleScroll, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
scrollerRef.current?.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [handleScroll]);
|
||||
|
||||
// 滚动到顶部
|
||||
@@ -274,7 +266,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
if (providers.size) {
|
||||
console.log(`[ProxyGroups] 发现提供者,数量: ${providers.size}`);
|
||||
Promise.allSettled(
|
||||
[...providers].map((p) => providerHealthCheck(p)),
|
||||
[...providers].map((p) => healthcheckProxyProvider(p)),
|
||||
).then(() => {
|
||||
console.log(`[ProxyGroups] 提供者健康检查完成`);
|
||||
onProxies();
|
||||
@@ -290,7 +282,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
try {
|
||||
await Promise.race([
|
||||
delayManager.checkListDelay(names, groupName, timeout),
|
||||
getGroupProxyDelays(groupName, url, timeout).then((result) => {
|
||||
delayGroup(groupName, url, timeout).then((result) => {
|
||||
console.log(
|
||||
`[ProxyGroups] getGroupProxyDelays返回结果数量:`,
|
||||
Object.keys(result || {}).length,
|
||||
|
||||
@@ -20,9 +20,9 @@ import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { ruleProviderUpdate } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { updateRuleProvider } from "tauri-plugin-mihomo-api";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
|
||||
// 定义规则提供者类型
|
||||
interface RuleProviderItem {
|
||||
@@ -60,7 +60,7 @@ export const ProviderButton = () => {
|
||||
// 设置更新状态
|
||||
setUpdating((prev) => ({ ...prev, [name]: true }));
|
||||
|
||||
await ruleProviderUpdate(name);
|
||||
await updateRuleProvider(name);
|
||||
|
||||
// 刷新数据
|
||||
await refreshRules();
|
||||
@@ -101,7 +101,7 @@ export const ProviderButton = () => {
|
||||
// 改为串行逐个更新所有provider
|
||||
for (const name of allProviders) {
|
||||
try {
|
||||
await ruleProviderUpdate(name);
|
||||
await updateRuleProvider(name);
|
||||
// 每个更新完成后更新状态
|
||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||
} catch (err) {
|
||||
@@ -160,112 +160,117 @@ export const ProviderButton = () => {
|
||||
|
||||
<DialogContent>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(ruleProviders || {}).map(([key, item]) => {
|
||||
const provider = item as RuleProviderItem;
|
||||
const time = dayjs(provider.updatedAt);
|
||||
const isUpdating = updating[key];
|
||||
{Object.entries(ruleProviders || {})
|
||||
.sort()
|
||||
.map(([key, item]) => {
|
||||
const provider = item as RuleProviderItem;
|
||||
const time = dayjs(provider.updatedAt);
|
||||
const isUpdating = updating[key];
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
sx={[
|
||||
{
|
||||
p: 0,
|
||||
mb: "8px",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
({ palette: { mode, primary } }) => {
|
||||
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
||||
const hoverColor =
|
||||
mode === "light"
|
||||
? alpha(primary.main, 0.1)
|
||||
: alpha(primary.main, 0.2);
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
sx={[
|
||||
{
|
||||
p: 0,
|
||||
mb: "8px",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
({ palette: { mode, primary } }) => {
|
||||
const bgcolor =
|
||||
mode === "light" ? "#ffffff" : "#24252f";
|
||||
const hoverColor =
|
||||
mode === "light"
|
||||
? alpha(primary.main, 0.1)
|
||||
: alpha(primary.main, 0.2);
|
||||
|
||||
return {
|
||||
backgroundColor: bgcolor,
|
||||
"&:hover": {
|
||||
backgroundColor: hoverColor,
|
||||
borderColor: alpha(primary.main, 0.3),
|
||||
},
|
||||
};
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ px: 2, py: 1 }}
|
||||
primary={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
noWrap
|
||||
title={key}
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||
<TypeBox component="span">
|
||||
{provider.ruleCount}
|
||||
</TypeBox>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
>
|
||||
<small>{t("Update At")}: </small>
|
||||
{time.fromNow()}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<TypeBox component="span">
|
||||
{provider.vehicleType}
|
||||
</TypeBox>
|
||||
<TypeBox component="span">{provider.behavior}</TypeBox>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
return {
|
||||
backgroundColor: bgcolor,
|
||||
"&:hover": {
|
||||
backgroundColor: hoverColor,
|
||||
borderColor: alpha(primary.main, 0.3),
|
||||
},
|
||||
};
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={() => updateProvider(key)}
|
||||
disabled={isUpdating}
|
||||
<ListItemText
|
||||
sx={{ px: 2, py: 1 }}
|
||||
primary={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
noWrap
|
||||
title={key}
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||
<TypeBox component="span">
|
||||
{provider.ruleCount}
|
||||
</TypeBox>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
>
|
||||
<small>{t("Update At")}: </small>
|
||||
{time.fromNow()}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<TypeBox component="span">
|
||||
{provider.vehicleType}
|
||||
</TypeBox>
|
||||
<TypeBox component="span">
|
||||
{provider.behavior}
|
||||
</TypeBox>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box
|
||||
sx={{
|
||||
animation: isUpdating
|
||||
? "spin 1s linear infinite"
|
||||
: "none",
|
||||
"@keyframes spin": {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" },
|
||||
},
|
||||
width: 40,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
title={t("Update Provider") as string}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={() => updateProvider(key)}
|
||||
disabled={isUpdating}
|
||||
sx={{
|
||||
animation: isUpdating
|
||||
? "spin 1s linear infinite"
|
||||
: "none",
|
||||
"@keyframes spin": {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" },
|
||||
},
|
||||
}}
|
||||
title={t("Update Provider") as string}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
|
||||
@@ -19,12 +19,8 @@ import { mutate } from "swr";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { changeClashCore, restartCore } from "@/services/cmds";
|
||||
import {
|
||||
closeAllConnections,
|
||||
upgradeCore,
|
||||
forceRefreshClashConfig,
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { closeAllConnections, upgradeCore } from "tauri-plugin-mihomo-api";
|
||||
|
||||
const VALID_CORE = [
|
||||
{ name: "Mihomo", core: "verge-mihomo", chip: "Release Version" },
|
||||
@@ -64,8 +60,6 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
mutateVerge();
|
||||
setTimeout(async () => {
|
||||
// 核心切换后强制刷新配置缓存
|
||||
await forceRefreshClashConfig();
|
||||
mutate("getClashConfig");
|
||||
mutate("getVersion");
|
||||
setChangingCore(null);
|
||||
|
||||
@@ -11,13 +11,7 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
@@ -27,7 +21,6 @@ import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { EditorViewer } from "@/components/profile/editor-viewer";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { getClashConfig } from "@/services/cmds";
|
||||
import {
|
||||
getAutotemProxy,
|
||||
getNetworkInterfacesInfo,
|
||||
@@ -37,6 +30,7 @@ import {
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { getBaseConfig } from "tauri-plugin-mihomo-api";
|
||||
|
||||
const DEFAULT_PAC = `function FindProxyForURL(url, host) {
|
||||
return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;";
|
||||
@@ -123,26 +117,21 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||
};
|
||||
|
||||
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig, {
|
||||
const { data: clashConfig } = useSWR("getClashConfig", getBaseConfig, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: true,
|
||||
dedupingInterval: 1000,
|
||||
errorRetryInterval: 5000,
|
||||
});
|
||||
|
||||
const [prevMixedPort, setPrevMixedPort] = useState(
|
||||
clashConfig?.["mixed-port"],
|
||||
);
|
||||
const [prevMixedPort, setPrevMixedPort] = useState(clashConfig?.mixedPort);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
clashConfig?.["mixed-port"] &&
|
||||
clashConfig?.["mixed-port"] !== prevMixedPort
|
||||
) {
|
||||
setPrevMixedPort(clashConfig?.["mixed-port"]);
|
||||
resetSystemProxy();
|
||||
}
|
||||
}, [clashConfig?.["mixed-port"]]);
|
||||
// useEffect(() => {
|
||||
// if (clashConfig?.mixedPort && clashConfig.mixedPort !== prevMixedPort) {
|
||||
// setPrevMixedPort(clashConfig.mixedPort);
|
||||
// resetSystemProxy();
|
||||
// }
|
||||
// }, [clashConfig]);
|
||||
|
||||
const resetSystemProxy = async () => {
|
||||
try {
|
||||
@@ -180,7 +169,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
if (isPacMode) {
|
||||
const host = value.proxy_host || "127.0.0.1";
|
||||
const port = verge?.verge_mixed_port || clashConfig["mixed-port"] || 7897;
|
||||
const port = verge?.verge_mixed_port || clashConfig.mixedPort || 7897;
|
||||
return `${host}:${port}`;
|
||||
} else {
|
||||
return systemProxyAddress;
|
||||
@@ -332,7 +321,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
if (pacContent) {
|
||||
pacContent = pacContent.replace(/%proxy_host%/g, value.proxy_host);
|
||||
// 将 mixed-port 转换为字符串
|
||||
const mixedPortStr = (clashConfig?.["mixed-port"] || "").toString();
|
||||
const mixedPortStr = (clashConfig?.mixedPort || "").toString();
|
||||
pacContent = pacContent.replace(/%mixed-port%/g, mixedPortStr);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { invoke_uwp_tool } from "@/services/cmds";
|
||||
import { updateGeoData } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
@@ -23,6 +22,8 @@ import { GuardState } from "./mods/guard-state";
|
||||
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
|
||||
import { SettingItem, SettingList } from "./mods/setting-comp";
|
||||
import { WebUIViewer } from "./mods/web-ui-viewer";
|
||||
import { updateGeo } from "tauri-plugin-mihomo-api";
|
||||
import { useClashLog } from "@/services/states";
|
||||
|
||||
const isWIN = getSystem() === "windows";
|
||||
|
||||
@@ -35,6 +36,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
|
||||
const { clash, version, mutateClash, patchClash } = useClash();
|
||||
const { verge, patchVerge } = useVerge();
|
||||
const [clashLog, setClashLog] = useClashLog();
|
||||
|
||||
const {
|
||||
ipv6,
|
||||
@@ -64,7 +66,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
};
|
||||
const onUpdateGeo = async () => {
|
||||
try {
|
||||
await updateGeoData();
|
||||
await updateGeo();
|
||||
showNotice("success", t("GeoData Updated"));
|
||||
} catch (err: any) {
|
||||
showNotice("error", err?.response.data.message || err.toString());
|
||||
@@ -186,7 +188,10 @@ const SettingClash = ({ onError }: Props) => {
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => e.target.value}
|
||||
onChange={(e) => onChangeData({ "log-level": e })}
|
||||
onGuard={(e) => patchClash({ "log-level": e })}
|
||||
onGuard={(e) => {
|
||||
setClashLog((pre: any) => ({ ...pre, logLevel: e }));
|
||||
return patchClash({ "log-level": e });
|
||||
}}
|
||||
>
|
||||
<Select size="small" sx={{ width: 100, "> div": { py: "7.5px" } }}>
|
||||
<MenuItem value="debug">Debug</MenuItem>
|
||||
|
||||
+4
-10
@@ -1,13 +1,12 @@
|
||||
import { useLockFn } from "ahooks";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
import { getVersion } from "@/services/cmds";
|
||||
import {
|
||||
getClashInfo,
|
||||
patchClashConfig,
|
||||
getRuntimeConfig,
|
||||
forceRefreshClashConfig,
|
||||
} from "@/services/cmds";
|
||||
import { getVersion } from "tauri-plugin-mihomo-api";
|
||||
|
||||
export const useClash = () => {
|
||||
const { data: clash, mutate: mutateClash } = useSWR(
|
||||
@@ -25,11 +24,9 @@ export const useClash = () => {
|
||||
mutateClash();
|
||||
});
|
||||
|
||||
const version = versionData?.premium
|
||||
? `${versionData.version} Premium`
|
||||
: versionData?.meta
|
||||
? `${versionData.version} Mihomo`
|
||||
: versionData?.version || "-";
|
||||
const version = versionData?.meta
|
||||
? `${versionData.version} Mihomo`
|
||||
: versionData?.version || "-";
|
||||
|
||||
return {
|
||||
clash,
|
||||
@@ -123,10 +120,7 @@ export const useClashInfo = () => {
|
||||
|
||||
await patchClashConfig(patch);
|
||||
mutateInfo();
|
||||
// 配置修改后强制刷新缓存
|
||||
await forceRefreshClashConfig();
|
||||
mutate("getClashConfig");
|
||||
// IPC调用不需要刷新axios实例
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { mutate } from "swr";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
||||
|
||||
export const initConnData: IConnections = {
|
||||
uploadTotal: 0,
|
||||
downloadTotal: 0,
|
||||
connections: [],
|
||||
};
|
||||
|
||||
export const useConnectionData = () => {
|
||||
const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now());
|
||||
const subscriptKey = `getClashConnection-${date}`;
|
||||
|
||||
const ws = useRef<MihomoWebSocket | null>(null);
|
||||
const wsFirstConnection = useRef<boolean>(true);
|
||||
const listenerRef = useRef<() => void | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
|
||||
const response = useSWRSubscription<IConnections, any, string | null>(
|
||||
subscriptKey,
|
||||
(_key, { next }) => {
|
||||
const connect = () =>
|
||||
MihomoWebSocket.connect_connections()
|
||||
.then((ws_) => {
|
||||
ws.current = ws_;
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
|
||||
listenerRef.current = ws_.addListener(async (msg) => {
|
||||
if (msg.type === "Text") {
|
||||
if (msg.data.startsWith("Websocket error")) {
|
||||
next(msg.data);
|
||||
await ws.current?.close();
|
||||
ws.current = null;
|
||||
timeoutRef.current = setTimeout(
|
||||
async () => await connect(),
|
||||
500,
|
||||
);
|
||||
} else {
|
||||
const data = JSON.parse(msg.data) as IConnections;
|
||||
next(null, (old = initConnData) => {
|
||||
const oldConn = old.connections;
|
||||
const maxLen = data.connections?.length;
|
||||
const connections: IConnectionsItem[] = [];
|
||||
const rest = (data.connections || []).filter((each) => {
|
||||
const index = oldConn.findIndex((o) => o.id === each.id);
|
||||
if (index >= 0 && index < maxLen) {
|
||||
const old = oldConn[index];
|
||||
each.curUpload = each.upload - old.upload;
|
||||
each.curDownload = each.download - old.download;
|
||||
connections[index] = each;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
for (let i = 0; i < maxLen; ++i) {
|
||||
if (!connections[i] && rest.length > 0) {
|
||||
connections[i] = rest.shift()!;
|
||||
connections[i].curUpload = 0;
|
||||
connections[i].curDownload = 0;
|
||||
}
|
||||
}
|
||||
return { ...data, connections };
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((_) => {
|
||||
if (!ws.current) {
|
||||
timeoutRef.current = setTimeout(async () => await connect(), 500);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
wsFirstConnection.current ||
|
||||
(ws.current && !wsFirstConnection.current)
|
||||
) {
|
||||
wsFirstConnection.current = false;
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
ws.current = null;
|
||||
}
|
||||
connect();
|
||||
}
|
||||
|
||||
return () => {
|
||||
ws.current?.close();
|
||||
listenerRef.current?.();
|
||||
listenerRef.current = null;
|
||||
};
|
||||
},
|
||||
{
|
||||
fallbackData: initConnData,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenRefreshWebsocket = listen(
|
||||
"verge://refresh-websocket",
|
||||
async () => {
|
||||
await ws.current?.close();
|
||||
setDate(Date.now());
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlistenRefreshWebsocket.then((fn) => fn());
|
||||
};
|
||||
}, [setDate]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate(`$sub$${subscriptKey}`);
|
||||
}, [date, subscriptKey]);
|
||||
|
||||
const refreshGetClashConnection = () => {
|
||||
setDate(Date.now());
|
||||
};
|
||||
|
||||
return { response, refreshGetClashConnection };
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
// import { getClashLogs } from "@/services/cmds";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import dayjs from "dayjs";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { mutate } from "swr";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
||||
import { useClashLog } from "@/services/states";
|
||||
import { getClashLogs } from "@/services/cmds";
|
||||
|
||||
const MAX_LOG_NUM = 1000;
|
||||
|
||||
export const useLogData = () => {
|
||||
// const [enableLog] = useEnableLog();
|
||||
// const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
|
||||
const [clashLog] = useClashLog();
|
||||
const enableLog = clashLog.enable;
|
||||
const logLevel = clashLog.logLevel;
|
||||
|
||||
const [date, setDate] = useLocalStorage("mihomo_logs_date", Date.now());
|
||||
const subscriptKey = enableLog ? `getClashLog-${date}` : null;
|
||||
|
||||
const ws = useRef<MihomoWebSocket | null>(null);
|
||||
const wsFirstConnection = useRef<boolean>(true);
|
||||
const listenerRef = useRef<() => void | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
|
||||
const response = useSWRSubscription<ILogItem[], any, string | null>(
|
||||
subscriptKey,
|
||||
(_key, { next }) => {
|
||||
const connect = () =>
|
||||
MihomoWebSocket.connect_logs(logLevel)
|
||||
.then(async (ws_) => {
|
||||
ws.current = ws_;
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
|
||||
const logs = await getClashLogs();
|
||||
let filterLogs: ILogItem[] = [];
|
||||
switch (logLevel) {
|
||||
case "debug":
|
||||
filterLogs = logs.filter((i) =>
|
||||
["debug", "info", "warning", "error"].includes(i.type),
|
||||
);
|
||||
break;
|
||||
case "info":
|
||||
filterLogs = logs.filter((i) =>
|
||||
["info", "warning", "error"].includes(i.type),
|
||||
);
|
||||
break;
|
||||
case "warning":
|
||||
filterLogs = logs.filter((i) =>
|
||||
["warning", "error"].includes(i.type),
|
||||
);
|
||||
break;
|
||||
case "error":
|
||||
filterLogs = logs.filter((i) => i.type === "error");
|
||||
break;
|
||||
case "silent":
|
||||
filterLogs = [];
|
||||
break;
|
||||
default:
|
||||
filterLogs = logs;
|
||||
break;
|
||||
}
|
||||
next(null, filterLogs);
|
||||
|
||||
const buffer: ILogItem[] = [];
|
||||
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const flush = () => {
|
||||
if (buffer.length > 0) {
|
||||
next(null, (l) => {
|
||||
let newList = [...(l ?? []), ...buffer.splice(0)];
|
||||
if (newList.length > MAX_LOG_NUM) {
|
||||
newList = newList.slice(
|
||||
-Math.min(MAX_LOG_NUM, newList.length),
|
||||
);
|
||||
}
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
flushTimer = null;
|
||||
};
|
||||
listenerRef.current = ws_.addListener(async (msg) => {
|
||||
if (msg.type === "Text") {
|
||||
if (msg.data.startsWith("Websocket error")) {
|
||||
next(msg.data);
|
||||
await ws.current?.close();
|
||||
ws.current = null;
|
||||
timeoutRef.current = setTimeout(
|
||||
async () => await connect(),
|
||||
500,
|
||||
);
|
||||
} else {
|
||||
const data = JSON.parse(msg.data) as ILogItem;
|
||||
data.time = dayjs().format("MM-DD HH:mm:ss");
|
||||
buffer.push(data);
|
||||
|
||||
// flush data
|
||||
if (!flushTimer) {
|
||||
flushTimer = setTimeout(flush, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((_) => {
|
||||
if (!ws.current) {
|
||||
timeoutRef.current = setTimeout(async () => await connect(), 500);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
wsFirstConnection.current ||
|
||||
(ws.current && !wsFirstConnection.current)
|
||||
) {
|
||||
wsFirstConnection.current = false;
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
ws.current = null;
|
||||
}
|
||||
connect();
|
||||
}
|
||||
|
||||
return () => {
|
||||
ws.current?.close();
|
||||
listenerRef.current?.();
|
||||
listenerRef.current = null;
|
||||
};
|
||||
},
|
||||
{
|
||||
fallbackData: [],
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenRefreshWebsocket = listen(
|
||||
"verge://refresh-websocket",
|
||||
async () => {
|
||||
await ws.current?.close();
|
||||
setDate(Date.now());
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlistenRefreshWebsocket.then((fn) => fn());
|
||||
};
|
||||
}, [setDate]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate(`$sub$${subscriptKey}`);
|
||||
}, [date, subscriptKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logLevel) return;
|
||||
ws.current?.close();
|
||||
setDate(Date.now());
|
||||
}, [logLevel]);
|
||||
|
||||
const refreshGetClashLog = (clear = false) => {
|
||||
if (clear) {
|
||||
mutate(`$sub$${subscriptKey}`, []);
|
||||
} else {
|
||||
setDate(Date.now());
|
||||
}
|
||||
};
|
||||
|
||||
return { response, refreshGetClashLog };
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { mutate } from "swr";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
||||
|
||||
export interface IMemoryUsageItem {
|
||||
inuse: number;
|
||||
oslimit?: number;
|
||||
}
|
||||
|
||||
export const useMemoryData = () => {
|
||||
const [date, setDate] = useLocalStorage("mihomo_memory_date", Date.now());
|
||||
const subscriptKey = `getClashMemory-${date}`;
|
||||
|
||||
const ws = useRef<MihomoWebSocket | null>(null);
|
||||
const wsFirstConnection = useRef<boolean>(true);
|
||||
const listenerRef = useRef<() => void | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
|
||||
const response = useSWRSubscription<IMemoryUsageItem, any, string | null>(
|
||||
subscriptKey,
|
||||
(_key, { next }) => {
|
||||
const connect = () =>
|
||||
MihomoWebSocket.connect_memory()
|
||||
.then((ws_) => {
|
||||
ws.current = ws_;
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
|
||||
listenerRef.current = ws_.addListener(async (msg) => {
|
||||
if (msg.type === "Text") {
|
||||
if (msg.data.startsWith("Websocket error")) {
|
||||
next(msg.data, { inuse: 0 });
|
||||
await ws.current?.close();
|
||||
ws.current = null;
|
||||
timeoutRef.current = setTimeout(
|
||||
async () => await connect(),
|
||||
500,
|
||||
);
|
||||
} else {
|
||||
const data = JSON.parse(msg.data) as IMemoryUsageItem;
|
||||
next(null, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((_) => {
|
||||
if (!ws.current) {
|
||||
timeoutRef.current = setTimeout(async () => await connect(), 500);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
wsFirstConnection.current ||
|
||||
(ws.current && !wsFirstConnection.current)
|
||||
) {
|
||||
wsFirstConnection.current = false;
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
ws.current = null;
|
||||
}
|
||||
connect();
|
||||
}
|
||||
|
||||
return () => {
|
||||
ws.current?.close();
|
||||
listenerRef.current?.();
|
||||
listenerRef.current = null;
|
||||
};
|
||||
},
|
||||
{
|
||||
fallbackData: { inuse: 0 },
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenRefreshWebsocket = listen(
|
||||
"verge://refresh-websocket",
|
||||
async () => {
|
||||
await ws.current?.close();
|
||||
setDate(Date.now());
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlistenRefreshWebsocket.then((fn) => fn());
|
||||
};
|
||||
}, [setDate]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate(`$sub$${subscriptKey}`);
|
||||
}, [date, subscriptKey]);
|
||||
|
||||
const refreshGetClashMemory = () => {
|
||||
setDate(Date.now());
|
||||
};
|
||||
|
||||
return { response, refreshGetClashMemory };
|
||||
};
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
getProfiles,
|
||||
patchProfile,
|
||||
patchProfilesConfig,
|
||||
forceRefreshProxies,
|
||||
} from "@/services/cmds";
|
||||
import { getProxies, updateProxy } from "@/services/cmds";
|
||||
import { calcuProxies } from "@/services/cmds";
|
||||
import { selectNodeForGroup } from "tauri-plugin-mihomo-api";
|
||||
|
||||
export const useProfiles = () => {
|
||||
const {
|
||||
@@ -72,7 +72,7 @@ export const useProfiles = () => {
|
||||
console.log("[ActivateSelected] 开始处理代理选择");
|
||||
|
||||
const [proxiesData, profileData] = await Promise.all([
|
||||
getProxies(),
|
||||
calcuProxies(),
|
||||
getProfiles(),
|
||||
]);
|
||||
|
||||
@@ -124,7 +124,7 @@ export const useProfiles = () => {
|
||||
`[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${targetProxy}`,
|
||||
);
|
||||
hasChange = true;
|
||||
updateProxy(name, targetProxy);
|
||||
selectNodeForGroup(name, targetProxy);
|
||||
}
|
||||
|
||||
newSelected.push({ name, now: targetProxy || now });
|
||||
@@ -141,11 +141,8 @@ export const useProfiles = () => {
|
||||
await patchProfile(profileData.current!, { selected: newSelected });
|
||||
console.log("[ActivateSelected] 代理选择配置保存成功");
|
||||
|
||||
// 切换节点后强制刷新后端缓存
|
||||
await forceRefreshProxies();
|
||||
|
||||
setTimeout(() => {
|
||||
mutate("getProxies", getProxies());
|
||||
mutate("getProxies", calcuProxies());
|
||||
}, 100);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
|
||||
@@ -3,22 +3,20 @@ import { useCallback, useMemo } from "react";
|
||||
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { updateProxyAndSync, syncTrayProxySelection } from "@/services/cmds";
|
||||
import {
|
||||
updateProxy,
|
||||
updateProxyAndSync,
|
||||
forceRefreshProxies,
|
||||
syncTrayProxySelection,
|
||||
closeConnections,
|
||||
getConnections,
|
||||
deleteConnection,
|
||||
} from "@/services/cmds";
|
||||
selectNodeForGroup,
|
||||
} from "tauri-plugin-mihomo-api";
|
||||
|
||||
// 缓存连接清理
|
||||
const cleanupConnections = async (previousProxy: string) => {
|
||||
try {
|
||||
const { connections } = await getConnections();
|
||||
const cleanupPromises = connections
|
||||
const cleanupPromises = (connections ?? [])
|
||||
.filter((conn) => conn.chains.includes(previousProxy))
|
||||
.map((conn) => deleteConnection(conn.id));
|
||||
.map((conn) => closeConnections(conn.id));
|
||||
|
||||
if (cleanupPromises.length > 0) {
|
||||
await Promise.allSettled(cleanupPromises);
|
||||
@@ -98,8 +96,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
||||
);
|
||||
|
||||
try {
|
||||
await updateProxy(groupName, proxyName);
|
||||
await forceRefreshProxies();
|
||||
await selectNodeForGroup(groupName, proxyName);
|
||||
await syncTrayProxySelection();
|
||||
onSuccess?.();
|
||||
console.log(
|
||||
|
||||
@@ -3,7 +3,7 @@ import useSWR, { mutate } from "swr";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { getAutotemProxy } from "@/services/cmds";
|
||||
import { closeAllConnections } from "@/services/cmds";
|
||||
import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
||||
|
||||
// 系统代理状态检测统一逻辑
|
||||
export const useSystemProxyState = () => {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { TrafficRef } from "@/components/layout/traffic-graph";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { mutate } from "swr";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
||||
|
||||
export const useTrafficData = () => {
|
||||
const [date, setDate] = useLocalStorage("mihomo_traffic_date", Date.now());
|
||||
const subscriptKey = `getClashTraffic-${date}`;
|
||||
|
||||
const trafficRef = useRef<TrafficRef>(null);
|
||||
const ws = useRef<MihomoWebSocket | null>(null);
|
||||
const wsFirstConnection = useRef<boolean>(true);
|
||||
const listenerRef = useRef<() => void | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
|
||||
const response = useSWRSubscription<ITrafficItem, any, string | null>(
|
||||
subscriptKey,
|
||||
(_key, { next }) => {
|
||||
const connect = async () => {
|
||||
MihomoWebSocket.connect_traffic()
|
||||
.then(async (ws_) => {
|
||||
ws.current = ws_;
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
|
||||
listenerRef.current = ws_.addListener(async (msg) => {
|
||||
if (msg.type === "Text") {
|
||||
if (msg.data.startsWith("Websocket error")) {
|
||||
next(msg.data, { up: 0, down: 0 });
|
||||
await ws.current?.close();
|
||||
ws.current = null;
|
||||
timeoutRef.current = setTimeout(
|
||||
async () => await connect(),
|
||||
500,
|
||||
);
|
||||
} else {
|
||||
const data = JSON.parse(msg.data) as ITrafficItem;
|
||||
trafficRef.current?.appendData(data);
|
||||
next(null, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((_) => {
|
||||
if (!ws.current) {
|
||||
timeoutRef.current = setTimeout(async () => await connect(), 500);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
wsFirstConnection.current ||
|
||||
(ws.current && !wsFirstConnection.current)
|
||||
) {
|
||||
wsFirstConnection.current = false;
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
ws.current = null;
|
||||
}
|
||||
connect();
|
||||
}
|
||||
|
||||
return () => {
|
||||
ws.current?.close();
|
||||
listenerRef.current?.();
|
||||
listenerRef.current = null;
|
||||
};
|
||||
},
|
||||
{
|
||||
fallbackData: { up: 0, down: 0 },
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenRefreshWebsocket = listen(
|
||||
"verge://refresh-websocket",
|
||||
async () => {
|
||||
await ws.current?.close();
|
||||
setDate(Date.now());
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlistenRefreshWebsocket.then((fn) => fn());
|
||||
};
|
||||
}, [setDate]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate(`$sub$${subscriptKey}`);
|
||||
}, [date, subscriptKey]);
|
||||
|
||||
const refreshGetClashTraffic = () => {
|
||||
setDate(Date.now());
|
||||
};
|
||||
|
||||
return { response, refreshGetClashTraffic };
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { getSystemMonitorOverviewSafe } from "@/services/cmds";
|
||||
import { useTrafficData } from "./use-traffic-data";
|
||||
|
||||
// 增强的流量数据点接口
|
||||
export interface ITrafficDataPoint {
|
||||
@@ -183,6 +182,9 @@ let lastValidData: ISystemMonitorOverview | null = null;
|
||||
export const useTrafficMonitorEnhanced = () => {
|
||||
const { clashInfo } = useClashInfo();
|
||||
const pageVisible = useVisibility();
|
||||
const {
|
||||
response: { data: traffic },
|
||||
} = useTrafficData();
|
||||
|
||||
// 初始化采样器
|
||||
if (!globalSampler) {
|
||||
@@ -230,69 +232,87 @@ export const useTrafficMonitorEnhanced = () => {
|
||||
refCounter.onCountChange(handleCountChange);
|
||||
}, []);
|
||||
|
||||
// 只有在有引用时才启用SWR
|
||||
const shouldFetch = clashInfo && pageVisible && refCounter.getCount() > 0;
|
||||
const monitorData = useRef<ISystemMonitorOverview | null>(null);
|
||||
useEffect(() => {
|
||||
if (globalSampler) {
|
||||
// 添加到采样器
|
||||
const timestamp = Date.now();
|
||||
const dataPoint: ITrafficDataPoint = {
|
||||
up: traffic?.up || 0,
|
||||
down: traffic?.down || 0,
|
||||
timestamp,
|
||||
name: new Date(timestamp).toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}),
|
||||
};
|
||||
globalSampler.addDataPoint(dataPoint);
|
||||
triggerUpdate();
|
||||
}
|
||||
}, [traffic, triggerUpdate]);
|
||||
|
||||
const { data: monitorData, error } = useSWR<ISystemMonitorOverview>(
|
||||
shouldFetch ? "getSystemMonitorOverviewSafe" : null,
|
||||
getSystemMonitorOverviewSafe,
|
||||
{
|
||||
refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新
|
||||
keepPreviousData: true,
|
||||
onSuccess: (data) => {
|
||||
// console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data);
|
||||
// const { data: monitorData, error } = useSWR<ISystemMonitorOverview>(
|
||||
// shouldFetch ? "getSystemMonitorOverviewSafe" : null,
|
||||
// getSystemMonitorOverviewSafe,
|
||||
// {
|
||||
// refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新
|
||||
// keepPreviousData: true,
|
||||
// onSuccess: (data) => {
|
||||
// // console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data);
|
||||
|
||||
if (data?.traffic?.raw && globalSampler) {
|
||||
// 保存最后有效数据
|
||||
lastValidData = data;
|
||||
// if (data?.traffic?.raw && globalSampler) {
|
||||
// // 保存最后有效数据
|
||||
// lastValidData = data;
|
||||
|
||||
// 添加到采样器
|
||||
const timestamp = Date.now();
|
||||
const dataPoint: ITrafficDataPoint = {
|
||||
up: data.traffic.raw.up_rate || 0,
|
||||
down: data.traffic.raw.down_rate || 0,
|
||||
timestamp,
|
||||
name: new Date(timestamp).toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}),
|
||||
};
|
||||
// // 添加到采样器
|
||||
// const timestamp = Date.now();
|
||||
// const dataPoint: ITrafficDataPoint = {
|
||||
// up: data.traffic.raw.up_rate || 0,
|
||||
// down: data.traffic.raw.down_rate || 0,
|
||||
// timestamp,
|
||||
// name: new Date(timestamp).toLocaleTimeString("en-US", {
|
||||
// hour12: false,
|
||||
// hour: "2-digit",
|
||||
// minute: "2-digit",
|
||||
// second: "2-digit",
|
||||
// }),
|
||||
// };
|
||||
|
||||
globalSampler.addDataPoint(dataPoint);
|
||||
triggerUpdate();
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(
|
||||
"[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:",
|
||||
{
|
||||
message: error?.message || "未知错误",
|
||||
stack: error?.stack || "无堆栈信息",
|
||||
},
|
||||
);
|
||||
// 网络错误时不清空数据,继续使用最后有效值
|
||||
// 但是添加一个错误标记的数据点(流量为0)
|
||||
if (globalSampler) {
|
||||
const timestamp = Date.now();
|
||||
const errorPoint: ITrafficDataPoint = {
|
||||
up: 0,
|
||||
down: 0,
|
||||
timestamp,
|
||||
name: new Date(timestamp).toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}),
|
||||
};
|
||||
globalSampler.addDataPoint(errorPoint);
|
||||
triggerUpdate();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
// globalSampler.addDataPoint(dataPoint);
|
||||
// triggerUpdate();
|
||||
// }
|
||||
// },
|
||||
// onError: (error) => {
|
||||
// console.error(
|
||||
// "[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:",
|
||||
// {
|
||||
// message: error?.message || "未知错误",
|
||||
// stack: error?.stack || "无堆栈信息",
|
||||
// },
|
||||
// );
|
||||
// // 网络错误时不清空数据,继续使用最后有效值
|
||||
// // 但是添加一个错误标记的数据点(流量为0)
|
||||
// if (globalSampler) {
|
||||
// const timestamp = Date.now();
|
||||
// const errorPoint: ITrafficDataPoint = {
|
||||
// up: 0,
|
||||
// down: 0,
|
||||
// timestamp,
|
||||
// name: new Date(timestamp).toLocaleTimeString("en-US", {
|
||||
// hour12: false,
|
||||
// hour: "2-digit",
|
||||
// minute: "2-digit",
|
||||
// second: "2-digit",
|
||||
// }),
|
||||
// };
|
||||
// globalSampler.addDataPoint(errorPoint);
|
||||
// triggerUpdate();
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
// );
|
||||
|
||||
// 获取指定时间范围的数据
|
||||
const getDataForTimeRange = useCallback(
|
||||
@@ -324,7 +344,7 @@ export const useTrafficMonitorEnhanced = () => {
|
||||
}, []);
|
||||
|
||||
// 构建返回的监控数据,优先使用当前数据,fallback到最后有效数据
|
||||
const currentData = monitorData || lastValidData;
|
||||
const currentData = monitorData.current || lastValidData;
|
||||
const trafficMonitorData = {
|
||||
traffic: currentData?.traffic || {
|
||||
raw: { up: 0, down: 0, up_rate: 0, down_rate: 0 },
|
||||
@@ -355,8 +375,7 @@ export const useTrafficMonitorEnhanced = () => {
|
||||
},
|
||||
|
||||
// 状态信息
|
||||
isLoading: !currentData && !error,
|
||||
error,
|
||||
isLoading: !currentData,
|
||||
isDataFresh: currentData?.traffic?.is_fresh || false,
|
||||
hasValidData: !!lastValidData,
|
||||
|
||||
@@ -370,14 +389,13 @@ export const useTrafficMonitorEnhanced = () => {
|
||||
* 轻量级流量数据Hook
|
||||
*/
|
||||
export const useTrafficDataEnhanced = () => {
|
||||
const { monitorData, isLoading, error, isDataFresh, hasValidData } =
|
||||
const { monitorData, isLoading, isDataFresh, hasValidData } =
|
||||
useTrafficMonitorEnhanced();
|
||||
|
||||
return {
|
||||
traffic: monitorData.traffic,
|
||||
memory: monitorData.memory,
|
||||
isLoading,
|
||||
error,
|
||||
isDataFresh,
|
||||
hasValidData,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ThemeModeProvider,
|
||||
UpdateStateProvider,
|
||||
} from "./services/states";
|
||||
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
||||
|
||||
const mainElementId = "root";
|
||||
const container = document.getElementById(mainElementId);
|
||||
@@ -90,3 +91,9 @@ window.addEventListener("error", (event) => {
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
console.error("[main.tsx] 未处理的Promise拒绝:", event.reason);
|
||||
});
|
||||
|
||||
// 页面关闭/刷新事件
|
||||
window.addEventListener("beforeunload", async () => {
|
||||
// 强制清理所有 WebSocket 实例, 防止内存泄漏
|
||||
await MihomoWebSocket.cleanupAll();
|
||||
});
|
||||
|
||||
+18
-12
@@ -19,8 +19,7 @@ import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { getAxios } from "@/services/api";
|
||||
import { forceRefreshClashConfig } from "@/services/cmds";
|
||||
import { useThemeMode, useEnableLog } from "@/services/states";
|
||||
import { useClashLog, useThemeMode } from "@/services/states";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
import { routers } from "./_routers";
|
||||
@@ -33,13 +32,16 @@ import { useListen } from "@/hooks/use-listen";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { initGlobalLogService } from "@/services/global-log-service";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { NoticeManager } from "@/components/base/NoticeManager";
|
||||
import { LogLevel } from "@/hooks/use-log-data";
|
||||
import { useMemoryData } from "@/hooks/use-memory-data";
|
||||
import { useTrafficData } from "@/hooks/use-traffic-data";
|
||||
import { useConnectionData } from "@/hooks/use-connection-data";
|
||||
import { useLogData } from "@/hooks/use-log-data-new";
|
||||
|
||||
const appWindow = getCurrentWebviewWindow();
|
||||
export const portableFlag = false;
|
||||
@@ -157,14 +159,20 @@ const handleNoticeMessage = (
|
||||
};
|
||||
|
||||
const Layout = () => {
|
||||
useTrafficData();
|
||||
useMemoryData();
|
||||
useConnectionData();
|
||||
useLogData();
|
||||
const mode = useThemeMode();
|
||||
const isDark = mode === "light" ? false : true;
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useCustomTheme();
|
||||
const { verge } = useVerge();
|
||||
const { clashInfo } = useClashInfo();
|
||||
const [enableLog] = useEnableLog();
|
||||
const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
|
||||
const [clashLog] = useClashLog();
|
||||
const enableLog = clashLog.enable;
|
||||
const logLevel = clashLog.logLevel;
|
||||
// const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
|
||||
const { language, start_page } = verge ?? {};
|
||||
const { switchLanguage } = useI18n();
|
||||
const navigate = useNavigate();
|
||||
@@ -193,19 +201,17 @@ const Layout = () => {
|
||||
);
|
||||
|
||||
// 初始化全局日志服务
|
||||
useEffect(() => {
|
||||
if (clashInfo) {
|
||||
initGlobalLogService(enableLog, logLevel);
|
||||
}
|
||||
}, [clashInfo, enableLog, logLevel]);
|
||||
// useEffect(() => {
|
||||
// if (clashInfo) {
|
||||
// initGlobalLogService(enableLog, logLevel);
|
||||
// }
|
||||
// }, [clashInfo, enableLog, logLevel]);
|
||||
|
||||
// 设置监听器
|
||||
useEffect(() => {
|
||||
const listeners = [
|
||||
addListener("verge://refresh-clash-config", async () => {
|
||||
await getAxios(true);
|
||||
// 后端配置变更事件触发,强制刷新配置缓存
|
||||
await forceRefreshClashConfig();
|
||||
mutate("getProxies");
|
||||
mutate("getVersion");
|
||||
mutate("getClashConfig");
|
||||
|
||||
+17
-16
@@ -20,10 +20,10 @@ import {
|
||||
import { ConnectionItem } from "@/components/connection/connection-item";
|
||||
import { ConnectionTable } from "@/components/connection/connection-table";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { closeAllConnections } from "@/services/cmds";
|
||||
import { useConnectionSetting } from "@/services/states";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
||||
import { useConnectionData } from "@/hooks/use-connection-data";
|
||||
|
||||
const initConn: IConnections = {
|
||||
uploadTotal: 0,
|
||||
@@ -39,8 +39,9 @@ const ConnectionsPage = () => {
|
||||
const [match, setMatch] = useState(() => (_: string) => true);
|
||||
const [curOrderOpt, setOrderOpt] = useState("Default");
|
||||
|
||||
// 使用全局数据
|
||||
const { connections } = useAppData();
|
||||
const {
|
||||
response: { data: connections },
|
||||
} = useConnectionData();
|
||||
|
||||
const [setting, setSetting] = useConnectionSetting();
|
||||
|
||||
@@ -72,30 +73,30 @@ const ConnectionsPage = () => {
|
||||
if (isPaused) {
|
||||
return (
|
||||
frozenData ?? {
|
||||
uploadTotal: connections.uploadTotal,
|
||||
downloadTotal: connections.downloadTotal,
|
||||
connections: connections.data,
|
||||
uploadTotal: connections?.uploadTotal,
|
||||
downloadTotal: connections?.downloadTotal,
|
||||
connections: connections?.connections,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
uploadTotal: connections.uploadTotal,
|
||||
downloadTotal: connections.downloadTotal,
|
||||
connections: connections.data,
|
||||
uploadTotal: connections?.uploadTotal,
|
||||
downloadTotal: connections?.downloadTotal,
|
||||
connections: connections?.connections,
|
||||
};
|
||||
}, [isPaused, frozenData, connections, pageVisible]);
|
||||
|
||||
const [filterConn] = useMemo(() => {
|
||||
const orderFunc = orderOpts[curOrderOpt];
|
||||
let conns = displayData.connections.filter((conn) => {
|
||||
let conns = displayData.connections?.filter((conn) => {
|
||||
const { host, destinationIP, process } = conn.metadata;
|
||||
return (
|
||||
match(host || "") || match(destinationIP || "") || match(process || "")
|
||||
);
|
||||
});
|
||||
|
||||
if (orderFunc) conns = orderFunc(conns);
|
||||
if (orderFunc) conns = orderFunc(conns ?? []);
|
||||
|
||||
return [conns];
|
||||
}, [displayData, match, curOrderOpt, orderOpts]);
|
||||
@@ -112,9 +113,9 @@ const ConnectionsPage = () => {
|
||||
setIsPaused((prev) => {
|
||||
if (!prev) {
|
||||
setFrozenData({
|
||||
uploadTotal: connections.uploadTotal,
|
||||
downloadTotal: connections.downloadTotal,
|
||||
connections: connections.data,
|
||||
uploadTotal: connections?.uploadTotal ?? 0,
|
||||
downloadTotal: connections?.downloadTotal ?? 0,
|
||||
connections: connections?.connections ?? [],
|
||||
});
|
||||
} else {
|
||||
setFrozenData(null);
|
||||
@@ -206,7 +207,7 @@ const ConnectionsPage = () => {
|
||||
<BaseSearchBox onSearch={handleSearch} />
|
||||
</Box>
|
||||
|
||||
{filterConn.length === 0 ? (
|
||||
{!filterConn || filterConn.length === 0 ? (
|
||||
<BaseEmpty />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable
|
||||
|
||||
+25
-26
@@ -3,7 +3,6 @@ import {
|
||||
PauseCircleOutlineRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { Box, Button, IconButton, MenuItem } from "@mui/material";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
@@ -14,26 +13,22 @@ import { SearchState } from "@/components/base/base-search-box";
|
||||
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
||||
import LogItem from "@/components/log/log-item";
|
||||
import { LogLevel } from "@/hooks/use-log-data";
|
||||
import {
|
||||
useGlobalLogData,
|
||||
clearGlobalLogs,
|
||||
changeLogLevel,
|
||||
toggleLogEnabled,
|
||||
} from "@/services/global-log-service";
|
||||
import { useEnableLog } from "@/services/states";
|
||||
|
||||
// 后端通过 /logs?level={level} 进行筛选,前端不再需要手动筛选日志级别
|
||||
import { toggleLogEnabled } from "@/services/global-log-service";
|
||||
import { useClashLog } from "@/services/states";
|
||||
import { useLogData } from "@/hooks/use-log-data-new";
|
||||
|
||||
const LogPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [enableLog, setEnableLog] = useEnableLog();
|
||||
const [logLevel, setLogLevel] = useLocalStorage<LogLevel>(
|
||||
"log:log-level",
|
||||
"info",
|
||||
);
|
||||
const [clashLog, setClashLog] = useClashLog();
|
||||
const enableLog = clashLog.enable;
|
||||
const logState = clashLog.logFilter;
|
||||
|
||||
const [match, setMatch] = useState(() => (_: string) => true);
|
||||
const logData = useGlobalLogData("all");
|
||||
const [searchState, setSearchState] = useState<SearchState>();
|
||||
const {
|
||||
response: { data: logData },
|
||||
refreshGetClashLog,
|
||||
} = useLogData();
|
||||
|
||||
const filterLogs = useMemo(() => {
|
||||
if (!logData || logData.length === 0) {
|
||||
@@ -49,18 +44,21 @@ const LogPage = () => {
|
||||
|
||||
const matchesSearch = match(searchText);
|
||||
|
||||
return matchesSearch;
|
||||
return (
|
||||
(logState == "all" ? true : data.type.includes(logState)) &&
|
||||
matchesSearch
|
||||
);
|
||||
});
|
||||
}, [logData, match]);
|
||||
}, [logData, logState, match]);
|
||||
|
||||
const handleLogLevelChange = (newLevel: LogLevel) => {
|
||||
setLogLevel(newLevel);
|
||||
changeLogLevel(newLevel);
|
||||
const handleLogLevelChange = (newLevel: string) => {
|
||||
setClashLog((pre: any) => ({ ...pre, logFilter: newLevel }));
|
||||
// changeLogLevel(newLevel);
|
||||
};
|
||||
|
||||
const handleToggleLog = async () => {
|
||||
await toggleLogEnabled();
|
||||
setEnableLog(!enableLog);
|
||||
setClashLog((pre: any) => ({ ...pre, enable: !enableLog }));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -92,7 +90,8 @@ const LogPage = () => {
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
clearGlobalLogs();
|
||||
refreshGetClashLog(true);
|
||||
// clearGlobalLogs();
|
||||
}}
|
||||
>
|
||||
{t("Clear")}
|
||||
@@ -111,13 +110,13 @@ const LogPage = () => {
|
||||
}}
|
||||
>
|
||||
<BaseStyledSelect
|
||||
value={logLevel}
|
||||
value={logState}
|
||||
onChange={(e) => handleLogLevelChange(e.target.value as LogLevel)}
|
||||
>
|
||||
<MenuItem value="all">ALL</MenuItem>
|
||||
<MenuItem value="debug">DEBUG</MenuItem>
|
||||
<MenuItem value="info">INFO</MenuItem>
|
||||
<MenuItem value="warning">WARNING</MenuItem>
|
||||
<MenuItem value="inf">INFO</MenuItem>
|
||||
<MenuItem value="warn">WARN</MenuItem>
|
||||
<MenuItem value="error">ERROR</MenuItem>
|
||||
</BaseStyledSelect>
|
||||
<BaseSearchBox
|
||||
|
||||
@@ -42,7 +42,6 @@ import { ConfigViewer } from "@/components/setting/mods/config-viewer";
|
||||
import { useListen } from "@/hooks/use-listen";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import {
|
||||
closeAllConnections,
|
||||
createProfile,
|
||||
deleteProfile,
|
||||
enhanceProfiles,
|
||||
@@ -55,6 +54,7 @@ import {
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useSetLoadingCache, useThemeMode } from "@/services/states";
|
||||
import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
||||
|
||||
// 记录profile切换状态
|
||||
const debugProfileSwitch = (action: string, profile: string, extra?: any) => {
|
||||
|
||||
@@ -9,12 +9,11 @@ import { ProviderButton } from "@/components/proxy/provider-button";
|
||||
import { ProxyGroups } from "@/components/proxy/proxy-groups";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import {
|
||||
closeAllConnections,
|
||||
getClashConfig,
|
||||
getRuntimeProxyChainConfig,
|
||||
patchClashMode,
|
||||
updateProxyChainConfigInRuntime,
|
||||
} from "@/services/cmds";
|
||||
import { closeAllConnections, getBaseConfig } from "tauri-plugin-mihomo-api";
|
||||
|
||||
const ProxyPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -33,7 +32,7 @@ const ProxyPage = () => {
|
||||
|
||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||
"getClashConfig",
|
||||
getClashConfig,
|
||||
getBaseConfig,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: true,
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ const RulesPage = () => {
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
</Box>
|
||||
|
||||
{filteredRules.length > 0 ? (
|
||||
{filteredRules && filteredRules.length > 0 ? (
|
||||
<>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
|
||||
+109
-169
@@ -12,19 +12,17 @@ import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import {
|
||||
forceRefreshProxies,
|
||||
getAppUptime,
|
||||
getClashConfig,
|
||||
getConnections,
|
||||
getMemoryData,
|
||||
getProxies,
|
||||
getProxyProviders,
|
||||
getRuleProviders,
|
||||
getRules,
|
||||
calcuProxies,
|
||||
calcuProxyProviders,
|
||||
getRunningMode,
|
||||
getSystemProxy,
|
||||
getTrafficData,
|
||||
} from "@/services/cmds";
|
||||
import {
|
||||
getBaseConfig,
|
||||
getRuleProviders,
|
||||
getRules,
|
||||
} from "tauri-plugin-mihomo-api";
|
||||
|
||||
// 连接速度计算接口
|
||||
interface ConnectionSpeedData {
|
||||
@@ -49,14 +47,14 @@ interface AppDataContextType {
|
||||
uptime: number;
|
||||
proxyProviders: any;
|
||||
ruleProviders: any;
|
||||
connections: {
|
||||
data: ConnectionWithSpeed[];
|
||||
count: number;
|
||||
uploadTotal: number;
|
||||
downloadTotal: number;
|
||||
};
|
||||
traffic: { up: number; down: number };
|
||||
memory: { inuse: number };
|
||||
// connections: {
|
||||
// data: ConnectionWithSpeed[];
|
||||
// count: number;
|
||||
// uploadTotal: number;
|
||||
// downloadTotal: number;
|
||||
// };
|
||||
// traffic: { up: number; down: number };
|
||||
// memory: { inuse: number };
|
||||
systemProxyAddress: string;
|
||||
|
||||
refreshProxy: () => Promise<any>;
|
||||
@@ -130,7 +128,7 @@ export const AppDataProvider = ({
|
||||
// 基础数据 - 中频率更新 (5秒)
|
||||
const { data: proxiesData, mutate: refreshProxy } = useSWR(
|
||||
"getProxies",
|
||||
getProxies,
|
||||
calcuProxies,
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
revalidateOnFocus: true,
|
||||
@@ -165,21 +163,6 @@ export const AppDataProvider = ({
|
||||
|
||||
lastProfileId = newProfileId;
|
||||
lastUpdateTime = now;
|
||||
|
||||
setTimeout(() => {
|
||||
// 先执行 forceRefreshProxies,完成后稍延迟再刷新前端数据,避免页面一直 loading
|
||||
forceRefreshProxies()
|
||||
.catch((e) =>
|
||||
console.warn("[AppDataProvider] forceRefreshProxies 失败:", e),
|
||||
)
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
refreshProxy().catch((e) =>
|
||||
console.warn("[AppDataProvider] 普通刷新也失败:", e),
|
||||
);
|
||||
}, 200); // 200ms 延迟,保证后端缓存已清理
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// 监听Clash配置刷新事件(enhance操作等)
|
||||
@@ -194,18 +177,6 @@ export const AppDataProvider = ({
|
||||
try {
|
||||
console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存");
|
||||
|
||||
// 添加超时保护
|
||||
const refreshPromise = Promise.race([
|
||||
forceRefreshProxies(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("forceRefreshProxies timeout")),
|
||||
8000,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
await refreshPromise;
|
||||
await refreshProxy();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -236,29 +207,6 @@ export const AppDataProvider = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 监听强制代理刷新事件(托盘代理切换立即刷新)
|
||||
const handleForceRefreshProxies = () => {
|
||||
console.log("[AppDataProvider] 强制代理刷新事件");
|
||||
|
||||
// 立即刷新,无延迟,无防抖
|
||||
forceRefreshProxies()
|
||||
.then(() => {
|
||||
console.log("[AppDataProvider] 强制刷新代理缓存完成");
|
||||
// 强制刷新完成后,立即刷新前端显示
|
||||
return refreshProxy();
|
||||
})
|
||||
.then(() => {
|
||||
console.log("[AppDataProvider] 前端代理数据刷新完成");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn("[AppDataProvider] 强制代理刷新失败:", e);
|
||||
// 如果强制刷新失败,尝试普通刷新
|
||||
refreshProxy().catch((e2) =>
|
||||
console.warn("[AppDataProvider] 普通代理刷新也失败:", e2),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 使用 Tauri 事件监听器替代 window 事件监听器
|
||||
const setupTauriListeners = async () => {
|
||||
try {
|
||||
@@ -270,15 +218,10 @@ export const AppDataProvider = ({
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
const unlistenForceRefresh = await listen(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlistenClash();
|
||||
unlistenProxy();
|
||||
unlistenForceRefresh();
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error);
|
||||
@@ -292,10 +235,6 @@ export const AppDataProvider = ({
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
window.addEventListener(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
@@ -306,10 +245,6 @@ export const AppDataProvider = ({
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
window.removeEventListener(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -336,7 +271,7 @@ export const AppDataProvider = ({
|
||||
|
||||
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
|
||||
"getClashConfig",
|
||||
getClashConfig,
|
||||
getBaseConfig,
|
||||
{
|
||||
refreshInterval: 60000, // 60秒刷新间隔,减少频繁请求
|
||||
revalidateOnFocus: false,
|
||||
@@ -348,7 +283,7 @@ export const AppDataProvider = ({
|
||||
// 提供者数据
|
||||
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
|
||||
"getProxyProviders",
|
||||
getProxyProviders,
|
||||
calcuProxyProviders,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
@@ -398,86 +333,93 @@ export const AppDataProvider = ({
|
||||
|
||||
// 高频率更新数据 (2秒)
|
||||
const { data: uptimeData } = useSWR("appUptime", getAppUptime, {
|
||||
refreshInterval: 2000,
|
||||
// TODO: 运行时间
|
||||
refreshInterval: 20000,
|
||||
revalidateOnFocus: false,
|
||||
suspense: false,
|
||||
});
|
||||
|
||||
// 连接数据 - 使用IPC轮询更新并计算速度
|
||||
const {
|
||||
data: connectionsData = {
|
||||
connections: [],
|
||||
uploadTotal: 0,
|
||||
downloadTotal: 0,
|
||||
},
|
||||
} = useSWR(
|
||||
clashInfo && pageVisible ? "getConnections" : null,
|
||||
async () => {
|
||||
const data = await getConnections();
|
||||
const rawConnections: IConnectionsItem[] = data.connections || [];
|
||||
// const {
|
||||
// data: connectionsData = {
|
||||
// connections: [],
|
||||
// uploadTotal: 0,
|
||||
// downloadTotal: 0,
|
||||
// },
|
||||
// } = useSWR(
|
||||
// clashInfo && pageVisible ? "getConnections" : null,
|
||||
// async () => {
|
||||
// const data = await getConnections();
|
||||
// const rawConnections =
|
||||
// data.connections?.map((item) => {
|
||||
// // TODO: transform bigint to number
|
||||
// return { ...item, upload: 0, download: 0 } as IConnectionsItem;
|
||||
// }) || [];
|
||||
|
||||
// 计算带速度的连接数据
|
||||
const connectionsWithSpeed = calculateConnectionSpeeds(rawConnections);
|
||||
// // 计算带速度的连接数据
|
||||
// const connectionsWithSpeed = calculateConnectionSpeeds(rawConnections);
|
||||
|
||||
// 更新上一次数据的引用
|
||||
const currentMap = new Map<string, ConnectionSpeedData>();
|
||||
const now = Date.now();
|
||||
rawConnections.forEach((conn) => {
|
||||
currentMap.set(conn.id, {
|
||||
id: conn.id,
|
||||
upload: conn.upload,
|
||||
download: conn.download,
|
||||
timestamp: now,
|
||||
});
|
||||
});
|
||||
previousConnectionsRef.current = currentMap;
|
||||
// // 更新上一次数据的引用
|
||||
// const currentMap = new Map<string, ConnectionSpeedData>();
|
||||
// const now = Date.now();
|
||||
// rawConnections.forEach((conn) => {
|
||||
// currentMap.set(conn.id, {
|
||||
// id: conn.id,
|
||||
// upload: conn.upload,
|
||||
// download: conn.download,
|
||||
// timestamp: now,
|
||||
// });
|
||||
// });
|
||||
// previousConnectionsRef.current = currentMap;
|
||||
|
||||
return {
|
||||
connections: connectionsWithSpeed,
|
||||
uploadTotal: data.uploadTotal || 0,
|
||||
downloadTotal: data.downloadTotal || 0,
|
||||
};
|
||||
},
|
||||
{
|
||||
refreshInterval: 1000, // 1秒刷新一次
|
||||
fallbackData: { connections: [], uploadTotal: 0, downloadTotal: 0 },
|
||||
keepPreviousData: true,
|
||||
onError: (error) => {
|
||||
console.error("[Connections] IPC 获取数据错误:", error);
|
||||
},
|
||||
},
|
||||
);
|
||||
// return {
|
||||
// connections: connectionsWithSpeed,
|
||||
// uploadTotal: data.uploadTotal || 0,
|
||||
// downloadTotal: data.downloadTotal || 0,
|
||||
// };
|
||||
// },
|
||||
// {
|
||||
// refreshInterval: 1000, // 1秒刷新一次
|
||||
// fallbackData: { connections: [], uploadTotal: 0, downloadTotal: 0 },
|
||||
// keepPreviousData: true,
|
||||
// onError: (error) => {
|
||||
// console.error("[Connections] IPC 获取数据错误:", error);
|
||||
// },
|
||||
// },
|
||||
// );
|
||||
|
||||
// 流量数据 - 使用IPC轮询更新
|
||||
const { data: trafficData = { up: 0, down: 0 } } = useSWR(
|
||||
clashInfo && pageVisible ? "getTrafficData" : null,
|
||||
getTrafficData,
|
||||
{
|
||||
refreshInterval: 1000, // 1秒刷新一次
|
||||
fallbackData: { up: 0, down: 0 },
|
||||
keepPreviousData: true,
|
||||
onSuccess: () => {
|
||||
// console.log("[Traffic][AppDataProvider] IPC 获取到流量数据:", data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("[Traffic][AppDataProvider] IPC 获取数据错误:", error);
|
||||
},
|
||||
},
|
||||
);
|
||||
// const trafficData = { up: 0, down: 0 };
|
||||
// const { data: trafficData = { up: 0, down: 0 } } = useSWR(
|
||||
// clashInfo && pageVisible ? "getTrafficData" : null,
|
||||
// getTrafficData,
|
||||
// {
|
||||
// refreshInterval: 1000, // 1秒刷新一次
|
||||
// fallbackData: { up: 0, down: 0 },
|
||||
// keepPreviousData: true,
|
||||
// onSuccess: () => {
|
||||
// // console.log("[Traffic][AppDataProvider] IPC 获取到流量数据:", data);
|
||||
// },
|
||||
// onError: (error) => {
|
||||
// console.error("[Traffic][AppDataProvider] IPC 获取数据错误:", error);
|
||||
// },
|
||||
// },
|
||||
// );
|
||||
|
||||
// 内存数据 - 使用IPC轮询更新
|
||||
const { data: memoryData = { inuse: 0 } } = useSWR(
|
||||
clashInfo && pageVisible ? "getMemoryData" : null,
|
||||
getMemoryData,
|
||||
{
|
||||
refreshInterval: 2000, // 2秒刷新一次
|
||||
fallbackData: { inuse: 0 },
|
||||
keepPreviousData: true,
|
||||
onError: (error) => {
|
||||
console.error("[Memory] IPC 获取数据错误:", error);
|
||||
},
|
||||
},
|
||||
);
|
||||
// const memoryData = { inuse: 0 };
|
||||
// const { data: memoryData = { inuse: 0 } } = useSWR(
|
||||
// clashInfo && pageVisible ? "getMemoryData" : null,
|
||||
// getMemoryData,
|
||||
// {
|
||||
// refreshInterval: 2000, // 2秒刷新一次
|
||||
// fallbackData: { inuse: 0 },
|
||||
// keepPreviousData: true,
|
||||
// onError: (error) => {
|
||||
// console.error("[Memory] IPC 获取数据错误:", error);
|
||||
// },
|
||||
// },
|
||||
// );
|
||||
|
||||
// 提供统一的刷新方法
|
||||
const refreshAll = React.useCallback(async () => {
|
||||
@@ -510,7 +452,7 @@ export const AppDataProvider = ({
|
||||
// PAC模式:显示我们期望设置的代理地址
|
||||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||||
const proxyPort =
|
||||
verge.verge_mixed_port || clashConfig["mixed-port"] || 7897;
|
||||
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
||||
return `${proxyHost}:${proxyPort}`;
|
||||
} else {
|
||||
// HTTP代理模式:优先使用系统地址,但如果格式不正确则使用期望地址
|
||||
@@ -525,7 +467,7 @@ export const AppDataProvider = ({
|
||||
// 系统地址无效,返回期望的代理地址
|
||||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||||
const proxyPort =
|
||||
verge.verge_mixed_port || clashConfig["mixed-port"] || 7897;
|
||||
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
||||
return `${proxyHost}:${proxyPort}`;
|
||||
}
|
||||
}
|
||||
@@ -535,26 +477,26 @@ export const AppDataProvider = ({
|
||||
// 数据
|
||||
proxies: proxiesData,
|
||||
clashConfig,
|
||||
rules: rulesData || [],
|
||||
rules: rulesData?.rules || [],
|
||||
sysproxy,
|
||||
runningMode,
|
||||
uptime: uptimeData || 0,
|
||||
|
||||
// 提供者数据
|
||||
proxyProviders: proxyProviders || {},
|
||||
ruleProviders: ruleProviders || {},
|
||||
ruleProviders: ruleProviders?.providers || {},
|
||||
|
||||
// 连接数据
|
||||
connections: {
|
||||
data: connectionsData.connections || [],
|
||||
count: connectionsData.connections?.length || 0,
|
||||
uploadTotal: connectionsData.uploadTotal || 0,
|
||||
downloadTotal: connectionsData.downloadTotal || 0,
|
||||
},
|
||||
// connections: {
|
||||
// data: connectionsData.connections || [],
|
||||
// count: connectionsData.connections?.length || 0,
|
||||
// uploadTotal: connectionsData.uploadTotal || 0,
|
||||
// downloadTotal: connectionsData.downloadTotal || 0,
|
||||
// },
|
||||
|
||||
// 实时流量数据
|
||||
traffic: trafficData,
|
||||
memory: memoryData,
|
||||
// traffic: trafficData,
|
||||
// memory: memoryData,
|
||||
|
||||
systemProxyAddress: calculateSystemProxyAddress(),
|
||||
|
||||
@@ -574,9 +516,9 @@ export const AppDataProvider = ({
|
||||
sysproxy,
|
||||
runningMode,
|
||||
uptimeData,
|
||||
connectionsData,
|
||||
trafficData,
|
||||
memoryData,
|
||||
// connectionsData,
|
||||
// trafficData,
|
||||
// memoryData,
|
||||
proxyProviders,
|
||||
ruleProviders,
|
||||
verge,
|
||||
@@ -589,9 +531,7 @@ export const AppDataProvider = ({
|
||||
refreshAll,
|
||||
]);
|
||||
|
||||
return (
|
||||
<AppDataContext.Provider value={value}>{children}</AppDataContext.Provider>
|
||||
);
|
||||
return <AppDataContext value={value}>{children}</AppDataContext>;
|
||||
};
|
||||
|
||||
// 自定义Hook访问全局数据
|
||||
|
||||
+36
-253
@@ -1,6 +1,8 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getProxies, getProxyProviders } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export async function copyClashEnv() {
|
||||
return invoke<void>("copy_clash_env");
|
||||
@@ -87,7 +89,7 @@ export async function getRuntimeLogs() {
|
||||
return invoke<Record<string, [string, string][]>>("get_runtime_logs");
|
||||
}
|
||||
|
||||
export async function getRuntimeProxyChainConfig(proxyChainExitNode: String) {
|
||||
export async function getRuntimeProxyChainConfig(proxyChainExitNode: string) {
|
||||
return invoke<string>("get_runtime_proxy_chain_config", {
|
||||
proxyChainExitNode,
|
||||
});
|
||||
@@ -107,55 +109,6 @@ export async function patchClashMode(payload: string) {
|
||||
return invoke<void>("patch_clash_mode", { payload });
|
||||
}
|
||||
|
||||
// New IPC-based API functions to replace HTTP API calls
|
||||
export async function getVersion() {
|
||||
return invoke<{
|
||||
premium: boolean;
|
||||
meta?: boolean;
|
||||
version: string;
|
||||
}>("get_clash_version");
|
||||
}
|
||||
|
||||
export async function getClashConfig() {
|
||||
return invoke<IConfigData>("get_clash_config");
|
||||
}
|
||||
|
||||
export async function forceRefreshClashConfig() {
|
||||
return invoke<IConfigData>("force_refresh_clash_config");
|
||||
}
|
||||
|
||||
export async function updateGeoData() {
|
||||
return invoke<void>("update_geo_data");
|
||||
}
|
||||
|
||||
export async function upgradeCore() {
|
||||
return invoke<void>("upgrade_clash_core");
|
||||
}
|
||||
|
||||
export async function getRules() {
|
||||
const response = await invoke<{ rules: IRuleItem[] }>("get_clash_rules");
|
||||
return response?.rules || [];
|
||||
}
|
||||
|
||||
export async function getProxyDelay(
|
||||
name: string,
|
||||
url?: string,
|
||||
timeout?: number,
|
||||
) {
|
||||
return invoke<{ delay: number }>("clash_api_get_proxy_delay", {
|
||||
name,
|
||||
url,
|
||||
timeout: timeout || 10000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateProxy(group: string, proxy: string) {
|
||||
// const start = Date.now();
|
||||
await invoke<void>("update_proxy_choice", { group, proxy });
|
||||
// const duration = Date.now() - start;
|
||||
// console.log(`[API] updateProxy 耗时: ${duration}ms`);
|
||||
}
|
||||
|
||||
export async function syncTrayProxySelection() {
|
||||
return invoke<void>("sync_tray_proxy_selection");
|
||||
}
|
||||
@@ -164,7 +117,7 @@ export async function updateProxyAndSync(group: string, proxy: string) {
|
||||
return invoke<void>("update_proxy_and_sync", { group, proxy });
|
||||
}
|
||||
|
||||
export async function getProxies(): Promise<{
|
||||
export async function calcuProxies(): Promise<{
|
||||
global: IProxyGroupItem;
|
||||
direct: IProxyItem;
|
||||
groups: IProxyGroupItem[];
|
||||
@@ -172,19 +125,17 @@ export async function getProxies(): Promise<{
|
||||
proxies: IProxyItem[];
|
||||
}> {
|
||||
const [proxyResponse, providerResponse] = await Promise.all([
|
||||
invoke<{ proxies: Record<string, IProxyItem> }>("get_proxies"),
|
||||
invoke<{ providers: Record<string, IProxyProviderItem> }>(
|
||||
"get_providers_proxies",
|
||||
),
|
||||
getProxies(),
|
||||
calcuProxyProviders(),
|
||||
]);
|
||||
|
||||
const proxyRecord = proxyResponse.proxies;
|
||||
const providerRecord = providerResponse.providers || {};
|
||||
const providerRecord = providerResponse;
|
||||
|
||||
// provider name map
|
||||
const providerMap = Object.fromEntries(
|
||||
Object.entries(providerRecord).flatMap(([provider, item]) =>
|
||||
item.proxies.map((p) => [p.name, { ...p, provider }]),
|
||||
item!.proxies.map((p) => [p.name, { ...p, provider }]),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -209,7 +160,7 @@ export async function getProxies(): Promise<{
|
||||
let groups: IProxyGroupItem[] = Object.values(proxyRecord).reduce<
|
||||
IProxyGroupItem[]
|
||||
>((acc, each) => {
|
||||
if (each.name !== "GLOBAL" && each.all) {
|
||||
if (each?.name !== "GLOBAL" && each?.all) {
|
||||
acc.push({
|
||||
...each,
|
||||
all: each.all!.map((item) => generateItem(item)),
|
||||
@@ -242,7 +193,7 @@ export async function getProxies(): Promise<{
|
||||
|
||||
const proxies = [direct, reject].concat(
|
||||
Object.values(proxyRecord).filter(
|
||||
(p) => !p.all?.length && p.name !== "DIRECT" && p.name !== "REJECT",
|
||||
(p) => !p?.all?.length && p?.name !== "DIRECT" && p?.name !== "REJECT",
|
||||
),
|
||||
);
|
||||
|
||||
@@ -254,197 +205,39 @@ export async function getProxies(): Promise<{
|
||||
return { global: _global, direct, groups, records: proxyRecord, proxies };
|
||||
}
|
||||
|
||||
export async function getProxyProviders() {
|
||||
const response = await invoke<{
|
||||
providers: Record<string, IProxyProviderItem>;
|
||||
}>("get_providers_proxies");
|
||||
if (!response || !response.providers) {
|
||||
console.warn(
|
||||
"getProxyProviders: Invalid response structure, returning empty object",
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
const providers = response.providers as Record<string, IProxyProviderItem>;
|
||||
|
||||
export async function calcuProxyProviders() {
|
||||
const providers = await getProxyProviders();
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).filter(([, item]) => {
|
||||
const type = item.vehicleType.toLowerCase();
|
||||
return type === "http" || type === "file";
|
||||
}),
|
||||
Object.entries(providers.providers)
|
||||
.sort()
|
||||
.filter(
|
||||
([_key, item]) =>
|
||||
item?.vehicleType === "HTTP" || item?.vehicleType === "File",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getRuleProviders() {
|
||||
const response = await invoke<{
|
||||
providers: Record<string, IRuleProviderItem>;
|
||||
}>("get_rule_providers");
|
||||
|
||||
const providers = (response.providers || {}) as Record<
|
||||
string,
|
||||
IRuleProviderItem
|
||||
>;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).filter(([, item]) => {
|
||||
const type = item.vehicleType.toLowerCase();
|
||||
return type === "http" || type === "file";
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function providerHealthCheck(name: string) {
|
||||
return invoke<void>("proxy_provider_health_check", { name });
|
||||
}
|
||||
|
||||
export async function proxyProviderUpdate(name: string) {
|
||||
return invoke<void>("update_proxy_provider", { name });
|
||||
}
|
||||
|
||||
export async function ruleProviderUpdate(name: string) {
|
||||
return invoke<void>("update_rule_provider", { name });
|
||||
}
|
||||
|
||||
export async function getConnections() {
|
||||
return invoke<IConnections>("get_clash_connections");
|
||||
}
|
||||
|
||||
export async function deleteConnection(id: string) {
|
||||
return invoke<void>("delete_clash_connection", { id });
|
||||
}
|
||||
|
||||
export async function closeAllConnections() {
|
||||
return invoke<void>("close_all_clash_connections");
|
||||
}
|
||||
|
||||
export async function getGroupProxyDelays(
|
||||
groupName: string,
|
||||
url?: string,
|
||||
timeout?: number,
|
||||
) {
|
||||
return invoke<Record<string, number>>("get_group_proxy_delays", {
|
||||
groupName,
|
||||
url,
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTrafficData() {
|
||||
// console.log("[Traffic][Service] 开始调用 get_traffic_data");
|
||||
const result = await invoke<ITrafficItem>("get_traffic_data");
|
||||
// console.log("[Traffic][Service] get_traffic_data 返回结果:", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getMemoryData() {
|
||||
console.log("[Memory][Service] 开始调用 get_memory_data");
|
||||
const result = await invoke<{
|
||||
inuse: number;
|
||||
oslimit?: number;
|
||||
usage_percent?: number;
|
||||
last_updated?: number;
|
||||
}>("get_memory_data");
|
||||
// console.debug("[Memory][Service] get_memory_data 返回结果:", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getFormattedTrafficData() {
|
||||
console.log("[Traffic][Service] 开始调用 get_formatted_traffic_data");
|
||||
const result = await invoke<IFormattedTrafficData>(
|
||||
"get_formatted_traffic_data",
|
||||
);
|
||||
// console.debug(
|
||||
// "[Traffic][Service] get_formatted_traffic_data 返回结果:",
|
||||
// result,
|
||||
// );
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getFormattedMemoryData() {
|
||||
console.log("[Memory][Service] 开始调用 get_formatted_memory_data");
|
||||
const result = await invoke<IFormattedMemoryData>(
|
||||
"get_formatted_memory_data",
|
||||
);
|
||||
// console.debug("[Memory][Service] get_formatted_memory_data 返回结果:", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getSystemMonitorOverview() {
|
||||
console.log("[Monitor][Service] 开始调用 get_system_monitor_overview");
|
||||
const result = await invoke<ISystemMonitorOverview>(
|
||||
"get_system_monitor_overview",
|
||||
);
|
||||
// console.debug(
|
||||
// "[Monitor][Service] get_system_monitor_overview 返回结果:",
|
||||
// result,
|
||||
// );
|
||||
return result;
|
||||
}
|
||||
|
||||
// 带数据验证的安全版本
|
||||
export async function getSystemMonitorOverviewSafe() {
|
||||
// console.log(
|
||||
// "[Monitor][Service] 开始调用安全版本 get_system_monitor_overview",
|
||||
// );
|
||||
try {
|
||||
const result = await invoke<any>("get_system_monitor_overview");
|
||||
// console.log("[Monitor][Service] 原始数据:", result);
|
||||
|
||||
// 导入验证器(动态导入避免循环依赖)
|
||||
const { systemMonitorValidator } = await import("@/utils/data-validator");
|
||||
|
||||
if (systemMonitorValidator.validate(result)) {
|
||||
// console.log("[Monitor][Service] 数据验证通过");
|
||||
return result as ISystemMonitorOverview;
|
||||
} else {
|
||||
// console.warn("[Monitor][Service] 数据验证失败,使用清理后的数据");
|
||||
return systemMonitorValidator.sanitize(result);
|
||||
}
|
||||
} catch {
|
||||
// console.error("[Monitor][Service] API调用失败:", error);
|
||||
// 返回安全的默认值
|
||||
const { systemMonitorValidator } = await import("@/utils/data-validator");
|
||||
return systemMonitorValidator.sanitize(null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startTrafficService() {
|
||||
console.log("[Traffic][Service] 开始调用 start_traffic_service");
|
||||
try {
|
||||
const result = await invoke<void>("start_traffic_service");
|
||||
console.log("[Traffic][Service] start_traffic_service 调用成功");
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("[Traffic][Service] start_traffic_service 调用失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopTrafficService() {
|
||||
console.log("[Traffic][Service] 开始调用 stop_traffic_service");
|
||||
const result = await invoke<void>("stop_traffic_service");
|
||||
console.log("[Traffic][Service] stop_traffic_service 调用成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function isDebugEnabled() {
|
||||
return invoke<boolean>("is_clash_debug_enabled");
|
||||
}
|
||||
|
||||
export async function gc() {
|
||||
return invoke<void>("clash_gc");
|
||||
}
|
||||
|
||||
export async function getClashLogs() {
|
||||
return invoke<any>("get_clash_logs");
|
||||
}
|
||||
const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/;
|
||||
const newRegex = /(.+?)\s+(.+?)\s+(.+)/;
|
||||
const logs = await invoke<string[]>("get_clash_logs");
|
||||
|
||||
export async function startLogsMonitoring(level?: string) {
|
||||
return invoke<void>("start_logs_monitoring", { level });
|
||||
}
|
||||
return logs.reduce<ILogItem[]>((acc, log) => {
|
||||
const result = log.match(regex);
|
||||
if (result) {
|
||||
const [_, _time, type, payload] = result;
|
||||
const time = dayjs(_time).format("MM-DD HH:mm:ss");
|
||||
acc.push({ time, type, payload });
|
||||
return acc;
|
||||
}
|
||||
|
||||
export async function stopLogsMonitoring() {
|
||||
return invoke<void>("stop_logs_monitoring");
|
||||
const result2 = log.match(newRegex);
|
||||
if (result2) {
|
||||
const [_, time, type, payload] = result2;
|
||||
acc.push({ time, type, payload });
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export async function clearLogs() {
|
||||
@@ -576,16 +369,6 @@ export async function cmdGetProxyDelay(
|
||||
}
|
||||
}
|
||||
|
||||
/// 用于profile切换等场景
|
||||
export async function forceRefreshProxies() {
|
||||
const start = Date.now();
|
||||
console.log("[API] 强制刷新代理缓存");
|
||||
const result = await invoke<any>("force_refresh_proxies");
|
||||
const duration = Date.now() - start;
|
||||
console.log(`[API] 代理缓存刷新完成,耗时: ${duration}ms`);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function cmdTestDelay(url: string) {
|
||||
return invoke<number>("test_delay", { url });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cmdGetProxyDelay } from "@/services/cmds";
|
||||
import { delayProxyByName } from "tauri-plugin-mihomo-api";
|
||||
|
||||
const hashKey = (name: string, group: string) => `${group ?? ""}::${name}`;
|
||||
|
||||
@@ -106,7 +106,7 @@ class DelayManager {
|
||||
|
||||
// 使用Promise.race来实现超时控制
|
||||
const result = await Promise.race([
|
||||
cmdGetProxyDelay(name, timeout, url),
|
||||
delayProxyByName(name, url, timeout),
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { create } from "zustand";
|
||||
|
||||
import {
|
||||
fetchLogsViaIPC,
|
||||
startLogsStreaming,
|
||||
stopLogsStreaming,
|
||||
clearLogs as clearLogsIPC,
|
||||
} from "@/services/ipc-log-service";
|
||||
@@ -96,7 +95,7 @@ export const initGlobalLogService = (
|
||||
console.log("[GlobalLog-IPC] 启用IPC流式日志服务");
|
||||
|
||||
// 启动流式监控
|
||||
startLogsStreaming(logLevel);
|
||||
// startLogsStreaming(logLevel);
|
||||
|
||||
// 立即获取一次日志
|
||||
fetchLogsViaIPCPeriodically();
|
||||
@@ -152,7 +151,7 @@ export const changeLogLevel = (level: LogLevel) => {
|
||||
|
||||
if (enabled) {
|
||||
// IPC流式模式下重新启动监控
|
||||
startLogsStreaming(level);
|
||||
// startLogsStreaming(level);
|
||||
fetchLogsViaIPCPeriodically();
|
||||
}
|
||||
};
|
||||
@@ -180,11 +179,11 @@ export const clearGlobalLogs = () => {
|
||||
};
|
||||
|
||||
// 自定义钩子,用于获取过滤后的日志数据
|
||||
export const useGlobalLogData = (logLevel: LogLevel = "all") => {
|
||||
export const useGlobalLogData = (logLevel: LogLevel = "info") => {
|
||||
const logs = useGlobalLogStore((state) => state.logs);
|
||||
|
||||
// 根据当前选择的日志等级过滤日志
|
||||
if (logLevel === "all") {
|
||||
if (logLevel === "info") {
|
||||
return logs;
|
||||
} else {
|
||||
return logs.filter((log) => log.type.toLowerCase() === logLevel);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
// IPC-based log service using Tauri commands with streaming support
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import {
|
||||
getClashLogs,
|
||||
startLogsMonitoring,
|
||||
stopLogsMonitoring,
|
||||
clearLogs as clearLogsCmd,
|
||||
} from "@/services/cmds";
|
||||
import { clearLogs as clearLogsCmd } from "@/services/cmds";
|
||||
|
||||
type LogLevel = "debug" | "info" | "warning" | "error" | "all";
|
||||
|
||||
@@ -21,7 +16,7 @@ interface ILogItem {
|
||||
export const startLogsStreaming = async (logLevel: LogLevel = "info") => {
|
||||
try {
|
||||
const level = logLevel === "all" ? undefined : logLevel;
|
||||
await startLogsMonitoring(level);
|
||||
// await startLogsMonitoring(level);
|
||||
console.log(
|
||||
`[IPC-LogService] Started logs monitoring with level: ${logLevel}`,
|
||||
);
|
||||
@@ -33,7 +28,7 @@ export const startLogsStreaming = async (logLevel: LogLevel = "info") => {
|
||||
// Stop logs monitoring
|
||||
export const stopLogsStreaming = async () => {
|
||||
try {
|
||||
await stopLogsMonitoring();
|
||||
// await stopLogsMonitoring();
|
||||
console.log("[IPC-LogService] Stopped logs monitoring");
|
||||
} catch (error) {
|
||||
console.error("[IPC-LogService] Failed to stop logs monitoring:", error);
|
||||
@@ -45,16 +40,16 @@ export const fetchLogsViaIPC = async (): Promise<ILogItem[]> => {
|
||||
try {
|
||||
// Server-side filtering handles the level via /logs?level={level}
|
||||
// We just fetch all cached logs regardless of the logLevel parameter
|
||||
const response = await getClashLogs();
|
||||
// const response = await getClashLogs();
|
||||
|
||||
// The response should be in the format expected by the frontend
|
||||
// Transform the logs to match the expected format
|
||||
if (Array.isArray(response)) {
|
||||
return response.map((log: any) => ({
|
||||
...log,
|
||||
time: log.time || dayjs().format("HH:mm:ss"),
|
||||
}));
|
||||
}
|
||||
// // The response should be in the format expected by the frontend
|
||||
// // Transform the logs to match the expected format
|
||||
// if (Array.isArray(response)) {
|
||||
// return response.map((log: any) => ({
|
||||
// ...log,
|
||||
// time: log.time || dayjs().format("HH:mm:ss"),
|
||||
// }));
|
||||
// }
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
|
||||
+18
-1
@@ -1,11 +1,28 @@
|
||||
import { createContextState } from "foxact/create-context-state";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
import { LogLevel } from "tauri-plugin-mihomo-api";
|
||||
|
||||
const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState<
|
||||
"light" | "dark"
|
||||
>("light");
|
||||
|
||||
export const useEnableLog = () => useLocalStorage("enable-log", false);
|
||||
interface IClashLog {
|
||||
enable: boolean;
|
||||
logLevel: LogLevel;
|
||||
logFilter: "all" | "debug" | "inf" | "warn" | "err";
|
||||
}
|
||||
const defaultClashLog: IClashLog = {
|
||||
enable: true,
|
||||
logLevel: "info",
|
||||
logFilter: "all",
|
||||
};
|
||||
export const useClashLog = () =>
|
||||
useLocalStorage<IClashLog>("clash-log", defaultClashLog, {
|
||||
serializer: JSON.stringify,
|
||||
deserializer: JSON.parse,
|
||||
});
|
||||
|
||||
// export const useEnableLog = () => useLocalStorage("enable-log", false);
|
||||
|
||||
interface IConnectionSetting {
|
||||
layout: "table" | "list";
|
||||
|
||||
Reference in New Issue
Block a user