Compare commits
37 Commits
feat-intro
...
dev
@@ -87,6 +87,7 @@
|
||||
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
|
||||
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
|
||||
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
|
||||
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
26
package.json
26
package.json
@@ -40,10 +40,10 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/lab": "7.0.0-beta.17",
|
||||
"@mui/material": "^7.3.4",
|
||||
"@mui/x-data-grid": "^8.16.0",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/x-data-grid": "^8.17.0",
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
@@ -54,7 +54,7 @@
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.9.6",
|
||||
"axios": "^1.13.1",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "1.11.19",
|
||||
"foxact": "^0.2.49",
|
||||
"i18next": "^25.6.0",
|
||||
@@ -68,7 +68,7 @@
|
||||
"react-dom": "19.2.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-i18next": "16.2.3",
|
||||
"react-i18next": "16.2.4",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.59.0",
|
||||
"react-router": "^7.9.5",
|
||||
@@ -80,20 +80,20 @@
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@eslint-react/eslint-plugin": "^2.3.1",
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@tauri-apps/cli": "2.9.2",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tauri-apps/cli": "2.9.3",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.2.0",
|
||||
"@vitejs/plugin-react-swc": "^4.2.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cli-color": "^2.0.4",
|
||||
"commander": "^14.0.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
@@ -112,13 +112,13 @@
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.93.3",
|
||||
"tar": "^7.5.2",
|
||||
"terser": "^5.44.0",
|
||||
"terser": "^5.44.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"vite": "^7.1.12",
|
||||
"typescript-eslint": "^8.46.3",
|
||||
"vite": "^7.2.1",
|
||||
"vite-plugin-monaco-editor-esm": "^2.0.2",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vitest": "^4.0.6"
|
||||
"vitest": "^4.0.7"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
|
||||
810
pnpm-lock.yaml
generated
810
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -226,3 +226,23 @@ needless_raw_string_hashes = "deny" # Too many in existing code
|
||||
|
||||
or_fun_call = "deny"
|
||||
cognitive_complexity = "deny"
|
||||
useless_let_if_seq = "deny"
|
||||
use_self = "deny"
|
||||
tuple_array_conversions = "deny"
|
||||
trait_duplication_in_bounds = "deny"
|
||||
suspicious_operation_groupings = "deny"
|
||||
string_lit_as_bytes = "deny"
|
||||
significant_drop_tightening = "deny"
|
||||
significant_drop_in_scrutinee = "deny"
|
||||
redundant_clone = "deny"
|
||||
# option_if_let_else = "deny" // 过于激进,暂时不开启
|
||||
needless_pass_by_ref_mut = "deny"
|
||||
needless_collect = "deny"
|
||||
missing_const_for_fn = "deny"
|
||||
iter_with_drain = "deny"
|
||||
iter_on_single_items = "deny"
|
||||
iter_on_empty_collections = "deny"
|
||||
# fallible_impl_from = "deny" // 过于激进,暂时不开启
|
||||
equatable_if_let = "deny"
|
||||
collection_is_never_read = "deny"
|
||||
branches_sharing_code = "deny"
|
||||
|
||||
@@ -189,7 +189,6 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
} else {
|
||||
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
|
||||
logging!(
|
||||
@@ -212,9 +211,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "Config regenerated successfully");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -13,19 +13,23 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let body_lower = body.to_lowercase();
|
||||
let mut status = "Failed";
|
||||
let mut region = None;
|
||||
|
||||
if body_lower.contains("youtube premium is not available in your country") {
|
||||
return UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if body_lower.contains("ad-free") {
|
||||
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||
Ok(re) => re,
|
||||
status = "No";
|
||||
} else if body_lower.contains("ad-free") {
|
||||
match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||
Ok(re) => {
|
||||
if let Some(caps) = re.captures(&body)
|
||||
&& let Some(m) = caps.get(1)
|
||||
{
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
region = Some(format!("{emoji}{country_code}"));
|
||||
status = "Yes";
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
@@ -33,34 +37,14 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
"Failed to compile YouTube Premium regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let region = re.captures(&body).and_then(|caps| {
|
||||
caps.get(1).map(|m| {
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
});
|
||||
|
||||
return UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -27,8 +27,8 @@ static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
pub async fn get_profiles() -> CmdResult<SharedBox<IProfiles>> {
|
||||
logging!(debug, Type::Cmd, "获取配置文件列表");
|
||||
let draft = Config::profiles().await;
|
||||
let latest = draft.latest_arc();
|
||||
Ok(latest)
|
||||
let data = draft.data_arc();
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 增强配置文件
|
||||
@@ -100,9 +100,11 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
match profiles_reorder_safe(&active_id, &over_id).await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "重新排序配置文件");
|
||||
Config::profiles().await.apply();
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::profiles().await.discard();
|
||||
logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err);
|
||||
Err(format!("重新排序配置文件失败: {}", err).into())
|
||||
}
|
||||
@@ -120,12 +122,16 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
||||
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
}
|
||||
Config::profiles().await.apply();
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => match err.to_string().as_str() {
|
||||
"the file already exists" => Err("the file already exists".into()),
|
||||
_ => Err(format!("add profile error: {err}").into()),
|
||||
},
|
||||
Err(err) => {
|
||||
Config::profiles().await.discard();
|
||||
match err.to_string().as_str() {
|
||||
"the file already exists" => Err("the file already exists".into()),
|
||||
_ => Err(format!("add profile error: {err}").into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,8 +139,12 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
match feat::update_profile(&index, option.as_ref(), true, true).await {
|
||||
Ok(_) => Ok(()),
|
||||
Ok(_) => {
|
||||
let _: () = Config::profiles().await.apply();
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
Config::profiles().await.discard();
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
@@ -144,12 +154,11 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
|
||||
/// 删除配置文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
println!("delete_profile: {}", index);
|
||||
// 使用Send-safe helper函数
|
||||
let should_update = profiles_delete_item_safe(&index).await.stringify_err()?;
|
||||
profiles_save_file_safe().await.stringify_err()?;
|
||||
|
||||
if should_update {
|
||||
Config::profiles().await.apply();
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -235,7 +244,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
error_msg.clone(),
|
||||
error_msg,
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
@@ -244,7 +253,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_parse_error",
|
||||
error_msg.clone(),
|
||||
error_msg,
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
@@ -253,19 +262,13 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_error",
|
||||
error_msg.clone(),
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::file_read_error", error_msg);
|
||||
Err(())
|
||||
}
|
||||
Err(_) => {
|
||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_timeout",
|
||||
error_msg.clone(),
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::file_read_timeout", error_msg);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
@@ -275,16 +278,15 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
}
|
||||
|
||||
/// 执行配置更新并处理结果
|
||||
async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
|
||||
async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> {
|
||||
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
current: Some(prev_profile.to_owned()),
|
||||
items: None,
|
||||
};
|
||||
Config::profiles()
|
||||
.await
|
||||
.edit_draft(|d| d.patch_config(&restore_profiles))
|
||||
.stringify_err()?;
|
||||
.edit_draft(|d| d.patch_config(&restore_profiles));
|
||||
Config::profiles().await.apply();
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
@@ -295,7 +297,7 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
|
||||
async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
@@ -311,9 +313,9 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
|
||||
}
|
||||
|
||||
if let Some(current) = ¤t_value {
|
||||
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current,);
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
if let Some(current) = current_value {
|
||||
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current);
|
||||
handle::Handle::notify_profile_changed(current.to_owned());
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
@@ -321,7 +323,7 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
|
||||
|
||||
async fn handle_validation_failure(
|
||||
error_msg: String,
|
||||
current_profile: Option<String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().await.discard();
|
||||
@@ -339,7 +341,7 @@ async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
|
||||
async fn handle_timeout(current_profile: Option<&String>) -> CmdResult<bool> {
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(error, Type::Cmd, "{}", timeout_msg);
|
||||
Config::profiles().await.discard();
|
||||
@@ -351,9 +353,12 @@ async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
|
||||
}
|
||||
|
||||
async fn perform_config_update(
|
||||
current_value: Option<String>,
|
||||
current_profile: Option<String>,
|
||||
current_value: Option<&String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<bool> {
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
CoreManager::global().update_config(),
|
||||
@@ -376,13 +381,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
.is_err()
|
||||
{
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Err("switch_in_progress".into());
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let target_profile = profiles.current.clone();
|
||||
let target_profile = profiles.current.as_ref();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
@@ -392,24 +394,22 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
);
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().await.latest_arc().current.clone();
|
||||
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
|
||||
let previous_profile = Config::profiles().await.data_arc().current.clone();
|
||||
logging!(info, Type::Cmd, "当前配置: {:?}", previous_profile);
|
||||
|
||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||
if let Some(new_profile) = profiles.current.as_ref()
|
||||
&& current_profile.as_ref() != Some(new_profile)
|
||||
&& validate_new_profile(new_profile).await.is_err()
|
||||
if let Some(switch_to_profile) = target_profile
|
||||
&& previous_profile.as_ref() != Some(switch_to_profile)
|
||||
&& validate_new_profile(switch_to_profile).await.is_err()
|
||||
{
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let _ = Config::profiles()
|
||||
Config::profiles()
|
||||
.await
|
||||
.edit_draft(|d| d.patch_config(&profiles));
|
||||
|
||||
let current_value = profiles.current.clone();
|
||||
|
||||
perform_config_update(current_value, current_profile).await
|
||||
perform_config_update(target_profile, previous_profile.as_ref()).await
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{CoreManager, handle},
|
||||
core::{CoreManager, handle, manager::RunningMode},
|
||||
logging,
|
||||
module::sysinfo::PlatformSpecification,
|
||||
utils::logging::Type,
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
sync::atomic::{AtomicI64, Ordering},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tokio::time::Instant;
|
||||
|
||||
// 存储应用启动时间的全局变量
|
||||
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||
// 获取当前系统时间,转换为毫秒级时间戳
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
AtomicI64::new(now)
|
||||
static APP_START_TIME: Lazy<Instant> = Lazy::new(Instant::now);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| unsafe { libc::geteuid() } == 0);
|
||||
#[cfg(target_os = "windows")]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| {
|
||||
Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
@@ -45,52 +47,18 @@ pub async fn get_system_info() -> CmdResult<String> {
|
||||
|
||||
/// 获取当前内核运行模式
|
||||
#[tauri::command]
|
||||
pub async fn get_running_mode() -> Result<String, String> {
|
||||
Ok(CoreManager::global().get_running_mode().to_string())
|
||||
pub async fn get_running_mode() -> Result<Arc<RunningMode>, String> {
|
||||
Ok(CoreManager::global().get_running_mode())
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[tauri::command]
|
||||
pub fn get_app_uptime() -> CmdResult<i64> {
|
||||
let start_time = APP_START_TIME.load(Ordering::Relaxed);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
Ok(now - start_time)
|
||||
pub fn get_app_uptime() -> CmdResult<u128> {
|
||||
Ok(APP_START_TIME.elapsed().as_millis())
|
||||
}
|
||||
|
||||
/// 检查应用是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
|
||||
let result = Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 非Windows平台检测是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
Ok(*APPS_RUN_AS_ADMIN)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ mod platform {
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
|
||||
pub fn invoke_uwp_tool() -> CmdResult {
|
||||
pub const fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
cmd::StringifyErr,
|
||||
config::{Config, IVerge},
|
||||
feat,
|
||||
utils::draft::SharedBox,
|
||||
};
|
||||
use crate::{cmd::StringifyErr, config::IVerge, feat, utils::draft::SharedBox};
|
||||
|
||||
/// 获取Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
|
||||
let verge = Config::verge().await;
|
||||
Ok(verge.latest_arc())
|
||||
feat::fetch_verge_config().await.stringify_err()
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
|
||||
@@ -22,11 +22,11 @@ pub struct Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn global() -> &'static Config {
|
||||
pub async fn global() -> &'static Self {
|
||||
static CONFIG: OnceCell<Config> = OnceCell::const_new();
|
||||
CONFIG
|
||||
.get_or_init(|| async {
|
||||
Config {
|
||||
Self {
|
||||
clash_config: Draft::new(IClashTemp::new().await),
|
||||
verge_config: Draft::new(IVerge::new().await),
|
||||
profiles_config: Draft::new(IProfiles::new().await),
|
||||
@@ -60,15 +60,15 @@ impl Config {
|
||||
if !cmd::system::is_admin().unwrap_or_default()
|
||||
&& service::is_service_available().await.is_err()
|
||||
{
|
||||
let verge = Config::verge().await;
|
||||
let verge = Self::verge().await;
|
||||
verge.edit_draft(|d| {
|
||||
d.enable_tun_mode = Some(false);
|
||||
});
|
||||
verge.apply();
|
||||
let _ = tray::Tray::global().update_tray_display().await;
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
|
||||
// 分离数据获取和异步调用避免Send问题
|
||||
let verge_data = Config::verge().await.latest_arc();
|
||||
let verge_data = Self::verge().await.latest_arc();
|
||||
logging_error!(Type::Core, verge_data.save_file().await);
|
||||
}
|
||||
|
||||
@@ -154,23 +154,21 @@ impl Config {
|
||||
ConfigType::Check => dirs::app_home_dir()?.join(files::CHECK_CONFIG),
|
||||
};
|
||||
|
||||
let runtime = Config::runtime().await;
|
||||
let config = runtime
|
||||
.latest_arc()
|
||||
let runtime = Self::runtime().await;
|
||||
let runtime_arc = runtime.latest_arc();
|
||||
let config = runtime_arc
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("failed to get runtime config"))?
|
||||
.clone();
|
||||
drop(runtime); // 显式释放锁
|
||||
.ok_or_else(|| anyhow!("failed to get runtime config"))?;
|
||||
|
||||
help::save_yaml(&path, &config, Some("# Generated by Clash Verge")).await?;
|
||||
help::save_yaml(&path, config, Some("# Generated by Clash Verge")).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub async fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||
|
||||
Config::runtime().await.edit_draft(|d| {
|
||||
Self::runtime().await.edit_draft(|d| {
|
||||
*d = IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
@@ -191,11 +189,11 @@ impl Config {
|
||||
};
|
||||
|
||||
let operation = || async {
|
||||
if Config::runtime().await.latest_arc().config.is_some() {
|
||||
if Self::runtime().await.latest_arc().config.is_some() {
|
||||
return Ok::<(), BackoffError<anyhow::Error>>(());
|
||||
}
|
||||
|
||||
Config::generate().await.map_err(BackoffError::transient)
|
||||
Self::generate().await.map_err(BackoffError::transient)
|
||||
};
|
||||
|
||||
if let Err(e) = backoff::future::retry(backoff_strategy, operation).await {
|
||||
|
||||
@@ -5,9 +5,16 @@ use aes_gcm::{
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::cell::Cell;
|
||||
use std::future::Future;
|
||||
|
||||
const NONCE_LENGTH: usize = 12;
|
||||
|
||||
// Use task-local context so the flag follows the async task across threads
|
||||
tokio::task_local! {
|
||||
static ENCRYPTION_ACTIVE: Cell<bool>;
|
||||
}
|
||||
|
||||
/// Encrypt data
|
||||
#[allow(deprecated)]
|
||||
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
@@ -59,39 +66,45 @@ where
|
||||
T: Serialize,
|
||||
S: Serializer,
|
||||
{
|
||||
// 如果序列化失败,返回 None
|
||||
let json = match serde_json::to_string(value) {
|
||||
Ok(j) => j,
|
||||
Err(_) => return serializer.serialize_none(),
|
||||
};
|
||||
|
||||
// 如果加密失败,返回 None
|
||||
match encrypt_data(&json) {
|
||||
Ok(encrypted) => serializer.serialize_str(&encrypted),
|
||||
Err(_) => serializer.serialize_none(),
|
||||
if is_encryption_active() {
|
||||
let json = serde_json::to_string(value).map_err(serde::ser::Error::custom)?;
|
||||
let encrypted = encrypt_data(&json).map_err(serde::ser::Error::custom)?;
|
||||
serializer.serialize_str(&encrypted)
|
||||
} else {
|
||||
value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize decrypted function
|
||||
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
pub fn deserialize_encrypted<'a, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de> + Default,
|
||||
D: Deserializer<'a>,
|
||||
{
|
||||
// 如果反序列化字符串失败,返回默认值
|
||||
let encrypted = match String::deserialize(deserializer) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Ok(T::default()),
|
||||
};
|
||||
if is_encryption_active() {
|
||||
let encrypted_opt: Option<String> = Option::deserialize(deserializer)?;
|
||||
|
||||
// 如果解密失败,返回默认值
|
||||
let decrypted_string = match decrypt_data(&encrypted) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Ok(T::default()),
|
||||
};
|
||||
// 如果 JSON 解析失败,返回默认值
|
||||
match serde_json::from_str(&decrypted_string) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => Ok(T::default()),
|
||||
match encrypted_opt {
|
||||
Some(encrypted) if !encrypted.is_empty() => {
|
||||
let decrypted_string =
|
||||
decrypt_data(&encrypted).map_err(serde::de::Error::custom)?;
|
||||
serde_json::from_str(&decrypted_string).map_err(serde::de::Error::custom)
|
||||
}
|
||||
_ => Ok(T::default()),
|
||||
}
|
||||
} else {
|
||||
T::deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn with_encryption<F, Fut, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = R>,
|
||||
{
|
||||
ENCRYPTION_ACTIVE.scope(Cell::new(true), f()).await
|
||||
}
|
||||
|
||||
fn is_encryption_active() -> bool {
|
||||
ENCRYPTION_ACTIVE.try_with(|c| c.get()).unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ impl PrfOption {
|
||||
impl PrfItem {
|
||||
/// From partial item
|
||||
/// must contain `itype`
|
||||
pub async fn from(item: &PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||
pub async fn from(item: &Self, file_data: Option<String>) -> Result<Self> {
|
||||
if item.itype.is_none() {
|
||||
bail!("type should not be null");
|
||||
}
|
||||
@@ -170,13 +170,13 @@ impl PrfItem {
|
||||
let name = item.name.as_ref();
|
||||
let desc = item.desc.as_ref();
|
||||
let option = item.option.as_ref();
|
||||
PrfItem::from_url(url, name, desc, option).await
|
||||
Self::from_url(url, name, desc, option).await
|
||||
}
|
||||
"local" => {
|
||||
let name = item.name.clone().unwrap_or_else(|| "Local File".into());
|
||||
let desc = item.desc.clone().unwrap_or_else(|| "".into());
|
||||
let option = item.option.as_ref();
|
||||
PrfItem::from_local(name, desc, file_data, option).await
|
||||
Self::from_local(name, desc, file_data, option).await
|
||||
}
|
||||
typ => bail!("invalid profile item type \"{typ}\""),
|
||||
}
|
||||
@@ -189,7 +189,7 @@ impl PrfItem {
|
||||
desc: String,
|
||||
file_data: Option<String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
) -> Result<Self> {
|
||||
let uid = help::get_uid("L").into();
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let opt_ref = option.as_ref();
|
||||
@@ -201,31 +201,31 @@ impl PrfItem {
|
||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut PrfItem::from_merge(None)?;
|
||||
let merge_item = &mut Self::from_merge(None)?;
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut PrfItem::from_script(None)?;
|
||||
let script_item = &mut Self::from_script(None)?;
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut PrfItem::from_rules()?;
|
||||
let rules_item = &mut Self::from_rules()?;
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut PrfItem::from_proxies()?;
|
||||
let proxies_item = &mut Self::from_proxies()?;
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut PrfItem::from_groups()?;
|
||||
let groups_item = &mut Self::from_groups()?;
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("local".into()),
|
||||
name: Some(name),
|
||||
@@ -256,7 +256,7 @@ impl PrfItem {
|
||||
name: Option<&String>,
|
||||
desc: Option<&String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
) -> Result<Self> {
|
||||
let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||
let accept_invalid_certs =
|
||||
@@ -393,32 +393,32 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut PrfItem::from_merge(None)?;
|
||||
let merge_item = &mut Self::from_merge(None)?;
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut PrfItem::from_script(None)?;
|
||||
let script_item = &mut Self::from_script(None)?;
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut PrfItem::from_rules()?;
|
||||
let rules_item = &mut Self::from_rules()?;
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut PrfItem::from_proxies()?;
|
||||
let proxies_item = &mut Self::from_proxies()?;
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut PrfItem::from_groups()?;
|
||||
let groups_item = &mut Self::from_groups()?;
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("remote".into()),
|
||||
name: Some(name),
|
||||
@@ -445,16 +445,15 @@ impl PrfItem {
|
||||
|
||||
/// ## Merge type (enhance)
|
||||
/// create the enhanced item by using `merge` rule
|
||||
pub fn from_merge(uid: Option<String>) -> Result<PrfItem> {
|
||||
let mut id = help::get_uid("m").into();
|
||||
let mut template = tmpl::ITEM_MERGE_EMPTY.into();
|
||||
if let Some(uid) = uid {
|
||||
id = uid;
|
||||
template = tmpl::ITEM_MERGE.into();
|
||||
}
|
||||
pub fn from_merge(uid: Option<String>) -> Result<Self> {
|
||||
let (id, template) = if let Some(uid) = uid {
|
||||
(uid, tmpl::ITEM_MERGE.into())
|
||||
} else {
|
||||
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
|
||||
};
|
||||
let file = format!("{id}.yaml").into();
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(id),
|
||||
itype: Some("merge".into()),
|
||||
name: None,
|
||||
@@ -472,14 +471,14 @@ impl PrfItem {
|
||||
|
||||
/// ## Script type (enhance)
|
||||
/// create the enhanced item by using javascript quick.js
|
||||
pub fn from_script(uid: Option<String>) -> Result<PrfItem> {
|
||||
let mut id = help::get_uid("s").into();
|
||||
if let Some(uid) = uid {
|
||||
id = uid;
|
||||
}
|
||||
pub fn from_script(uid: Option<String>) -> Result<Self> {
|
||||
let id = if let Some(uid) = uid {
|
||||
uid
|
||||
} else {
|
||||
help::get_uid("s").into()
|
||||
};
|
||||
let file = format!("{id}.js").into(); // js ext
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(id),
|
||||
itype: Some("script".into()),
|
||||
name: None,
|
||||
@@ -496,11 +495,11 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
/// ## Rules type (enhance)
|
||||
pub fn from_rules() -> Result<PrfItem> {
|
||||
pub fn from_rules() -> Result<Self> {
|
||||
let uid = help::get_uid("r").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("rules".into()),
|
||||
name: None,
|
||||
@@ -517,11 +516,11 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
/// ## Proxies type (enhance)
|
||||
pub fn from_proxies() -> Result<PrfItem> {
|
||||
pub fn from_proxies() -> Result<Self> {
|
||||
let uid = help::get_uid("p").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("proxies".into()),
|
||||
name: None,
|
||||
@@ -538,11 +537,11 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
/// ## Groups type (enhance)
|
||||
pub fn from_groups() -> Result<PrfItem> {
|
||||
pub fn from_groups() -> Result<Self> {
|
||||
let uid = help::get_uid("g").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Ok(PrfItem {
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("groups".into()),
|
||||
name: None,
|
||||
@@ -584,7 +583,34 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl PrfItem {
|
||||
/// 获取current指向的订阅的merge
|
||||
pub fn current_merge(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.merge.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的script
|
||||
pub fn current_script(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.script.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的rules
|
||||
pub fn current_rules(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.rules.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的proxies
|
||||
pub fn current_proxies(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.proxies.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的groups
|
||||
pub fn current_groups(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.groups.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// 向前兼容,默认为订阅启用自动更新
|
||||
fn default_allow_auto_update() -> Option<bool> {
|
||||
const fn default_allow_auto_update() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use anyhow::{Context, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashSet;
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
use tokio::fs;
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
@@ -32,7 +32,7 @@ pub struct CleanupResult {
|
||||
macro_rules! patch {
|
||||
($lv: expr, $rv: expr, $key: tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key.clone();
|
||||
$lv.$key = $rv.$key.to_owned();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -50,28 +50,26 @@ impl IProfiles {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn new() -> Self {
|
||||
match dirs::profiles_path() {
|
||||
Ok(path) => match help::read_yaml::<Self>(&path).await {
|
||||
Ok(mut profiles) => {
|
||||
if profiles.items.is_none() {
|
||||
profiles.items = Some(vec![]);
|
||||
let path = match dirs::profiles_path() {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "{err}");
|
||||
return Self::default();
|
||||
}
|
||||
};
|
||||
|
||||
match help::read_yaml::<Self>(&path).await {
|
||||
Ok(mut profiles) => {
|
||||
let items = profiles.items.get_or_insert_with(Vec::new);
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d").into());
|
||||
}
|
||||
// compatible with the old old old version
|
||||
if let Some(items) = profiles.items.as_mut() {
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
profiles
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::default()
|
||||
}
|
||||
},
|
||||
profiles
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::default()
|
||||
@@ -89,7 +87,7 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// 只修改current,valid和chain
|
||||
pub fn patch_config(&mut self, patch: &IProfiles) -> Result<()> {
|
||||
pub fn patch_config(&mut self, patch: &Self) {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
@@ -102,16 +100,14 @@ impl IProfiles {
|
||||
self.current = some_uid.cloned();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_current(&self) -> Option<String> {
|
||||
self.current.clone()
|
||||
pub const fn get_current(&self) -> Option<&String> {
|
||||
self.current.as_ref()
|
||||
}
|
||||
|
||||
/// get items ref
|
||||
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
|
||||
pub const fn get_items(&self) -> Option<&Vec<PrfItem>> {
|
||||
self.items.as_ref()
|
||||
}
|
||||
|
||||
@@ -132,6 +128,15 @@ impl IProfiles {
|
||||
bail!("failed to get the profile item \"uid:{}\"", uid_str);
|
||||
}
|
||||
|
||||
pub fn get_item_arc(&self, uid: &str) -> Option<Arc<PrfItem>> {
|
||||
self.items.as_ref().and_then(|items| {
|
||||
items
|
||||
.iter()
|
||||
.find(|it| it.uid.as_deref() == Some(uid))
|
||||
.map(|it| Arc::new(it.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
/// append new item
|
||||
/// if the file_data is some
|
||||
/// then should save the data to file
|
||||
@@ -357,88 +362,18 @@ impl IProfiles {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的merge
|
||||
pub fn current_merge(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let merge = item.option.as_ref().and_then(|e| e.merge.clone());
|
||||
return merge;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的script
|
||||
pub fn current_script(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let script = item.option.as_ref().and_then(|e| e.script.clone());
|
||||
return script;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的rules
|
||||
pub fn current_rules(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let rules = item.option.as_ref().and_then(|e| e.rules.clone());
|
||||
return rules;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的proxies
|
||||
pub fn current_proxies(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let proxies = item.option.as_ref().and_then(|e| e.proxies.clone());
|
||||
return proxies;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的groups
|
||||
pub fn current_groups(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let groups = item.option.as_ref().and_then(|e| e.groups.clone());
|
||||
return groups;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断profile是否是current指向的
|
||||
pub fn is_current_profile_index(&self, index: &String) -> bool {
|
||||
self.current.as_ref() == Some(index)
|
||||
}
|
||||
|
||||
/// 获取所有的profiles(uid,名称)
|
||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(&String, &String)>> {
|
||||
self.items.as_ref().map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
||||
if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) {
|
||||
Some((uid, name))
|
||||
} else {
|
||||
None
|
||||
@@ -449,11 +384,11 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// 通过 uid 获取名称
|
||||
pub fn get_name_by_uid(&self, uid: &String) -> Option<String> {
|
||||
pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> {
|
||||
if let Some(items) = &self.items {
|
||||
for item in items {
|
||||
if item.uid.as_ref() == Some(uid) {
|
||||
return item.name.clone();
|
||||
return item.name.as_ref();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -551,14 +486,14 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// 获取所有 active profile 关联的文件名
|
||||
fn get_all_active_files(&self) -> HashSet<String> {
|
||||
let mut active_files = HashSet::new();
|
||||
fn get_all_active_files(&self) -> HashSet<&str> {
|
||||
let mut active_files: HashSet<&str> = HashSet::new();
|
||||
|
||||
if let Some(items) = &self.items {
|
||||
for item in items {
|
||||
// 收集所有类型 profile 的文件
|
||||
if let Some(file) = &item.file {
|
||||
active_files.insert(file.clone());
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
|
||||
@@ -571,35 +506,35 @@ impl IProfiles {
|
||||
&& let Ok(merge_item) = self.get_item(merge_uid)
|
||||
&& let Some(file) = &merge_item.file
|
||||
{
|
||||
active_files.insert(file.clone());
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
if let Some(script_uid) = &option.script
|
||||
&& let Ok(script_item) = self.get_item(script_uid)
|
||||
&& let Some(file) = &script_item.file
|
||||
{
|
||||
active_files.insert(file.clone());
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
if let Some(rules_uid) = &option.rules
|
||||
&& let Ok(rules_item) = self.get_item(rules_uid)
|
||||
&& let Some(file) = &rules_item.file
|
||||
{
|
||||
active_files.insert(file.clone());
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
if let Some(proxies_uid) = &option.proxies
|
||||
&& let Ok(proxies_item) = self.get_item(proxies_uid)
|
||||
&& let Some(file) = &proxies_item.file
|
||||
{
|
||||
active_files.insert(file.clone());
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
if let Some(groups_uid) = &option.groups
|
||||
&& let Ok(groups_item) = self.get_item(groups_uid)
|
||||
&& let Some(file) = &groups_item.file
|
||||
{
|
||||
active_files.insert(file.clone());
|
||||
active_files.insert(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,19 +46,24 @@ pub struct IVerge {
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
|
||||
/// enable group icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_group_icon: Option<bool>,
|
||||
|
||||
/// common tray icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub common_tray_icon: Option<bool>,
|
||||
|
||||
/// tray icon
|
||||
#[cfg(target_os = "macos")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_icon: Option<String>,
|
||||
|
||||
/// menu icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub menu_icon: Option<String>,
|
||||
|
||||
/// menu order
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub menu_order: Option<Vec<String>>,
|
||||
|
||||
/// sysproxy tray icon
|
||||
@@ -115,6 +120,7 @@ pub struct IVerge {
|
||||
|
||||
/// hotkey map
|
||||
/// format: {func},{key}
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
|
||||
/// enable global hotkey
|
||||
@@ -134,7 +140,7 @@ pub struct IVerge {
|
||||
pub default_latency_test: Option<String>,
|
||||
|
||||
/// 默认的延迟测试超时时间
|
||||
pub default_latency_timeout: Option<i32>,
|
||||
pub default_latency_timeout: Option<i16>,
|
||||
|
||||
/// 是否自动检测当前节点延迟
|
||||
pub enable_auto_delay_detection: Option<bool>,
|
||||
@@ -143,7 +149,7 @@ pub struct IVerge {
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
|
||||
/// proxy 页面布局 列数
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
pub proxy_layout_column: Option<u8>,
|
||||
|
||||
/// 测试站列表
|
||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||
@@ -202,6 +208,7 @@ pub struct IVerge {
|
||||
)]
|
||||
pub webdav_password: Option<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
|
||||
// pub enable_tray_icon: Option<bool>,
|
||||
@@ -255,7 +262,7 @@ impl IVerge {
|
||||
/// 验证并修正配置文件中的clash_core值
|
||||
pub async fn validate_and_fix_config() -> Result<()> {
|
||||
let config_path = dirs::verge_path()?;
|
||||
let mut config = match help::read_yaml::<IVerge>(&config_path).await {
|
||||
let mut config = match help::read_yaml::<Self>(&config_path).await {
|
||||
Ok(config) => config,
|
||||
Err(_) => Self::template(),
|
||||
};
|
||||
@@ -304,7 +311,7 @@ impl IVerge {
|
||||
}
|
||||
|
||||
/// 配置修正后重新加载配置
|
||||
async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
|
||||
async fn reload_config_after_fix(updated_config: Self) -> Result<()> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
@@ -344,7 +351,7 @@ impl IVerge {
|
||||
|
||||
pub async fn new() -> Self {
|
||||
match dirs::verge_path() {
|
||||
Ok(path) => match help::read_yaml::<IVerge>(&path).await {
|
||||
Ok(path) => match help::read_yaml::<Self>(&path).await {
|
||||
Ok(mut config) => {
|
||||
// compatibility
|
||||
if let Some(start_page) = config.start_page.clone()
|
||||
@@ -439,7 +446,7 @@ impl IVerge {
|
||||
/// patch verge config
|
||||
/// only save to file
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn patch_config(&mut self, patch: &IVerge) {
|
||||
pub fn patch_config(&mut self, patch: &Self) {
|
||||
macro_rules! patch {
|
||||
($key: tt) => {
|
||||
if patch.$key.is_some() {
|
||||
@@ -524,7 +531,7 @@ impl IVerge {
|
||||
patch!(enable_external_controller);
|
||||
}
|
||||
|
||||
pub fn get_singleton_port() -> u16 {
|
||||
pub const fn get_singleton_port() -> u16 {
|
||||
crate::constants::network::ports::SINGLETON_SERVER
|
||||
}
|
||||
|
||||
@@ -545,156 +552,3 @@ impl IVerge {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct IVergeResponse {
|
||||
pub app_log_level: Option<String>,
|
||||
pub app_log_max_size: Option<u64>,
|
||||
pub app_log_max_count: Option<usize>,
|
||||
pub language: Option<String>,
|
||||
pub theme_mode: Option<String>,
|
||||
pub tray_event: Option<String>,
|
||||
pub env_type: Option<String>,
|
||||
pub start_page: Option<String>,
|
||||
pub startup_script: Option<String>,
|
||||
pub traffic_graph: Option<bool>,
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
pub enable_group_icon: Option<bool>,
|
||||
pub common_tray_icon: Option<bool>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub tray_icon: Option<String>,
|
||||
pub menu_icon: Option<String>,
|
||||
pub menu_order: Option<Vec<String>>,
|
||||
pub sysproxy_tray_icon: Option<bool>,
|
||||
pub tun_tray_icon: Option<bool>,
|
||||
pub enable_tun_mode: Option<bool>,
|
||||
pub enable_auto_launch: Option<bool>,
|
||||
pub enable_silent_start: Option<bool>,
|
||||
pub enable_system_proxy: Option<bool>,
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
pub enable_global_hotkey: Option<bool>,
|
||||
pub use_default_bypass: Option<bool>,
|
||||
pub system_proxy_bypass: Option<String>,
|
||||
pub proxy_guard_duration: Option<u64>,
|
||||
pub proxy_auto_config: Option<bool>,
|
||||
pub pac_file_content: Option<String>,
|
||||
pub proxy_host: Option<String>,
|
||||
pub theme_setting: Option<IVergeTheme>,
|
||||
pub web_ui_list: Option<Vec<String>>,
|
||||
pub clash_core: Option<String>,
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
pub auto_close_connection: Option<bool>,
|
||||
pub auto_check_update: Option<bool>,
|
||||
pub default_latency_test: Option<String>,
|
||||
pub default_latency_timeout: Option<i32>,
|
||||
pub enable_auto_delay_detection: Option<bool>,
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||
pub auto_log_clean: Option<i32>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub verge_redir_port: Option<u16>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub verge_redir_enabled: Option<bool>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub verge_tproxy_port: Option<u16>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub verge_tproxy_enabled: Option<bool>,
|
||||
pub verge_mixed_port: Option<u16>,
|
||||
pub verge_socks_port: Option<u16>,
|
||||
pub verge_socks_enabled: Option<bool>,
|
||||
pub verge_port: Option<u16>,
|
||||
pub verge_http_enabled: Option<bool>,
|
||||
pub webdav_url: Option<String>,
|
||||
pub webdav_username: Option<String>,
|
||||
pub webdav_password: Option<String>,
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
// pub enable_tray_icon: Option<bool>,
|
||||
pub tray_inline_proxy_groups: Option<bool>,
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub enable_hover_jump_navigator: Option<bool>,
|
||||
pub hover_jump_navigator_delay: Option<u64>,
|
||||
pub enable_external_controller: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<IVerge> for IVergeResponse {
|
||||
fn from(verge: IVerge) -> Self {
|
||||
// 先获取验证后的clash_core值,避免后续借用冲突
|
||||
let valid_clash_core = verge.get_valid_clash_core();
|
||||
Self {
|
||||
app_log_level: verge.app_log_level,
|
||||
app_log_max_size: verge.app_log_max_size,
|
||||
app_log_max_count: verge.app_log_max_count,
|
||||
language: verge.language,
|
||||
theme_mode: verge.theme_mode,
|
||||
tray_event: verge.tray_event,
|
||||
env_type: verge.env_type,
|
||||
start_page: verge.start_page,
|
||||
startup_script: verge.startup_script,
|
||||
traffic_graph: verge.traffic_graph,
|
||||
enable_memory_usage: verge.enable_memory_usage,
|
||||
enable_group_icon: verge.enable_group_icon,
|
||||
common_tray_icon: verge.common_tray_icon,
|
||||
#[cfg(target_os = "macos")]
|
||||
tray_icon: verge.tray_icon,
|
||||
menu_icon: verge.menu_icon,
|
||||
menu_order: verge.menu_order,
|
||||
sysproxy_tray_icon: verge.sysproxy_tray_icon,
|
||||
tun_tray_icon: verge.tun_tray_icon,
|
||||
enable_tun_mode: verge.enable_tun_mode,
|
||||
enable_auto_launch: verge.enable_auto_launch,
|
||||
enable_silent_start: verge.enable_silent_start,
|
||||
enable_system_proxy: verge.enable_system_proxy,
|
||||
enable_proxy_guard: verge.enable_proxy_guard,
|
||||
enable_global_hotkey: verge.enable_global_hotkey,
|
||||
use_default_bypass: verge.use_default_bypass,
|
||||
system_proxy_bypass: verge.system_proxy_bypass,
|
||||
proxy_guard_duration: verge.proxy_guard_duration,
|
||||
proxy_auto_config: verge.proxy_auto_config,
|
||||
pac_file_content: verge.pac_file_content,
|
||||
proxy_host: verge.proxy_host,
|
||||
theme_setting: verge.theme_setting,
|
||||
web_ui_list: verge.web_ui_list,
|
||||
clash_core: Some(valid_clash_core),
|
||||
hotkeys: verge.hotkeys,
|
||||
auto_close_connection: verge.auto_close_connection,
|
||||
auto_check_update: verge.auto_check_update,
|
||||
default_latency_test: verge.default_latency_test,
|
||||
default_latency_timeout: verge.default_latency_timeout,
|
||||
enable_auto_delay_detection: verge.enable_auto_delay_detection,
|
||||
enable_builtin_enhanced: verge.enable_builtin_enhanced,
|
||||
proxy_layout_column: verge.proxy_layout_column,
|
||||
test_list: verge.test_list,
|
||||
auto_log_clean: verge.auto_log_clean,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
verge_redir_port: verge.verge_redir_port,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
verge_redir_enabled: verge.verge_redir_enabled,
|
||||
#[cfg(target_os = "linux")]
|
||||
verge_tproxy_port: verge.verge_tproxy_port,
|
||||
#[cfg(target_os = "linux")]
|
||||
verge_tproxy_enabled: verge.verge_tproxy_enabled,
|
||||
verge_mixed_port: verge.verge_mixed_port,
|
||||
verge_socks_port: verge.verge_socks_port,
|
||||
verge_socks_enabled: verge.verge_socks_enabled,
|
||||
verge_port: verge.verge_port,
|
||||
verge_http_enabled: verge.verge_http_enabled,
|
||||
webdav_url: verge.webdav_url,
|
||||
webdav_username: verge.webdav_username,
|
||||
webdav_password: verge.webdav_password,
|
||||
enable_tray_speed: verge.enable_tray_speed,
|
||||
// enable_tray_icon: verge.enable_tray_icon,
|
||||
tray_inline_proxy_groups: verge.tray_inline_proxy_groups,
|
||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
enable_dns_settings: verge.enable_dns_settings,
|
||||
home_cards: verge.home_cards,
|
||||
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
||||
hover_jump_navigator_delay: verge.hover_jump_navigator_delay,
|
||||
enable_external_controller: verge.enable_external_controller,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,11 +347,12 @@ impl AsyncProxyQuery {
|
||||
&mut buffer_size,
|
||||
);
|
||||
|
||||
let mut proxy_server = String::new();
|
||||
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
||||
let proxy_server = if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
||||
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
|
||||
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
|
||||
}
|
||||
String::from_utf16_lossy(&buffer[..end_pos])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// 读取代理绕过列表
|
||||
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
|
||||
@@ -368,14 +369,16 @@ impl AsyncProxyQuery {
|
||||
&mut bypass_buffer_size,
|
||||
);
|
||||
|
||||
let mut bypass_list = String::new();
|
||||
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
|
||||
let end_pos = bypass_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(bypass_buffer.len());
|
||||
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
|
||||
}
|
||||
let bypass_list =
|
||||
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
|
||||
let end_pos = bypass_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(bypass_buffer.len());
|
||||
String::from_utf16_lossy(&bypass_buffer[..end_pos])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
|
||||
@@ -45,12 +45,12 @@ enum Operation {
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
fn timeout(&self) -> u64 {
|
||||
const fn timeout(&self) -> u64 {
|
||||
match self {
|
||||
Operation::Upload => TIMEOUT_UPLOAD,
|
||||
Operation::Download => TIMEOUT_DOWNLOAD,
|
||||
Operation::List => TIMEOUT_LIST,
|
||||
Operation::Delete => TIMEOUT_DELETE,
|
||||
Self::Upload => TIMEOUT_UPLOAD,
|
||||
Self::Download => TIMEOUT_DOWNLOAD,
|
||||
Self::List => TIMEOUT_LIST,
|
||||
Self::Delete => TIMEOUT_DELETE,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,9 +61,9 @@ pub struct WebDavClient {
|
||||
}
|
||||
|
||||
impl WebDavClient {
|
||||
pub fn global() -> &'static WebDavClient {
|
||||
pub fn global() -> &'static Self {
|
||||
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
||||
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
|
||||
WEBDAV_CLIENT.get_or_init(|| Self {
|
||||
config: Arc::new(ArcSwapOption::new(None)),
|
||||
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
|
||||
})
|
||||
@@ -87,7 +87,7 @@ impl WebDavClient {
|
||||
(*cfg_arc).clone()
|
||||
} else {
|
||||
// 释放锁后获取异步配置
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
let verge = Config::verge().await.data_arc();
|
||||
if verge.webdav_url.is_none()
|
||||
|| verge.webdav_username.is_none()
|
||||
|| verge.webdav_password.is_none()
|
||||
|
||||
@@ -75,7 +75,7 @@ struct ProxyConfig {
|
||||
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
|
||||
|
||||
impl EventDrivenProxyManager {
|
||||
pub fn global() -> &'static EventDrivenProxyManager {
|
||||
pub fn global() -> &'static Self {
|
||||
&PROXY_MANAGER
|
||||
}
|
||||
|
||||
|
||||
@@ -102,14 +102,14 @@ impl Handle {
|
||||
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
|
||||
}
|
||||
|
||||
// TODO 利用 &str 等缩短 Clone
|
||||
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||
let handle = Self::global();
|
||||
let status_str = status.into();
|
||||
let msg_str = msg.into();
|
||||
|
||||
if !*handle.startup_completed.read() {
|
||||
let mut errors = handle.startup_errors.write();
|
||||
errors.push(ErrorMessage {
|
||||
handle.startup_errors.write().push(ErrorMessage {
|
||||
status: status_str,
|
||||
message: msg_str,
|
||||
});
|
||||
@@ -158,7 +158,7 @@ impl Handle {
|
||||
.spawn(move || {
|
||||
thread::sleep(timing::STARTUP_ERROR_DELAY);
|
||||
|
||||
let handle = Handle::global();
|
||||
let handle = Self::global();
|
||||
if handle.is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,16 +28,16 @@ pub enum HotkeyFunction {
|
||||
impl fmt::Display for HotkeyFunction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard",
|
||||
HotkeyFunction::ClashModeRule => "clash_mode_rule",
|
||||
HotkeyFunction::ClashModeGlobal => "clash_mode_global",
|
||||
HotkeyFunction::ClashModeDirect => "clash_mode_direct",
|
||||
HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy",
|
||||
HotkeyFunction::ToggleTunMode => "toggle_tun_mode",
|
||||
HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode",
|
||||
HotkeyFunction::Quit => "quit",
|
||||
Self::OpenOrCloseDashboard => "open_or_close_dashboard",
|
||||
Self::ClashModeRule => "clash_mode_rule",
|
||||
Self::ClashModeGlobal => "clash_mode_global",
|
||||
Self::ClashModeDirect => "clash_mode_direct",
|
||||
Self::ToggleSystemProxy => "toggle_system_proxy",
|
||||
Self::ToggleTunMode => "toggle_tun_mode",
|
||||
Self::EntryLightweightMode => "entry_lightweight_mode",
|
||||
Self::Quit => "quit",
|
||||
#[cfg(target_os = "macos")]
|
||||
HotkeyFunction::Hide => "hide",
|
||||
Self::Hide => "hide",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
@@ -48,16 +48,16 @@ impl FromStr for HotkeyFunction {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim() {
|
||||
"open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard),
|
||||
"clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule),
|
||||
"clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal),
|
||||
"clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect),
|
||||
"toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy),
|
||||
"toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode),
|
||||
"entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode),
|
||||
"quit" => Ok(HotkeyFunction::Quit),
|
||||
"open_or_close_dashboard" => Ok(Self::OpenOrCloseDashboard),
|
||||
"clash_mode_rule" => Ok(Self::ClashModeRule),
|
||||
"clash_mode_global" => Ok(Self::ClashModeGlobal),
|
||||
"clash_mode_direct" => Ok(Self::ClashModeDirect),
|
||||
"toggle_system_proxy" => Ok(Self::ToggleSystemProxy),
|
||||
"toggle_tun_mode" => Ok(Self::ToggleTunMode),
|
||||
"entry_lightweight_mode" => Ok(Self::EntryLightweightMode),
|
||||
"quit" => Ok(Self::Quit),
|
||||
#[cfg(target_os = "macos")]
|
||||
"hide" => Ok(HotkeyFunction::Hide),
|
||||
"hide" => Ok(Self::Hide),
|
||||
_ => bail!("invalid hotkey function: {}", s),
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,8 @@ pub enum SystemHotkey {
|
||||
impl fmt::Display for SystemHotkey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
SystemHotkey::CmdQ => "CMD+Q",
|
||||
SystemHotkey::CmdW => "CMD+W",
|
||||
Self::CmdQ => "CMD+Q",
|
||||
Self::CmdW => "CMD+W",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
@@ -84,10 +84,10 @@ impl fmt::Display for SystemHotkey {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl SystemHotkey {
|
||||
pub fn function(self) -> HotkeyFunction {
|
||||
pub const fn function(self) -> HotkeyFunction {
|
||||
match self {
|
||||
SystemHotkey::CmdQ => HotkeyFunction::Quit,
|
||||
SystemHotkey::CmdW => HotkeyFunction::Hide,
|
||||
Self::CmdQ => HotkeyFunction::Quit,
|
||||
Self::CmdW => HotkeyFunction::Hide,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,28 +211,18 @@ impl Hotkey {
|
||||
let is_quit = matches!(function, HotkeyFunction::Quit);
|
||||
|
||||
manager.on_shortcut(hotkey, move |_app_handle, hotkey_event, event| {
|
||||
let hotkey_event_owned = *hotkey_event;
|
||||
let event_owned = event;
|
||||
let function_owned = function;
|
||||
let is_quit_owned = is_quit;
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
if event_owned.state == ShortcutState::Pressed {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Hotkey pressed: {:?}",
|
||||
hotkey_event_owned
|
||||
);
|
||||
|
||||
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
|
||||
if let Some(window) = handle::Handle::get_window()
|
||||
&& window.is_focused().unwrap_or(false)
|
||||
{
|
||||
logging!(debug, Type::Hotkey, "Executing quit function");
|
||||
Self::execute_function(function_owned);
|
||||
}
|
||||
} else {
|
||||
if event.state == ShortcutState::Pressed {
|
||||
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey_event);
|
||||
let hotkey = hotkey_event.key;
|
||||
if hotkey == Code::KeyQ && is_quit {
|
||||
if let Some(window) = handle::Handle::get_window()
|
||||
&& window.is_focused().unwrap_or(false)
|
||||
{
|
||||
logging!(debug, Type::Hotkey, "Executing quit function");
|
||||
Self::execute_function(function);
|
||||
}
|
||||
} else {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
@@ -242,19 +232,19 @@ impl Hotkey {
|
||||
.unwrap_or(true);
|
||||
|
||||
if is_enable_global_hotkey {
|
||||
Self::execute_function(function_owned);
|
||||
Self::execute_function(function);
|
||||
} else {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let is_visible = WindowManager::is_main_window_visible();
|
||||
let is_focused = WindowManager::is_main_window_focused();
|
||||
|
||||
if is_focused && is_visible {
|
||||
Self::execute_function(function_owned);
|
||||
Self::execute_function(function);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})?;
|
||||
|
||||
logging!(
|
||||
|
||||
@@ -137,7 +137,7 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_connection_io_error(kind: std::io::ErrorKind) -> bool {
|
||||
const fn is_connection_io_error(kind: std::io::ErrorKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
std::io::ErrorKind::ConnectionAborted
|
||||
|
||||
@@ -68,7 +68,8 @@ impl CoreManager {
|
||||
#[cfg(target_os = "windows")]
|
||||
self.wait_for_service_if_needed().await;
|
||||
|
||||
let mode = match SERVICE_MANAGER.lock().await.current() {
|
||||
let value = SERVICE_MANAGER.lock().await.current();
|
||||
let mode = match value {
|
||||
ServiceStatus::Ready => RunningMode::Service,
|
||||
_ => RunningMode::Sidecar,
|
||||
};
|
||||
|
||||
@@ -62,8 +62,12 @@ impl CoreManager {
|
||||
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
|
||||
let mut now = DeferredNow::default();
|
||||
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
|
||||
let w = shared_writer.lock().await;
|
||||
write_sidecar_log(w, &mut now, Level::Error, &message);
|
||||
write_sidecar_log(
|
||||
shared_writer.lock().await,
|
||||
&mut now,
|
||||
Level::Error,
|
||||
&message,
|
||||
);
|
||||
CLASH_LOGGER.append_log(message).await;
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
|
||||
@@ -75,8 +79,12 @@ impl CoreManager {
|
||||
} else {
|
||||
CompactString::from("Process terminated")
|
||||
};
|
||||
let w = shared_writer.lock().await;
|
||||
write_sidecar_log(w, &mut now, Level::Info, &message);
|
||||
write_sidecar_log(
|
||||
shared_writer.lock().await,
|
||||
&mut now,
|
||||
Level::Info,
|
||||
&message,
|
||||
);
|
||||
CLASH_LOGGER.clear_logs().await;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::handle::Handle;
|
||||
use crate::{
|
||||
constants::{retry, timing},
|
||||
logging,
|
||||
@@ -11,7 +12,7 @@ use std::{
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
time::Instant,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri::{Emitter, WebviewWindow};
|
||||
|
||||
@@ -91,30 +92,26 @@ impl NotificationSystem {
|
||||
}
|
||||
|
||||
fn worker_loop(rx: mpsc::Receiver<FrontendEvent>) {
|
||||
use super::handle::Handle;
|
||||
|
||||
let handle = Handle::global();
|
||||
|
||||
while !handle.is_exiting() {
|
||||
match rx.recv() {
|
||||
loop {
|
||||
let handle = Handle::global();
|
||||
if handle.is_exiting() {
|
||||
break;
|
||||
}
|
||||
match rx.recv_timeout(Duration::from_millis(1_000)) {
|
||||
Ok(event) => Self::process_event(handle, event),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"receive event error, stop notification worker: {}",
|
||||
e
|
||||
);
|
||||
break;
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => (),
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clippy 似乎对 parking lot 的 RwLock 有误报,这里禁用相关警告
|
||||
#[allow(clippy::significant_drop_tightening)]
|
||||
fn process_event(handle: &super::handle::Handle, event: FrontendEvent) {
|
||||
let system_guard = handle.notification_system.read();
|
||||
let Some(system) = system_guard.as_ref() else {
|
||||
return;
|
||||
let binding = handle.notification_system.read();
|
||||
let system = match binding.as_ref() {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if system.should_skip_event(&event) {
|
||||
|
||||
@@ -136,7 +136,7 @@ async fn uninstall_service() -> Result<()> {
|
||||
let elevator = crate::utils::help::linux_elevator();
|
||||
let status = match get_effective_uid() {
|
||||
0 => StdCommand::new(uninstall_shell).status()?,
|
||||
_ => StdCommand::new(elevator.clone())
|
||||
_ => StdCommand::new(elevator)
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
.arg(uninstall_shell)
|
||||
@@ -177,7 +177,7 @@ async fn install_service() -> Result<()> {
|
||||
let elevator = crate::utils::help::linux_elevator();
|
||||
let status = match get_effective_uid() {
|
||||
0 => StdCommand::new(install_shell).status()?,
|
||||
_ => StdCommand::new(elevator.clone())
|
||||
_ => StdCommand::new(elevator)
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
.arg(install_shell)
|
||||
@@ -449,7 +449,7 @@ impl ServiceManager {
|
||||
Self(ServiceStatus::Unavailable("Need Checks".into()))
|
||||
}
|
||||
|
||||
pub fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
|
||||
pub const fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
|
||||
Some(clash_verge_service_ipc::IpcConfig {
|
||||
default_timeout: Duration::from_millis(30),
|
||||
retry_delay: Duration::from_millis(250),
|
||||
@@ -532,7 +532,7 @@ impl ServiceManager {
|
||||
return Err(anyhow::anyhow!("服务不可用: {}", reason));
|
||||
}
|
||||
}
|
||||
let _ = tray::Tray::global().update_tray_display().await;
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ async fn execute_sysproxy_command(args: Vec<std::string::String>) -> Result<()>
|
||||
|
||||
impl Default for Sysopt {
|
||||
fn default() -> Self {
|
||||
Sysopt {
|
||||
Self {
|
||||
initialed: AtomicBool::new(false),
|
||||
update_sysproxy: AtomicBool::new(false),
|
||||
reset_sysproxy: AtomicBool::new(false),
|
||||
|
||||
@@ -46,7 +46,7 @@ singleton!(Timer, TIMER_INSTANCE);
|
||||
|
||||
impl Timer {
|
||||
fn new() -> Self {
|
||||
Timer {
|
||||
Self {
|
||||
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
|
||||
timer_map: Arc::new(RwLock::new(HashMap::new())),
|
||||
timer_count: AtomicU64::new(1),
|
||||
@@ -154,19 +154,17 @@ impl Timer {
|
||||
/// 每 3 秒更新系统托盘菜单,总共执行 3 次
|
||||
pub fn add_update_tray_menu_task(&self) -> Result<()> {
|
||||
let tid = self.timer_count.fetch_add(1, Ordering::SeqCst);
|
||||
let delay_timer = self.delay_timer.write();
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(tid)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_count_down_by_seconds(3, 3)
|
||||
.spawn_async_routine(|| async move {
|
||||
logging!(debug, Type::Timer, "Updating tray menu");
|
||||
crate::core::tray::Tray::global()
|
||||
.update_tray_display()
|
||||
.await
|
||||
crate::core::tray::Tray::global().update_menu().await
|
||||
})
|
||||
.context("failed to create update tray menu timer task")?;
|
||||
delay_timer
|
||||
self.delay_timer
|
||||
.write()
|
||||
.add_task(task)
|
||||
.context("failed to add update tray menu timer task")?;
|
||||
Ok(())
|
||||
@@ -195,14 +193,12 @@ impl Timer {
|
||||
|
||||
// Perform sync operations while holding locks
|
||||
{
|
||||
let mut timer_map = self.timer_map.write();
|
||||
let delay_timer = self.delay_timer.write();
|
||||
|
||||
for (uid, diff) in diff_map {
|
||||
match diff {
|
||||
DiffFlag::Del(tid) => {
|
||||
timer_map.remove(&uid);
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
self.timer_map.write().remove(&uid);
|
||||
let value = self.delay_timer.write().remove_task(tid);
|
||||
if let Err(e) = value {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
@@ -222,12 +218,13 @@ impl Timer {
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(uid.clone(), task);
|
||||
self.timer_map.write().insert(uid.clone(), task);
|
||||
operations_to_add.push((uid, tid, interval));
|
||||
}
|
||||
DiffFlag::Mod(tid, interval) => {
|
||||
// Remove old task first
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
let value = self.delay_timer.write().remove_task(tid);
|
||||
if let Err(e) = value {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
@@ -245,7 +242,7 @@ impl Timer {
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(uid.clone(), task);
|
||||
self.timer_map.write().insert(uid.clone(), task);
|
||||
operations_to_add.push((uid, tid, interval));
|
||||
}
|
||||
}
|
||||
@@ -255,8 +252,8 @@ impl Timer {
|
||||
// Now perform async operations without holding locks
|
||||
for (uid, tid, interval) in operations_to_add {
|
||||
// Re-acquire locks for individual operations
|
||||
let mut delay_timer = self.delay_timer.write();
|
||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||
let delay_timer = self.delay_timer.write();
|
||||
if let Err(e) = self.add_task(&delay_timer, uid.clone(), tid, interval) {
|
||||
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
|
||||
|
||||
// Rollback on failure - remove from timer_map
|
||||
@@ -373,7 +370,7 @@ impl Timer {
|
||||
/// Add a timer task with better error handling
|
||||
fn add_task(
|
||||
&self,
|
||||
delay_timer: &mut DelayTimer,
|
||||
delay_timer: &DelayTimer,
|
||||
uid: String,
|
||||
tid: TaskID,
|
||||
minutes: u64,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use tauri::Emitter;
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri_plugin_mihomo::models::Proxies;
|
||||
use tokio::fs;
|
||||
@@ -55,18 +54,17 @@ fn get_tray_click_debounce() -> &'static Mutex<Instant> {
|
||||
|
||||
fn should_handle_tray_click() -> bool {
|
||||
let debounce_lock = get_tray_click_debounce();
|
||||
let mut last_click = debounce_lock.lock();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
||||
*last_click = now;
|
||||
if now.duration_since(*debounce_lock.lock()) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
||||
*debounce_lock.lock() = now;
|
||||
true
|
||||
} else {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Tray,
|
||||
"托盘点击被防抖机制忽略,距离上次点击 {}ms",
|
||||
now.duration_since(*last_click).as_millis()
|
||||
now.duration_since(*debounce_lock.lock()).as_millis()
|
||||
);
|
||||
false
|
||||
}
|
||||
@@ -198,7 +196,7 @@ impl TrayState {
|
||||
|
||||
impl Default for Tray {
|
||||
fn default() -> Self {
|
||||
Tray {
|
||||
Self {
|
||||
last_menu_update: Mutex::new(None),
|
||||
menu_updating: AtomicBool::new(false),
|
||||
}
|
||||
@@ -318,11 +316,9 @@ impl Tray {
|
||||
.unwrap_or("rule")
|
||||
.to_owned()
|
||||
};
|
||||
let profile_uid_and_name = Config::profiles()
|
||||
.await
|
||||
.latest_arc()
|
||||
.all_profile_uid_and_name()
|
||||
.unwrap_or_default();
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_arc = profiles_config.latest_arc();
|
||||
let profile_uid_and_name = profiles_arc.all_profile_uid_and_name().unwrap_or_default();
|
||||
let is_lightweight_mode = is_in_lightweight_mode();
|
||||
|
||||
match app_handle.tray_by_id("main") {
|
||||
@@ -433,24 +429,6 @@ impl Tray {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘显示状态的函数
|
||||
pub async fn update_tray_display(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘显示状态更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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"))?;
|
||||
|
||||
// 更新菜单
|
||||
self.update_menu().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘提示
|
||||
pub async fn update_tooltip(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
@@ -476,7 +454,7 @@ impl Tray {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_arc();
|
||||
if let Some(current_profile_uid) = profiles.get_current()
|
||||
&& let Ok(profile) = profiles.get_item(¤t_profile_uid)
|
||||
&& let Ok(profile) = profiles.get_item(current_profile_uid)
|
||||
{
|
||||
current_profile_name = match &profile.name {
|
||||
Some(profile_name) => profile_name.to_string(),
|
||||
@@ -525,9 +503,7 @@ impl Tray {
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新");
|
||||
return Ok(());
|
||||
}
|
||||
// self.update_menu().await?;
|
||||
// 更新轻量模式显示状态
|
||||
self.update_tray_display().await?;
|
||||
self.update_menu().await?;
|
||||
self.update_icon().await?;
|
||||
self.update_tooltip().await?;
|
||||
Ok(())
|
||||
@@ -572,72 +548,38 @@ impl Tray {
|
||||
let tray = builder.build(app_handle)?;
|
||||
|
||||
tray.on_tray_icon_event(|_app_handle, event| {
|
||||
// 忽略移动、进入和离开等无需处理的事件,避免不必要的刷新
|
||||
match event {
|
||||
TrayIconEvent::Move { .. }
|
||||
| TrayIconEvent::Enter { .. }
|
||||
| TrayIconEvent::Leave { .. } => {
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
AsyncHandler::spawn(|| async move {
|
||||
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
|
||||
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
|
||||
|
||||
AsyncHandler::spawn(|| async move {
|
||||
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
|
||||
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
|
||||
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
// 添加防抖检查,防止快速连击
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
let fut: Pin<Box<dyn Future<Output = ()> + Send>> = match tray_event.as_str() {
|
||||
"system_proxy" => Box::pin(async move {
|
||||
feat::toggle_system_proxy().await;
|
||||
}),
|
||||
"tun_mode" => Box::pin(async move {
|
||||
feat::toggle_tun_mode(None).await;
|
||||
}),
|
||||
"main_window" => Box::pin(async move {
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy().await,
|
||||
"tun_mode" => feat::toggle_tun_mode(None).await,
|
||||
"main_window" => {
|
||||
if !lightweight::exit_lightweight_mode().await {
|
||||
WindowManager::show_main_window().await;
|
||||
};
|
||||
}),
|
||||
_ => Box::pin(async move {}),
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
fut.await;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
tray.on_menu_event(on_menu_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 托盘统一的状态更新函数
|
||||
pub async fn update_all_states(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘状态更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 确保所有状态更新完成
|
||||
self.update_tray_display().await?;
|
||||
// self.update_menu().await?;
|
||||
self.update_icon().await?;
|
||||
self.update_tooltip().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
|
||||
@@ -666,7 +608,7 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
|
||||
|
||||
async fn create_profile_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
profile_uid_and_name: Vec<(String, String)>,
|
||||
profile_uid_and_name: Vec<(&String, &String)>,
|
||||
) -> Result<Vec<CheckMenuItem<Wry>>> {
|
||||
let futures = profile_uid_and_name
|
||||
.iter()
|
||||
@@ -870,7 +812,7 @@ async fn create_tray_menu(
|
||||
system_proxy_enabled: bool,
|
||||
tun_mode_enabled: bool,
|
||||
tun_mode_available: bool,
|
||||
profile_uid_and_name: Vec<(String, String)>,
|
||||
profile_uid_and_name: Vec<(&String, &String)>,
|
||||
is_lightweight_mode: bool,
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
@@ -881,7 +823,7 @@ async fn create_tray_menu(
|
||||
let profiles_ref = profiles_config.latest_arc();
|
||||
profiles_ref
|
||||
.get_current()
|
||||
.and_then(|uid| profiles_ref.get_item(&uid).ok())
|
||||
.and_then(|uid| profiles_ref.get_item(uid).ok())
|
||||
.and_then(|profile| profile.selected.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
@@ -1255,66 +1197,27 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
id if id.starts_with("proxy_") => {
|
||||
// proxy_{group_name}_{proxy_name}
|
||||
let parts: Vec<&str> = id.splitn(3, '_').collect();
|
||||
|
||||
if parts.len() == 3 && parts[0] == "proxy" {
|
||||
let group_name = parts[1];
|
||||
let proxy_name = parts[2];
|
||||
|
||||
match handle::Handle::mihomo()
|
||||
.await
|
||||
.select_node_for_group(group_name, proxy_name)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Tray,
|
||||
"切换代理成功: {} -> {}",
|
||||
group_name,
|
||||
proxy_name
|
||||
);
|
||||
let _ = handle::Handle::app_handle()
|
||||
.emit("verge://refresh-proxy-config", ());
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Tray,
|
||||
"切换代理失败: {} -> {}, 错误: {:?}",
|
||||
group_name,
|
||||
proxy_name,
|
||||
e
|
||||
);
|
||||
|
||||
// Fallback to IPC update
|
||||
if (handle::Handle::mihomo()
|
||||
.await
|
||||
.select_node_for_group(group_name, proxy_name)
|
||||
.await)
|
||||
.is_ok()
|
||||
{
|
||||
logging!(
|
||||
info,
|
||||
Type::Tray,
|
||||
"代理切换回退成功: {} -> {}",
|
||||
group_name,
|
||||
proxy_name
|
||||
);
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let _ = app_handle.emit("verge://force-refresh-proxies", ());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rest = match id.strip_prefix("proxy_") {
|
||||
Some(r) => r,
|
||||
None => return,
|
||||
};
|
||||
let (group_name, proxy_name) = match rest.split_once('_') {
|
||||
Some((g, p)) => (g, p),
|
||||
None => return,
|
||||
};
|
||||
feat::switch_proxy_node(group_name, proxy_name).await;
|
||||
}
|
||||
_ => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Tray,
|
||||
"Unhandled tray menu event: {:?}",
|
||||
event.id
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Ensure tray state update is awaited and properly handled
|
||||
if let Err(e) = Tray::global().update_all_states().await {
|
||||
logging!(warn, Type::Tray, "Failed to update tray state: {e}");
|
||||
}
|
||||
// We dont expected to refresh tray state here
|
||||
// as the inner handle function (SHOULD) already takes care of it
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct CoreConfigValidator {
|
||||
}
|
||||
|
||||
impl CoreConfigValidator {
|
||||
pub fn new() -> Self {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
is_processing: AtomicBool::new(false),
|
||||
}
|
||||
@@ -275,41 +275,43 @@ impl CoreConfigValidator {
|
||||
logging!(info, Type::Validate, "验证目录: {}", app_dir_str);
|
||||
|
||||
// 使用子进程运行clash验证配置
|
||||
let output = app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core.as_str())?
|
||||
.args(["-t", "-d", app_dir_str, "-f", config_path])
|
||||
.output()
|
||||
.await?;
|
||||
let command = app_handle.shell().sidecar(clash_core.as_str())?.args([
|
||||
"-t",
|
||||
"-d",
|
||||
app_dir_str,
|
||||
"-f",
|
||||
config_path,
|
||||
]);
|
||||
let output = command.output().await?;
|
||||
|
||||
let stderr = std::string::String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
|
||||
let status = &output.status;
|
||||
let stderr = &output.stderr;
|
||||
let stdout = &output.stdout;
|
||||
|
||||
// 检查进程退出状态和错误输出
|
||||
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
|
||||
let has_error =
|
||||
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||
let has_error = !status.success() || contains_any_keyword(stderr, &error_keywords);
|
||||
|
||||
logging!(info, Type::Validate, "-------- 验证结果 --------");
|
||||
|
||||
if !stderr.is_empty() {
|
||||
logging!(info, Type::Validate, "stderr输出:\n{}", stderr);
|
||||
logging!(info, Type::Validate, "stderr输出:\n{:?}", stderr);
|
||||
}
|
||||
|
||||
if has_error {
|
||||
logging!(info, Type::Validate, "发现错误,开始处理错误信息");
|
||||
let error_msg = if !stdout.is_empty() {
|
||||
stdout.into()
|
||||
let error_msg: String = if !stdout.is_empty() {
|
||||
str::from_utf8(stdout).unwrap_or_default().into()
|
||||
} else if !stderr.is_empty() {
|
||||
stderr.into()
|
||||
} else if let Some(code) = output.status.code() {
|
||||
format!("验证进程异常退出,退出码: {code}")
|
||||
str::from_utf8(stderr).unwrap_or_default().into()
|
||||
} else if let Some(code) = status.code() {
|
||||
format!("验证进程异常退出,退出码: {code}").into()
|
||||
} else {
|
||||
"验证进程被终止".into()
|
||||
};
|
||||
|
||||
logging!(info, Type::Validate, "-------- 验证结束 --------");
|
||||
Ok((false, error_msg.into())) // 返回错误消息给调用者处理
|
||||
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||
} else {
|
||||
logging!(info, Type::Validate, "验证成功");
|
||||
logging!(info, Type::Validate, "-------- 验证结束 --------");
|
||||
@@ -342,6 +344,23 @@ fn has_ext<P: AsRef<std::path::Path>>(path: P, ext: &str) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn contains_any_keyword<'a>(buf: &'a [u8], keywords: &'a [&str]) -> bool {
|
||||
for &kw in keywords {
|
||||
let needle = kw.as_bytes();
|
||||
if needle.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut i = 0;
|
||||
while i + needle.len() <= buf.len() {
|
||||
if &buf[i..i + needle.len()] == needle {
|
||||
return true;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
singleton_lazy!(
|
||||
CoreConfigValidator,
|
||||
CORECONFIGVALIDATOR,
|
||||
|
||||
@@ -70,7 +70,7 @@ pub trait AsyncChainItemFrom {
|
||||
}
|
||||
|
||||
impl AsyncChainItemFrom for Option<ChainItem> {
|
||||
async fn from_async(item: &PrfItem) -> Option<ChainItem> {
|
||||
async fn from_async(item: &PrfItem) -> Self {
|
||||
let itype = item.itype.as_ref()?.as_str();
|
||||
let file = item.file.clone()?;
|
||||
let uid = item.uid.clone().unwrap_or_else(|| "".into());
|
||||
@@ -116,22 +116,21 @@ impl AsyncChainItemFrom for Option<ChainItem> {
|
||||
}
|
||||
impl ChainItem {
|
||||
/// 内建支持一些脚本
|
||||
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
|
||||
pub fn builtin() -> Vec<(ChainSupport, Self)> {
|
||||
// meta 的一些处理
|
||||
let meta_guard =
|
||||
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn =
|
||||
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
// meta 的一些处理
|
||||
let meta_guard_alpha =
|
||||
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn_alpha =
|
||||
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
vec![
|
||||
(ChainSupport::ClashMeta, hy_alpn),
|
||||
@@ -154,8 +153,7 @@ impl ChainSupport {
|
||||
match core {
|
||||
Some(core) => matches!(
|
||||
(self, core.as_str()),
|
||||
(ChainSupport::ClashMeta, "verge-mihomo")
|
||||
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
|
||||
(Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha")
|
||||
),
|
||||
None => true,
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ fn deep_merge(a: &mut Value, b: &Value) {
|
||||
|
||||
pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
|
||||
let mut config = Value::from(config);
|
||||
let merge = use_lowercase(merge.clone());
|
||||
let merge = use_lowercase(merge);
|
||||
|
||||
deep_merge(&mut config, &Value::from(merge));
|
||||
|
||||
|
||||
@@ -44,6 +44,43 @@ struct ProfileItems {
|
||||
profile_name: String,
|
||||
}
|
||||
|
||||
impl Default for ProfileItems {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: Default::default(),
|
||||
profile_name: Default::default(),
|
||||
merge_item: ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
},
|
||||
script_item: ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
},
|
||||
rules_item: ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Rules(SeqMap::default()),
|
||||
},
|
||||
proxies_item: ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Proxies(SeqMap::default()),
|
||||
},
|
||||
groups_item: ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Groups(SeqMap::default()),
|
||||
},
|
||||
global_merge: ChainItem {
|
||||
uid: "Merge".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
},
|
||||
global_script: ChainItem {
|
||||
uid: "Script".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_config_values() -> ConfigValues {
|
||||
let clash_config = { Config::clash().await.latest_arc().0.clone() };
|
||||
|
||||
@@ -89,18 +126,10 @@ async fn get_config_values() -> ConfigValues {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn collect_profile_items() -> ProfileItems {
|
||||
// 从profiles里拿东西 - 先收集需要的数据,然后释放锁
|
||||
let (
|
||||
current,
|
||||
merge_uid,
|
||||
script_uid,
|
||||
rules_uid,
|
||||
proxies_uid,
|
||||
groups_uid,
|
||||
_current_profile_uid,
|
||||
name,
|
||||
) = {
|
||||
let (current, merge_uid, script_uid, rules_uid, proxies_uid, groups_uid, name) = {
|
||||
let current = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_clone = profiles.latest_arc();
|
||||
@@ -109,13 +138,31 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
let current_profile_uid = match profiles_ref.get_current() {
|
||||
Some(uid) => uid.clone(),
|
||||
None => return ProfileItems::default(),
|
||||
};
|
||||
|
||||
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
|
||||
let script_uid = profiles_ref.current_script().unwrap_or_default();
|
||||
let rules_uid = profiles_ref.current_rules().unwrap_or_default();
|
||||
let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
|
||||
let groups_uid = profiles_ref.current_groups().unwrap_or_default();
|
||||
let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
|
||||
let current_item = match profiles_ref.get_item_arc(¤t_profile_uid) {
|
||||
Some(item) => item,
|
||||
None => return ProfileItems::default(),
|
||||
};
|
||||
|
||||
let merge_uid = current_item
|
||||
.current_merge()
|
||||
.unwrap_or_else(|| "Merge".into());
|
||||
let script_uid = current_item
|
||||
.current_script()
|
||||
.unwrap_or_else(|| "Script".into());
|
||||
let rules_uid = current_item
|
||||
.current_rules()
|
||||
.unwrap_or_else(|| "Rules".into());
|
||||
let proxies_uid = current_item
|
||||
.current_proxies()
|
||||
.unwrap_or_else(|| "Proxies".into());
|
||||
let groups_uid = current_item
|
||||
.current_groups()
|
||||
.unwrap_or_else(|| "Groups".into());
|
||||
|
||||
let name = profiles_ref
|
||||
.get_item(¤t_profile_uid)
|
||||
@@ -130,7 +177,6 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
rules_uid,
|
||||
proxies_uid,
|
||||
groups_uid,
|
||||
current_profile_uid,
|
||||
name,
|
||||
)
|
||||
};
|
||||
@@ -140,7 +186,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_arc();
|
||||
profiles.get_item(merge_uid).ok().cloned()
|
||||
profiles.get_item(&merge_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -157,7 +203,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_arc();
|
||||
profiles.get_item(script_uid).ok().cloned()
|
||||
profiles.get_item(&script_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -174,7 +220,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_arc();
|
||||
profiles.get_item(rules_uid).ok().cloned()
|
||||
profiles.get_item(&rules_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -191,7 +237,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_arc();
|
||||
profiles.get_item(proxies_uid).ok().cloned()
|
||||
profiles.get_item(&proxies_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -208,7 +254,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_arc();
|
||||
profiles.get_item(groups_uid).ok().cloned()
|
||||
profiles.get_item(&groups_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -284,7 +330,7 @@ fn process_global_items(
|
||||
|
||||
if let ChainType::Script(script) = global_script.data {
|
||||
let mut logs = vec![];
|
||||
match use_script(script, config.to_owned(), profile_name.to_owned()) {
|
||||
match use_script(script, config.to_owned(), profile_name) {
|
||||
Ok((res_config, res_logs)) => {
|
||||
exists_keys.extend(use_keys(&res_config));
|
||||
config = res_config;
|
||||
|
||||
@@ -62,7 +62,7 @@ pub fn use_script(
|
||||
});"#,
|
||||
));
|
||||
|
||||
let config = use_lowercase(config.clone());
|
||||
let config = use_lowercase(config);
|
||||
let config_str = serde_json::to_string(&config)?;
|
||||
|
||||
// 仅处理 name 参数中的特殊字符
|
||||
|
||||
@@ -47,6 +47,7 @@ fn after_change_clash_mode() {
|
||||
for connection in connections_array {
|
||||
let _ = mihomo.close_connection(&connection.id).await;
|
||||
}
|
||||
drop(mihomo);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
core::{CoreManager, handle, hotkey, sysopt, tray},
|
||||
logging_error,
|
||||
module::lightweight,
|
||||
utils::logging::Type,
|
||||
utils::{draft::SharedBox, logging::Type},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use serde_yaml_ng::Mapping;
|
||||
@@ -250,3 +250,9 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_verge_config() -> Result<SharedBox<IVerge>> {
|
||||
let draft = Config::verge().await;
|
||||
let data = draft.data_arc();
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
@@ -2,23 +2,75 @@ use crate::{
|
||||
cmd,
|
||||
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
|
||||
core::{CoreManager, handle, tray},
|
||||
logging,
|
||||
logging, logging_error,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use smartstring::alias::String;
|
||||
use tauri::Emitter;
|
||||
|
||||
/// Toggle proxy profile
|
||||
pub async fn toggle_proxy_profile(profile_index: String) {
|
||||
match cmd::patch_profiles_config_by_profile_index(profile_index).await {
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
cmd::patch_profiles_config_by_profile_index(profile_index).await
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
|
||||
match handle::Handle::mihomo()
|
||||
.await
|
||||
.select_node_for_group(group_name, proxy_name)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let result = tray::Tray::global().update_menu().await;
|
||||
if let Err(err) = result {
|
||||
logging!(error, Type::Tray, "更新菜单失败: {}", err);
|
||||
}
|
||||
logging!(
|
||||
info,
|
||||
Type::Tray,
|
||||
"切换代理成功: {} -> {}",
|
||||
group_name,
|
||||
proxy_name
|
||||
);
|
||||
let _ = handle::Handle::app_handle().emit("verge://refresh-proxy-config", ());
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Tray, "{err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Tray,
|
||||
"切换代理失败: {} -> {}, 错误: {:?}",
|
||||
group_name,
|
||||
proxy_name,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match handle::Handle::mihomo()
|
||||
.await
|
||||
.select_node_for_group(group_name, proxy_name)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Tray,
|
||||
"代理切换回退成功: {} -> {}",
|
||||
group_name,
|
||||
proxy_name
|
||||
);
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Tray,
|
||||
"代理切换最终失败: {} -> {}, 错误: {:?}",
|
||||
group_name,
|
||||
proxy_name,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,13 +143,13 @@ async fn perform_profile_update(
|
||||
let profiles = Config::profiles().await;
|
||||
profiles.latest_arc().is_current_profile_index(uid)
|
||||
};
|
||||
let profile_name = {
|
||||
let profiles = Config::profiles().await;
|
||||
profiles
|
||||
.latest_arc()
|
||||
.get_name_by_uid(uid)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_arc = profiles.latest_arc();
|
||||
let profile_name = profiles_arc
|
||||
.get_name_by_uid(uid)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| String::from("UnKown Profile"));
|
||||
|
||||
let mut last_err;
|
||||
|
||||
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
|
||||
|
||||
@@ -351,23 +351,21 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn handle_window_destroyed() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use crate::core::hotkey::SystemHotkey;
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.await
|
||||
.latest_arc()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
let _ = hotkey::Hotkey::global().reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
use crate::core::hotkey::SystemHotkey;
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.await
|
||||
.latest_arc()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
let _ = hotkey::Hotkey::global().reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,6 +445,7 @@ pub fn run() {
|
||||
tauri::WindowEvent::Focused(focused) => {
|
||||
event_handlers::handle_window_focus(focused);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::WindowEvent::Destroyed => {
|
||||
event_handlers::handle_window_destroyed();
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ fn main() {
|
||||
console_subscriber::init();
|
||||
|
||||
// Check for --no-tray command line argument
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.contains(&"--no-tray".into()) {
|
||||
#[cfg(target_os = "linux")]
|
||||
if std::env::args().any(|x| x == "--no-tray") {
|
||||
unsafe {
|
||||
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ enum LightweightState {
|
||||
impl From<u8> for LightweightState {
|
||||
fn from(v: u8) -> Self {
|
||||
match v {
|
||||
1 => LightweightState::In,
|
||||
2 => LightweightState::Exiting,
|
||||
_ => LightweightState::Normal,
|
||||
1 => Self::In,
|
||||
2 => Self::Exiting,
|
||||
_ => Self::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LightweightState {
|
||||
fn as_u8(self) -> u8 {
|
||||
const fn as_u8(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
@@ -79,40 +79,24 @@ pub fn is_in_lightweight_mode() -> bool {
|
||||
}
|
||||
|
||||
async fn refresh_lightweight_tray_state() {
|
||||
if let Err(err) = Tray::global().update_tray_display().await {
|
||||
if let Err(err) = Tray::global().update_menu().await {
|
||||
logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn auto_lightweight_boot() -> Result<()> {
|
||||
let verge_config = Config::verge().await;
|
||||
let enable_auto = verge_config
|
||||
.latest_arc()
|
||||
let is_enable_auto = verge_config
|
||||
.data_arc()
|
||||
.enable_auto_light_weight_mode
|
||||
.unwrap_or(false);
|
||||
let is_silent_start = verge_config
|
||||
.latest_arc()
|
||||
.enable_silent_start
|
||||
.unwrap_or(false);
|
||||
|
||||
let is_silent_start = verge_config.data_arc().enable_silent_start.unwrap_or(false);
|
||||
if is_enable_auto {
|
||||
enable_auto_light_weight_mode().await;
|
||||
}
|
||||
if is_silent_start {
|
||||
logging!(info, Type::Lightweight, "静默启动:直接进入轻量模式");
|
||||
let _ = entry_lightweight_mode().await;
|
||||
return Ok(());
|
||||
entry_lightweight_mode().await;
|
||||
}
|
||||
|
||||
if !enable_auto {
|
||||
logging!(info, Type::Lightweight, "未开启自动轻量模式,跳过初始化");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
"非静默启动:注册自动轻量模式监听器"
|
||||
);
|
||||
enable_auto_light_weight_mode().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -135,7 +119,7 @@ pub fn disable_auto_light_weight_mode() {
|
||||
|
||||
pub async fn entry_lightweight_mode() -> bool {
|
||||
if !try_transition(LightweightState::Normal, LightweightState::In) {
|
||||
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用");
|
||||
logging!(debug, Type::Lightweight, "无需进入轻量模式,跳过调用");
|
||||
refresh_lightweight_tray_state().await;
|
||||
return false;
|
||||
}
|
||||
@@ -149,7 +133,7 @@ pub async fn entry_lightweight_mode() -> bool {
|
||||
pub async fn exit_lightweight_mode() -> bool {
|
||||
if !try_transition(LightweightState::In, LightweightState::Exiting) {
|
||||
logging!(
|
||||
info,
|
||||
debug,
|
||||
Type::Lightweight,
|
||||
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
|
||||
);
|
||||
@@ -171,10 +155,6 @@ pub async fn add_light_weight_timer() {
|
||||
|
||||
fn setup_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let old_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if old_id != 0 {
|
||||
window.unlisten(old_id);
|
||||
}
|
||||
let handler_id = window.listen("tauri://close-requested", move |_event| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
if let Err(e) = setup_light_weight_timer().await {
|
||||
@@ -196,21 +176,17 @@ fn cancel_window_close_listener() {
|
||||
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
window.unlisten(id);
|
||||
logging!(info, Type::Lightweight, "取消了窗口关闭监听");
|
||||
logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_webview_focus_listener() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let old_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if old_id != 0 {
|
||||
window.unlisten(old_id);
|
||||
}
|
||||
let handler_id = window.listen("tauri://focus", move |_event| {
|
||||
log_err!(cancel_light_weight_timer());
|
||||
logging!(
|
||||
info,
|
||||
debug,
|
||||
Type::Lightweight,
|
||||
"监听到窗口获得焦点,取消轻量模式计时"
|
||||
);
|
||||
@@ -224,7 +200,7 @@ fn cancel_webview_focus_listener() {
|
||||
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
window.unlisten(id);
|
||||
logging!(info, Type::Lightweight, "取消了窗口焦点监听");
|
||||
logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,14 +212,14 @@ async fn setup_light_weight_timer() -> Result<()> {
|
||||
|
||||
let once_by_minutes = Config::verge()
|
||||
.await
|
||||
.latest_arc()
|
||||
.data_arc()
|
||||
.auto_light_weight_minutes
|
||||
.unwrap_or(10);
|
||||
|
||||
{
|
||||
let timer_map = Timer::global().timer_map.read();
|
||||
if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) {
|
||||
logging!(warn, Type::Timer, "轻量模式计时器已存在,跳过创建");
|
||||
logging!(debug, Type::Timer, "轻量模式计时器已存在,跳过创建");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -292,14 +268,17 @@ async fn setup_light_weight_timer() -> Result<()> {
|
||||
}
|
||||
|
||||
fn cancel_light_weight_timer() -> Result<()> {
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
|
||||
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) {
|
||||
delay_timer
|
||||
let value = Timer::global()
|
||||
.timer_map
|
||||
.write()
|
||||
.remove(LIGHT_WEIGHT_TASK_UID);
|
||||
if let Some(task) = value {
|
||||
Timer::global()
|
||||
.delay_timer
|
||||
.write()
|
||||
.remove_task(task.task_id)
|
||||
.context("failed to remove timer task")?;
|
||||
logging!(info, Type::Timer, "计时器已取消");
|
||||
logging!(debug, Type::Timer, "计时器已取消");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Drop for CommandChildGuard {
|
||||
}
|
||||
|
||||
impl CommandChildGuard {
|
||||
pub fn new(child: CommandChild) -> Self {
|
||||
pub const fn new(child: CommandChild) -> Self {
|
||||
Self(Some(child))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use once_cell::sync::OnceCell;
|
||||
#[cfg(unix)]
|
||||
use std::iter;
|
||||
use std::{fs, path::PathBuf};
|
||||
use tauri::Manager;
|
||||
|
||||
@@ -226,8 +228,7 @@ pub fn get_encryption_key() -> Result<Vec<u8>> {
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
|
||||
["/tmp"]
|
||||
.iter()
|
||||
iter::once("/tmp")
|
||||
.map(PathBuf::from)
|
||||
.find(|path| path.exists())
|
||||
.or_else(|| {
|
||||
|
||||
@@ -43,18 +43,20 @@ impl<T: Clone> Draft<T> {
|
||||
{
|
||||
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
|
||||
let mut guard = self.inner.write();
|
||||
if guard.1.is_none() {
|
||||
// 创建草稿 snapshot(Arc clone,cheap)
|
||||
guard.1 = Some(Arc::clone(&guard.0));
|
||||
}
|
||||
// 此时 guaranteed: guard.1 is Some(Arc<Box<T>>)
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let arc_box = guard.1.as_mut().unwrap();
|
||||
let mut draft_arc = if guard.1.is_none() {
|
||||
Arc::clone(&guard.0)
|
||||
} else {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
guard.1.take().unwrap()
|
||||
};
|
||||
drop(guard);
|
||||
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
|
||||
let boxed = Arc::make_mut(arc_box); // &mut Box<T>
|
||||
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
|
||||
// 对 Box<T> 解引用得到 &mut T
|
||||
|
||||
f(&mut **boxed)
|
||||
let result = f(&mut **boxed);
|
||||
// 恢复修改后的草稿 Arc
|
||||
self.inner.write().1 = Some(draft_arc);
|
||||
result
|
||||
}
|
||||
|
||||
/// 将草稿提交到已提交位置(替换),并清除草稿
|
||||
@@ -90,8 +92,7 @@ impl<T: Clone> Draft<T> {
|
||||
let (new_local, res) = f(local).await?;
|
||||
|
||||
// 将新的 Box<T> 放到已提交位置(包进 Arc)
|
||||
let mut guard = self.inner.write();
|
||||
guard.0 = Arc::new(new_local);
|
||||
self.inner.write().0 = Arc::new(new_local);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
|
||||
use crate::{config::with_encryption, enhance::seq::SeqMap, logging, utils::logging::Type};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use nanoid::nanoid;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
@@ -13,7 +13,7 @@ pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
|
||||
let yaml_str = tokio::fs::read_to_string(path).await?;
|
||||
|
||||
Ok(serde_yaml_ng::from_str::<T>(&yaml_str)?)
|
||||
Ok(with_encryption(|| async { serde_yaml_ng::from_str::<T>(&yaml_str) }).await?)
|
||||
}
|
||||
|
||||
/// read mapping from yaml
|
||||
@@ -65,7 +65,7 @@ pub async fn save_yaml<T: Serialize + Sync>(
|
||||
data: &T,
|
||||
prefix: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let data_str = serde_yaml_ng::to_string(data)?;
|
||||
let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?;
|
||||
|
||||
let yaml_str = match prefix {
|
||||
Some(prefix) => format!("{prefix}\n\n{data_str}"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
// #[cfg(not(feature = "tracing"))]
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
use crate::utils::logging::NoModuleFilter;
|
||||
use crate::{
|
||||
@@ -49,7 +49,9 @@ pub async fn init_logger() -> Result<()> {
|
||||
#[cfg(feature = "tracing")]
|
||||
spec.module("tauri", log::LevelFilter::Debug);
|
||||
#[cfg(feature = "tracing")]
|
||||
spec.module("wry", log::LevelFilter::Debug);
|
||||
spec.module("wry", log::LevelFilter::Off);
|
||||
#[cfg(feature = "tracing")]
|
||||
spec.module("tauri_plugin_mihomo", log::LevelFilter::Off);
|
||||
let spec = spec.build();
|
||||
|
||||
let logger = Logger::with(spec)
|
||||
@@ -67,6 +69,12 @@ pub async fn init_logger() -> Result<()> {
|
||||
);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let logger = logger.filter(Box::new(NoModuleFilter(&["wry", "tauri"])));
|
||||
#[cfg(feature = "tracing")]
|
||||
let logger = logger.filter(Box::new(NoModuleFilter(&[
|
||||
"wry",
|
||||
"tauri_plugin_mihomo",
|
||||
"kode_bridge",
|
||||
])));
|
||||
|
||||
let _handle = logger.start()?;
|
||||
|
||||
@@ -498,7 +506,7 @@ pub fn init_scheme() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
pub const fn init_scheme() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ struct IntelGpuDetection {
|
||||
}
|
||||
|
||||
impl IntelGpuDetection {
|
||||
fn should_disable_dmabuf(&self) -> bool {
|
||||
const fn should_disable_dmabuf(&self) -> bool {
|
||||
self.intel_is_primary || self.inconclusive
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ enum NvidiaDmabufDisableReason {
|
||||
}
|
||||
|
||||
impl NvidiaGpuDetection {
|
||||
fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
|
||||
const fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
|
||||
if !session.is_wayland {
|
||||
return None;
|
||||
}
|
||||
@@ -144,11 +144,11 @@ impl DmabufOverrides {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_env_override(&self) -> bool {
|
||||
const fn has_env_override(&self) -> bool {
|
||||
self.dmabuf_override.is_some()
|
||||
}
|
||||
|
||||
fn should_override_env(&self, decision: &DmabufDecision) -> bool {
|
||||
const fn should_override_env(&self, decision: &DmabufDecision) -> bool {
|
||||
if self.user_preference.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -36,24 +36,24 @@ pub enum Type {
|
||||
impl fmt::Display for Type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Type::Cmd => write!(f, "[Cmd]"),
|
||||
Type::Core => write!(f, "[Core]"),
|
||||
Type::Config => write!(f, "[Config]"),
|
||||
Type::Setup => write!(f, "[Setup]"),
|
||||
Type::System => write!(f, "[System]"),
|
||||
Type::Service => write!(f, "[Service]"),
|
||||
Type::Hotkey => write!(f, "[Hotkey]"),
|
||||
Type::Window => write!(f, "[Window]"),
|
||||
Type::Tray => write!(f, "[Tray]"),
|
||||
Type::Timer => write!(f, "[Timer]"),
|
||||
Type::Frontend => write!(f, "[Frontend]"),
|
||||
Type::Backup => write!(f, "[Backup]"),
|
||||
Type::File => write!(f, "[File]"),
|
||||
Type::Lightweight => write!(f, "[Lightweight]"),
|
||||
Type::Network => write!(f, "[Network]"),
|
||||
Type::ProxyMode => write!(f, "[ProxMode]"),
|
||||
Type::Validate => write!(f, "[Validate]"),
|
||||
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"),
|
||||
Self::Cmd => write!(f, "[Cmd]"),
|
||||
Self::Core => write!(f, "[Core]"),
|
||||
Self::Config => write!(f, "[Config]"),
|
||||
Self::Setup => write!(f, "[Setup]"),
|
||||
Self::System => write!(f, "[System]"),
|
||||
Self::Service => write!(f, "[Service]"),
|
||||
Self::Hotkey => write!(f, "[Hotkey]"),
|
||||
Self::Window => write!(f, "[Window]"),
|
||||
Self::Tray => write!(f, "[Tray]"),
|
||||
Self::Timer => write!(f, "[Timer]"),
|
||||
Self::Frontend => write!(f, "[Frontend]"),
|
||||
Self::Backup => write!(f, "[Backup]"),
|
||||
Self::File => write!(f, "[File]"),
|
||||
Self::Lightweight => write!(f, "[Lightweight]"),
|
||||
Self::Network => write!(f, "[Network]"),
|
||||
Self::ProxyMode => write!(f, "[ProxMode]"),
|
||||
Self::Validate => write!(f, "[Validate]"),
|
||||
Self::ClashVergeRev => write!(f, "[ClashVergeRev]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ pub struct HttpResponse {
|
||||
}
|
||||
|
||||
impl HttpResponse {
|
||||
pub fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
|
||||
pub const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
|
||||
Self {
|
||||
status,
|
||||
headers,
|
||||
@@ -34,11 +34,11 @@ impl HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> StatusCode {
|
||||
pub const fn status(&self) -> StatusCode {
|
||||
self.status
|
||||
}
|
||||
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
pub const fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
@@ -80,8 +80,7 @@ impl NetworkManager {
|
||||
}
|
||||
|
||||
async fn record_connection_error(&self, error: &str) {
|
||||
let mut last_error = self.last_connection_error.lock().await;
|
||||
*last_error = Some((Instant::now(), error.into()));
|
||||
*self.last_connection_error.lock().await = Some((Instant::now(), error.into()));
|
||||
|
||||
let mut count = self.connection_error_count.lock().await;
|
||||
*count += 1;
|
||||
@@ -89,13 +88,11 @@ impl NetworkManager {
|
||||
|
||||
async fn should_reset_clients(&self) -> bool {
|
||||
let count = *self.connection_error_count.lock().await;
|
||||
let last_error_guard = self.last_connection_error.lock().await;
|
||||
|
||||
if count > 5 {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some((time, _)) = &*last_error_guard
|
||||
if let Some((time, _)) = &*self.last_connection_error.lock().await
|
||||
&& time.elapsed() < Duration::from_secs(30)
|
||||
&& count > 2
|
||||
{
|
||||
@@ -119,18 +116,15 @@ impl NetworkManager {
|
||||
accept_invalid_certs: bool,
|
||||
timeout_secs: Option<u64>,
|
||||
) -> Result<HttpClient> {
|
||||
let proxy_uri_clone = proxy_uri.clone();
|
||||
let headers_clone = default_headers.clone();
|
||||
|
||||
{
|
||||
let mut builder = HttpClient::builder();
|
||||
|
||||
builder = match proxy_uri_clone {
|
||||
builder = match proxy_uri {
|
||||
Some(uri) => builder.proxy(Some(uri)),
|
||||
None => builder.proxy(None),
|
||||
};
|
||||
|
||||
for (name, value) in headers_clone.iter() {
|
||||
for (name, value) in default_headers.iter() {
|
||||
builder = builder.default_header(name, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use smartstring::alias::String;
|
||||
use tauri::Url;
|
||||
|
||||
use crate::{
|
||||
config::{PrfItem, profiles},
|
||||
config::{Config, PrfItem, profiles},
|
||||
core::handle,
|
||||
logging, logging_error,
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
|
||||
@@ -29,55 +31,79 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
||||
let name_owned: Option<String> = link_parsed
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.into_owned().into());
|
||||
let name = name_owned.as_ref();
|
||||
let (url_param, name) =
|
||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
||||
let name_owned: Option<String> = link_parsed
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.into_owned().into());
|
||||
|
||||
let url_param = if let Some(query) = link_parsed.query() {
|
||||
let prefix = "url=";
|
||||
if let Some(pos) = query.find(prefix) {
|
||||
let raw_url = &query[pos + prefix.len()..];
|
||||
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
|
||||
let url_param = if let Some(query) = link_parsed.query() {
|
||||
let prefix = "url=";
|
||||
if let Some(pos) = query.find(prefix) {
|
||||
let raw_url = &query[pos + prefix.len()..];
|
||||
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
(url_param, name_owned)
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
match url_param {
|
||||
Some(ref url) => {
|
||||
logging!(info, Type::Config, "decoded subscription url: {url}");
|
||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
||||
Ok(mut item) => {
|
||||
let uid = match item.uid.clone() {
|
||||
Some(uid) => uid,
|
||||
None => {
|
||||
logging!(error, Type::Config, "Profile item missing UID");
|
||||
handle::Handle::notice_message(
|
||||
"import_sub_url::error",
|
||||
"Profile item missing UID".to_string(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let result = profiles::profiles_append_item_safe(&mut item).await;
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
"failed to import subscription url: {:?}",
|
||||
result
|
||||
);
|
||||
handle::Handle::notice_message("import_sub_url::ok", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None => bail!("failed to get profile url"),
|
||||
let url = if let Some(ref url) = url_param {
|
||||
url
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"missing url parameter in deep link: {}",
|
||||
param_str
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut item = match PrfItem::from_url(url, name.as_ref(), None, None).await {
|
||||
Ok(item) => item,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"failed to parse profile from url: {:?}",
|
||||
e
|
||||
);
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let uid = item.uid.clone().unwrap_or_default();
|
||||
match profiles::profiles_append_item_safe(&mut item).await {
|
||||
Ok(_) => {
|
||||
Config::profiles().await.apply();
|
||||
let _ = Config::profiles().await.data_arc().save_file().await;
|
||||
handle::Handle::notice_message(
|
||||
"import_sub_url::ok",
|
||||
"", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
);
|
||||
handle::Handle::refresh_verge();
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"failed to import subscription url: {:?}",
|
||||
e
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,9 +54,7 @@ fn get_ui_ready_notify() -> &'static Arc<Notify> {
|
||||
// 更新UI准备阶段
|
||||
pub fn update_ui_ready_stage(stage: UiReadyStage) {
|
||||
let state = get_ui_ready_state();
|
||||
let mut stage_lock = state.stage.write();
|
||||
|
||||
*stage_lock = stage;
|
||||
*state.stage.write() = stage;
|
||||
// 如果是最终阶段,标记UI完全就绪
|
||||
if stage == UiReadyStage::Ready {
|
||||
mark_ui_ready();
|
||||
|
||||
@@ -31,6 +31,8 @@ pub async fn check_singleton() -> Result<()> {
|
||||
let client = ClientBuilder::new()
|
||||
.timeout(Duration::from_millis(500))
|
||||
.build()?;
|
||||
// 需要确保 Send
|
||||
#[allow(clippy::needless_collect)]
|
||||
let argvs: Vec<std::string::String> = std::env::args().collect();
|
||||
if argvs.len() > 1 {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
||||
@@ -81,6 +81,7 @@ fn should_handle_window_operation() -> bool {
|
||||
|
||||
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
|
||||
*last_operation = now;
|
||||
drop(last_operation);
|
||||
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
|
||||
logging!(info, Type::Window, "[防抖] 窗口操作被允许执行");
|
||||
true
|
||||
|
||||
@@ -71,8 +71,9 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
setOpen(false);
|
||||
showNotice("success", t("Port settings saved"));
|
||||
},
|
||||
onError: () => {
|
||||
showNotice("error", t("Failed to save port settings"));
|
||||
onError: (e) => {
|
||||
showNotice("error", e.message || t("Failed to save port settings"));
|
||||
// showNotice("error", t("Failed to save port settings"));
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -70,8 +70,8 @@ export const useClashInfo = () => {
|
||||
|
||||
if (patch["redir-port"]) {
|
||||
const port = patch["redir-port"];
|
||||
if (port < 1111) {
|
||||
throw new Error("The port should not < 1111");
|
||||
if (port < 1000) {
|
||||
throw new Error("The port should not < 1000");
|
||||
}
|
||||
if (port > 65536) {
|
||||
throw new Error("The port should not > 65536");
|
||||
@@ -80,8 +80,8 @@ export const useClashInfo = () => {
|
||||
|
||||
if (patch["tproxy-port"]) {
|
||||
const port = patch["tproxy-port"];
|
||||
if (port < 1111) {
|
||||
throw new Error("The port should not < 1111");
|
||||
if (port < 1000) {
|
||||
throw new Error("The port should not < 1000");
|
||||
}
|
||||
if (port > 65536) {
|
||||
throw new Error("The port should not > 65536");
|
||||
@@ -90,8 +90,8 @@ export const useClashInfo = () => {
|
||||
|
||||
if (patch["mixed-port"]) {
|
||||
const port = patch["mixed-port"];
|
||||
if (port < 1111) {
|
||||
throw new Error("The port should not < 1111");
|
||||
if (port < 1000) {
|
||||
throw new Error("The port should not < 1000");
|
||||
}
|
||||
if (port > 65536) {
|
||||
throw new Error("The port should not > 65536");
|
||||
@@ -100,8 +100,8 @@ export const useClashInfo = () => {
|
||||
|
||||
if (patch["socks-port"]) {
|
||||
const port = patch["socks-port"];
|
||||
if (port < 1111) {
|
||||
throw new Error("The port should not < 1111");
|
||||
if (port < 1000) {
|
||||
throw new Error("The port should not < 1000");
|
||||
}
|
||||
if (port > 65536) {
|
||||
throw new Error("The port should not > 65536");
|
||||
@@ -110,8 +110,8 @@ export const useClashInfo = () => {
|
||||
|
||||
if (patch["port"]) {
|
||||
const port = patch["port"];
|
||||
if (port < 1111) {
|
||||
throw new Error("The port should not < 1111");
|
||||
if (port < 1000) {
|
||||
throw new Error("The port should not < 1000");
|
||||
}
|
||||
if (port > 65536) {
|
||||
throw new Error("The port should not > 65536");
|
||||
|
||||
@@ -11,7 +11,10 @@ export const handleNoticeMessage = (
|
||||
) => {
|
||||
const handlers: Record<string, () => void> = {
|
||||
"import_sub_url::ok": () => {
|
||||
navigate("/profile", { state: { current: msg } });
|
||||
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
|
||||
// navigate("/profile", { state: { current: msg } });
|
||||
navigate("/profile");
|
||||
showNotice("success", t("Import Subscription Successful"));
|
||||
},
|
||||
"import_sub_url::error": () => {
|
||||
|
||||
@@ -13,15 +13,15 @@ import {
|
||||
sortableKeyboardCoordinates,
|
||||
} from "@dnd-kit/sortable";
|
||||
import {
|
||||
CheckBoxOutlineBlankRounded,
|
||||
CheckBoxRounded,
|
||||
ClearRounded,
|
||||
ContentPasteRounded,
|
||||
DeleteRounded,
|
||||
IndeterminateCheckBoxRounded,
|
||||
LocalFireDepartmentRounded,
|
||||
RefreshRounded,
|
||||
TextSnippetOutlined,
|
||||
CheckBoxOutlineBlankRounded,
|
||||
CheckBoxRounded,
|
||||
IndeterminateCheckBoxRounded,
|
||||
DeleteRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material";
|
||||
|
||||
2
src/services/types.d.ts
vendored
2
src/services/types.d.ts
vendored
@@ -801,7 +801,7 @@ interface IVergeConfig {
|
||||
common_tray_icon?: boolean;
|
||||
sysproxy_tray_icon?: boolean;
|
||||
tun_tray_icon?: boolean;
|
||||
enable_tray_speed?: boolean;
|
||||
// enable_tray_speed?: boolean;
|
||||
// enable_tray_icon?: boolean;
|
||||
tray_inline_proxy_groups?: boolean;
|
||||
enable_tun_mode?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user