Compare commits

...
11 Commits
Author SHA1 Message Date
Tunglies 787463a226 chore: bump version to 2.4.4 2025-12-19 16:25:24 +08:00
Slinetrac 8d5d72957b fix(home): correct delay sorting for Current Node card #5870
Closes #5870
2025-12-19 14:24:04 +08:00
Slinetrac 7d805cb83e chore(deps): bump npm deps 2025-12-19 10:32:40 +08:00
oomeow a1286ad057 fix: always occupies hotkey globally when app launch silently (#5866)
* fix: always occupies hotkey when app launch silently

* docs: update Changelog.md

* chore: update
2025-12-18 22:21:15 +08:00
oomeow 9713343323 refactor: use sysproxy-rs to set system proxy on windows (#5846)
* refactor: use sysproxy-rs to set system proxy on windows

* fix: remove download sysproxy.exe task

* chore: unified processing system proxy reset

* docs: update Changelog.md
2025-12-18 20:23:29 +08:00
wonfen b282217e5c doc: update promotion 2025-12-18 18:42:18 +08:00
Slinetrac 4b29a140b5 docs: Changelog.md 2025-12-17 16:57:34 +08:00
Slinetrac b6af7b7440 fix(tun): set auto-redirect default to false to improve cross-distro compatibility #5845
Closes #5845
2025-12-17 16:20:06 +08:00
Tunglies bcb8e831c5 fix(runtime): aovid force re-reresh runtime after create proxy chain 2025-12-17 12:52:25 +08:00
Slinetrac a9e6391417 feat(layout/nav): add reset-to-default menu order action 2025-12-17 11:33:31 +08:00
Slinetrac 721929a2a1 refactor(layout/nav): extract nav menu order logic into hook 2025-12-17 11:06:38 +08:00
38 changed files with 673 additions and 709 deletions
Generated
+3 -3
View File
@@ -1186,7 +1186,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "clash-verge"
version = "2.4.4-rc.1"
version = "2.4.4"
dependencies = [
"aes-gcm",
"anyhow",
@@ -7385,8 +7385,8 @@ dependencies = [
[[package]]
name = "sysproxy"
version = "0.4.1"
source = "git+https://github.com/clash-verge-rev/sysproxy-rs#279a5cf1c4470d0b28af9a5c3fb7c2fcb503a906"
version = "0.4.2"
source = "git+https://github.com/clash-verge-rev/sysproxy-rs#51256d5921a01bbcf24fe5629296d2bfe493329d"
dependencies = [
"interfaces",
"iptools",
+5
View File
@@ -29,6 +29,8 @@
- 修复 Windows 下自定义标题栏按钮在最小化 / 关闭后 hover 状态残留
- 修复直接覆盖 `config.yaml` 使用时无法展开代理组
- 修复 macOS 下应用启动时系统托盘图标颜色闪烁
- 修复应用静默启动模式下非全局热键一直抢占其他应用按键问题
- 修复首页当前节点卡片按延迟排序时,打开节点列表后,`timeout` 节点被排在正常节点前的问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
@@ -39,6 +41,8 @@
- 日志页面支持按时间倒序
- 增加「重新激活订阅」的全局快捷键
- WebView2 Runtime 修复构建升级到 133.0.3065.92
- 侧边栏右键新增「恢复默认排序」
- Linux 下新增对 TUN 「自动重定向」(`auto-redirect` 字段)的配置支持,默认关闭
</details>
@@ -65,6 +69,7 @@
- 改进旧版 Service 需要重新安装检测流程
- 优化 macOS, Linux 和 Windows 系统信号处理
- 链式代理仅显示 Selector 类型规则组
- 优化 Windows 系统代理设置,不再依赖 `sysproxy.exe` 来设置代理
</details>
+16 -10
View File
@@ -48,17 +48,23 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
## Promotion
#### [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
### ✈️ [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
- 海外团队,无跑路风险,高达 50% 返佣
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
- 解锁流媒体及 ChatGPT
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
🚀 高性能海外技术流机场,支持免费试用与优惠套餐,全面解锁流媒体及 AI 服务,全球首家采用 **QUIC 协议**。
🎁 使用 **Clash Verge 专属邀请链接** 注册即送 **3 天免费试用**,每日 **1GB 流量**:👉 [点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### **核心优势:**
- 📱 自研 iOS 客户端(业内"唯一")技术经得起考验,极大**持续研发**投入
- 🧑‍💻 **12小时真人客服**(顺带解决 Clash Verge 使用问题)
- 💰 优惠套餐每月**仅需 21 元,160G 流量,年付 8 折**
- 🌍 海外团队,无跑路风险,高达 50% 返佣
- ⚙️ **集群负载均衡**设计,**负载监控和随时扩容**,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
- ⚡ 全球首家**Quic 协议机场**,现已上线更快的 Tuic 协议(Clash Verge 客户端最佳搭配)
- 🎬 解锁**流媒体及 主流 AI**
🌐 官网:👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
+16 -10
View File
@@ -51,17 +51,23 @@ Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
## Promotion
#### [Doggygo VPN — Performance-oriented global accelerator](https://verge.dginv.click/#/register?code=oaxsAGo6)
### ✈️ [Doggygo VPN — A Technical-Grade Proxy Service](https://verge.dginv.click/#/register?code=oaxsAGo6)
- High-performance overseas network service with free trials, discounted plans, streaming unlocks, and first-class Hysteria protocol support.
- Register through the exclusive Clash Verge link to get a 3-day trial with 1 GB of traffic per day: [Sign up](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Exclusive 20% off coupon for Clash Verge users: `verge20` (limited to 500 uses)
- Discounted bundle from ¥15.8 per month for 160 GB, plus an additional 20% off for yearly billing
- Operated by an overseas team with reliable service and up to 50% revenue share
- Load-balanced clusters with high-speed dedicated routes (compatible with legacy clients), exceptionally low latency, smooth 4K playback
- First global provider to support the `Hysteria2` protocol—perfect fit for the Clash Verge client
- Supports streaming services and ChatGPT access
- Official site: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
🚀 A high-performance, overseas, technical-grade proxy service offering free trials and discounted plans, fully unlocking streaming platforms and AI services. The world’s first provider to adopt the **QUIC protocol**.
🎁 Register via the **Clash Verge exclusive invitation link** to receive **3 days of free trial**, with **1GB traffic per day**: 👉 [Register here](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### **Core Advantages:**
- 📱 Self-developed iOS client (the industry’s “only one”), with technology proven in production and **significant ongoing R&D investment**
- 🧑‍💻 **12-hour live customer support** (also assists with Clash Verge usage issues)
- 💰 Discounted plans at **only CNY 21 per month, 160GB traffic, 20% off with annual billing**
- 🌍 Overseas team, no risk of shutdown or exit scams, with up to **50% referral commission**
- ⚙️ **Cluster-based load balancing** architecture with **real-time load monitoring and elastic scaling**, high-speed dedicated lines (compatible with legacy clients), ultra-low latency, unaffected by peak hours, **4K streaming loads instantly**
- ⚡ The world’s first **QUIC-protocol-based proxy service**, now upgraded with the faster **Tuic protocol** (best paired with the Clash Verge client)
- 🎬 Unlocks **streaming platforms and mainstream AI services**
🌐 Official Website: 👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Build Infrastructure Sponsor — [YXVM Dedicated Servers](https://yxvm.com/aff.php?aff=827)
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.4.4-rc.1",
"version": "2.4.4",
"license": "GPL-3.0-only",
"scripts": {
"prepare": "husky || true",
@@ -59,7 +59,7 @@
"foxact": "^0.2.49",
"i18next": "^25.7.3",
"js-yaml": "^4.1.1",
"lodash-es": "^4.17.21",
"lodash-es": "^4.17.22",
"monaco-editor": "^0.55.1",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.6",
@@ -69,7 +69,7 @@
"react-hook-form": "^7.68.0",
"react-i18next": "16.5.0",
"react-markdown": "10.1.0",
"react-router": "^7.10.1",
"react-router": "^7.11.0",
"react-virtuoso": "^4.17.0",
"swr": "^2.3.8",
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
@@ -97,7 +97,7 @@
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.25",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-unused-imports": "^4.3.0",
"glob": "^13.0.0",
"globals": "^16.5.0",
@@ -107,7 +107,7 @@
"lint-staged": "^16.2.7",
"node-fetch": "^3.3.2",
"prettier": "^3.7.4",
"sass": "^1.96.0",
"sass": "^1.97.0",
"tar": "^7.5.2",
"terser": "^5.44.1",
"typescript": "^5.9.3",
@@ -125,7 +125,7 @@
]
},
"type": "module",
"packageManager": "pnpm@10.26.0",
"packageManager": "pnpm@10.26.1",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
+287 -287
View File
File diff suppressed because it is too large Load Diff
+1 -12
View File
@@ -576,7 +576,7 @@ const resolveServicePermission = async () => {
};
// =======================
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback)
// =======================
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
@@ -624,11 +624,6 @@ const resolveEnableLoopback = () =>
file: "enableLoopback.exe",
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
});
const resolveWinSysproxy = () =>
resolveResource({
file: "sysproxy.exe",
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
});
const resolveSetDnsScript = () =>
resolveResource({
@@ -676,12 +671,6 @@ const tasks = [
retry: 5,
unixOnly: platform === "linux" || platform === "darwin",
},
{
name: "windows-sysproxy",
func: resolveWinSysproxy,
retry: 5,
winOnly: true,
},
{
name: "set_dns_script",
func: resolveSetDnsScript,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.4.4-rc.1"
version = "2.4.4"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
+1 -1
View File
@@ -99,7 +99,7 @@ pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option<ser
runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
// 我们需要在 CoreManager 中验证并应用配置,这里不应该直接调用 runtime.apply()
}
logging_error!(Type::Core, CoreManager::global().update_config().await);
logging_error!(Type::Core, CoreManager::global().apply_generate_confihg().await);
Ok(())
}
+6 -2
View File
@@ -287,8 +287,12 @@ singleton!(Hotkey, INSTANCE);
impl Hotkey {
pub async fn init(&self, skip: bool) -> Result<()> {
if skip {
logging!(debug, Type::Hotkey, "skip register all hotkeys");
return Ok(());
}
let verge = Config::verge().await;
let enable_global_hotkey = !skip && verge.data_arc().enable_global_hotkey.unwrap_or(true);
let enable_global_hotkey = verge.latest_arc().enable_global_hotkey.unwrap_or(true);
logging!(
debug,
@@ -298,7 +302,7 @@ impl Hotkey {
);
// Extract hotkeys data before async operations
let hotkeys = verge.data_arc().hotkeys.clone();
let hotkeys = verge.latest_arc().hotkeys.clone();
if let Some(hotkeys) = hotkeys {
logging!(debug, Type::Hotkey, "Has {} hotkeys need to register", hotkeys.len());
+3
View File
@@ -60,7 +60,10 @@ impl CoreManager {
async fn perform_config_update(&self) -> Result<(bool, String)> {
Config::generate().await?;
self.apply_generate_confihg().await
}
pub async fn apply_generate_confihg(&self) -> Result<(bool, String)> {
match CoreConfigValidator::global().validate_config().await {
Ok((true, _)) => {
let run_path = Config::generate_file(ConfigType::Run).await?;
+48 -116
View File
@@ -23,7 +23,6 @@ use tauri_plugin_autostart::ManagerExt as _;
pub struct Sysopt {
update_sysproxy: AtomicBool,
reset_sysproxy: AtomicBool,
#[cfg(not(target_os = "windows"))]
inner_proxy: Arc<RwLock<(Sysproxy, Autoproxy)>>,
guard: Arc<RwLock<GuardMonitor>>,
}
@@ -33,7 +32,6 @@ impl Default for Sysopt {
Self {
update_sysproxy: AtomicBool::new(false),
reset_sysproxy: AtomicBool::new(false),
#[cfg(not(target_os = "windows"))]
inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))),
guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))),
}
@@ -69,35 +67,6 @@ async fn get_bypass() -> String {
}
}
// Uses tokio Command with CREATE_NO_WINDOW flag to avoid DLL initialization issues during shutdown
#[cfg(target_os = "windows")]
async fn execute_sysproxy_command(args: Vec<std::string::String>) -> Result<()> {
use crate::utils::dirs;
use anyhow::bail;
#[allow(unused_imports)] // Required for .creation_flags() method
use std::os::windows::process::CommandExt as _;
use tokio::process::Command;
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
bail!("sysproxy.exe not found");
}
let output = Command::new(sysproxy_exe)
.args(args)
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
.output()
.await?;
if !output.status.success() {
bail!("sysproxy exe run failed");
}
Ok(())
}
singleton!(Sysopt, SYSOPT);
impl Sysopt {
@@ -112,12 +81,12 @@ impl Sysopt {
pub async fn refresh_guard(&self) {
logging!(info, Type::Core, "Refreshing system proxy guard...");
let verge = Config::verge().await.latest_arc();
if !verge.enable_system_proxy.unwrap_or(false) {
if !verge.enable_system_proxy.unwrap_or_default() {
logging!(info, Type::Core, "System proxy is disabled.");
self.access_guard().write().stop();
return;
}
if !verge.enable_proxy_guard.unwrap_or(false) {
if !verge.enable_proxy_guard.unwrap_or_default() {
logging!(info, Type::Core, "System proxy guard is disabled.");
return;
}
@@ -169,94 +138,62 @@ impl Sysopt {
};
let pac_port = IVerge::get_singleton_port();
let (sys_enable, pac_enable, proxy_host) = {
let (sys_enable, pac_enable, proxy_host, proxy_guard) = {
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
verge.enable_system_proxy.unwrap_or_default(),
verge.proxy_auto_config.unwrap_or_default(),
verge.proxy_host.clone().unwrap_or_else(|| String::from("127.0.0.1")),
verge.enable_proxy_guard.unwrap_or_default(),
)
};
#[cfg(not(target_os = "windows"))]
{
// 先 await, 避免持有锁导致的 Send 问题
let bypass = get_bypass().await;
// 先 await, 避免持有锁导致的 Send 问题
let bypass = get_bypass().await;
let (sys, auto) = &mut *self.inner_proxy.write();
let (sys, auto) = &mut *self.inner_proxy.write();
sys.enable = false;
sys.host = proxy_host.clone().into();
sys.port = port;
sys.bypass = bypass.into();
auto.enable = false;
auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac");
self.access_guard().write().set_guard_type(GuardType::None);
if !sys_enable && !pac_enable {
// disable proxy
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
return Ok(());
}
if pac_enable {
sys.enable = false;
sys.host = proxy_host.clone().into();
sys.port = port;
sys.bypass = bypass.into();
auto.enable = false;
auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac");
if !sys_enable {
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
self.access_guard()
.write()
.set_guard_type(GuardType::Sysproxy(sys.clone()));
return Ok(());
}
if pac_enable {
sys.enable = false;
auto.enable = true;
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
auto.enable = true;
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
if proxy_guard {
self.access_guard()
.write()
.set_guard_type(GuardType::Autoproxy(auto.clone()));
return Ok(());
}
return Ok(());
}
if sys_enable {
auto.enable = false;
sys.enable = true;
auto.set_auto_proxy()?;
sys.set_system_proxy()?;
if sys_enable {
auto.enable = false;
sys.enable = true;
auto.set_auto_proxy()?;
sys.set_system_proxy()?;
if proxy_guard {
self.access_guard()
.write()
.set_guard_type(GuardType::Sysproxy(sys.clone()));
return Ok(());
}
return Ok(());
}
#[cfg(target_os = "windows")]
{
if !sys_enable {
self.access_guard().write().set_guard_type(GuardType::None);
return self.reset_sysproxy().await;
}
let (args, guard_type): (Vec<std::string::String>, GuardType) = if pac_enable {
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
(
vec!["pac".into(), address.clone()],
GuardType::Autoproxy(Autoproxy {
enable: true,
url: address,
}),
)
} else {
let address = format!("{proxy_host}:{port}");
let bypass = get_bypass().await;
let bypass_for_guard = bypass.as_str().to_owned();
(
vec!["global".into(), address.clone(), bypass.into()],
GuardType::Sysproxy(Sysproxy {
enable: true,
host: proxy_host.clone().into(),
port,
bypass: bypass_for_guard,
}),
)
};
execute_sysproxy_command(args).await?;
self.access_guard().write().set_guard_type(guard_type);
}
Ok(())
}
@@ -274,20 +211,15 @@ impl Sysopt {
self.reset_sysproxy.store(false, Ordering::SeqCst);
}
//直接关闭所有代理
#[cfg(not(target_os = "windows"))]
{
let (sys, auto) = &mut *self.inner_proxy.write();
sys.enable = false;
sys.set_system_proxy()?;
auto.enable = false;
auto.set_auto_proxy()?;
}
// close proxy guard
self.access_guard().write().set_guard_type(GuardType::None);
#[cfg(target_os = "windows")]
{
execute_sysproxy_command(vec!["set".into(), "1".into()]).await?;
}
// 直接关闭所有代理
let (sys, auto) = &mut *self.inner_proxy.write();
sys.enable = false;
sys.set_system_proxy()?;
auto.enable = false;
auto.set_auto_proxy()?;
Ok(())
}
+17 -85
View File
@@ -45,93 +45,25 @@ pub async fn clean_async() -> bool {
// 重置系统代理
let proxy_task = tokio::task::spawn(async {
#[cfg(target_os = "windows")]
{
use sysproxy::{Autoproxy, Sysproxy};
use winapi::um::winuser::{GetSystemMetrics, SM_SHUTTINGDOWN};
// 检查系统代理是否开启
let sys_proxy_enabled = Config::verge().await.data_arc().enable_system_proxy.unwrap_or(false);
if !sys_proxy_enabled {
logging!(info, Type::Window, "系统代理未启用,跳过重置");
return true;
}
// 检查是否正在关机
let is_shutting_down = unsafe { GetSystemMetrics(SM_SHUTTINGDOWN) != 0 };
if is_shutting_down {
// sysproxy-rs 操作注册表(避免.exe的dll错误)
logging!(info, Type::Window, "检测到正在关机,syspro-rs操作注册表关闭系统代理");
match Sysproxy::get_system_proxy() {
Ok(mut sysproxy) => {
sysproxy.enable = false;
if let Err(e) = sysproxy.set_system_proxy() {
logging!(warn, Type::Window, "Warning: 关机时关闭系统代理失败: {e}");
} else {
logging!(info, Type::Window, "系统代理已关闭(通过注册表)");
}
}
Err(e) => {
logging!(warn, Type::Window, "Warning: 关机时获取代理设置失败: {e}");
}
}
// 关闭自动代理配置
if let Ok(mut autoproxy) = Autoproxy::get_auto_proxy() {
autoproxy.enable = false;
let _ = autoproxy.set_auto_proxy();
}
return true;
}
// 正常退出:使用 sysproxy.exe 重置代理
logging!(info, Type::Window, "sysproxy.exe重置系统代理");
match timeout(Duration::from_secs(2), sysopt::Sysopt::global().reset_sysproxy()).await {
Ok(Ok(_)) => {
logging!(info, Type::Window, "系统代理已重置");
true
}
Ok(Err(e)) => {
logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}");
true
}
Err(_) => {
logging!(warn, Type::Window, "Warning: 重置系统代理超时,继续退出流程");
true
}
}
let sys_proxy_enabled = Config::verge().await.data_arc().enable_system_proxy.unwrap_or(false);
if !sys_proxy_enabled {
logging!(info, Type::Window, "系统代理未启用,跳过重置");
return true;
}
// 非 Windows 平台:正常重置代理
#[cfg(not(target_os = "windows"))]
{
let sys_proxy_enabled = Config::verge().await.data_arc().enable_system_proxy.unwrap_or(false);
if !sys_proxy_enabled {
logging!(info, Type::Window, "系统代理未启用,跳过重置");
return true;
logging!(info, Type::Window, "开始重置系统代理...");
match timeout(Duration::from_millis(1500), sysopt::Sysopt::global().reset_sysproxy()).await {
Ok(Ok(_)) => {
logging!(info, Type::Window, "系统代理已重置");
true
}
logging!(info, Type::Window, "开始重置系统代理...");
match timeout(Duration::from_millis(1500), sysopt::Sysopt::global().reset_sysproxy()).await {
Ok(Ok(_)) => {
logging!(info, Type::Window, "系统代理已重置");
true
}
Ok(Err(e)) => {
logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}");
true
}
Err(_) => {
logging!(warn, Type::Window, "Warning: 重置系统代理超时,继续退出");
true
}
Ok(Err(e)) => {
logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}");
false
}
Err(_) => {
logging!(warn, Type::Window, "Warning: 重置系统代理超时,继续退出");
false
}
}
});
@@ -183,7 +115,7 @@ pub async fn clean_async() -> bool {
Type::Window,
"Warning: 停止core超时(可能系统正在关机),继续退出"
);
true
false
}
}
});
+3 -1
View File
@@ -336,7 +336,9 @@ pub fn run() {
.register_system_hotkey(SystemHotkey::CmdW)
.await;
}
let _ = hotkey::Hotkey::global().init(true).await;
if !is_enable_global_hotkey {
let _ = hotkey::Hotkey::global().init(false).await;
}
return;
}
+3 -1
View File
@@ -131,7 +131,9 @@ pub(super) async fn init_timer() {
}
pub(super) async fn init_hotkey() {
logging_error!(Type::Setup, Hotkey::global().init(false).await);
// if hotkey is not use by global, skip init it
let skip_register_hotkeys = !Config::verge().await.latest_arc().enable_global_hotkey.unwrap_or(true);
logging_error!(Type::Setup, Hotkey::global().init(skip_register_hotkeys).await);
}
pub(super) async fn init_auto_lightweight_boot() {
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "2.4.4-rc.1",
"version": "2.4.4",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
+29 -9
View File
@@ -111,6 +111,7 @@ export const CurrentProxyCard = () => {
const { verge } = useVerge();
const { current: currentProfile } = useProfiles();
const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false;
const defaultLatencyTimeout = verge?.default_latency_timeout;
const currentProfileId = currentProfile?.uid || null;
const getProfileStorageKey = useCallback(
@@ -742,20 +743,38 @@ export const CurrentProxyCard = () => {
if (sortType === 1) {
const refreshTick = delaySortRefresh;
const effectiveTimeout =
typeof defaultLatencyTimeout === "number" && defaultLatencyTimeout > 0
? defaultLatencyTimeout
: 10000;
const categorizeDelay = (delay: number): [number, number] => {
if (!Number.isFinite(delay)) return [5, Number.MAX_SAFE_INTEGER];
if (delay > 1e5) return [4, delay];
if (delay === 0 || (delay >= effectiveTimeout && delay <= 1e5)) {
return [3, delay || effectiveTimeout];
}
if (delay < 0) return [5, Number.MAX_SAFE_INTEGER];
return [0, delay];
};
list.sort((a, b) => {
const recordA = state.proxyData.records[a.name];
const recordB = state.proxyData.records[b.name];
if (!recordA) return 1;
if (!recordB) return -1;
const [ar, av] = recordA
? categorizeDelay(
delayManager.getDelayFix(recordA, state.selection.group),
)
: [6, Number.MAX_SAFE_INTEGER];
const [br, bv] = recordB
? categorizeDelay(
delayManager.getDelayFix(recordB, state.selection.group),
)
: [6, Number.MAX_SAFE_INTEGER];
const ad = delayManager.getDelayFix(recordA, state.selection.group);
const bd = delayManager.getDelayFix(recordB, state.selection.group);
if (ad === -1 || ad === -2) return 1;
if (bd === -1 || bd === -2) return -1;
if (ad !== bd) return ad - bd;
if (ar !== br) return ar - br;
if (av !== bv) return av - bv;
return refreshTick >= 0 ? a.name.localeCompare(b.name) : 0;
});
} else {
@@ -800,6 +819,7 @@ export const CurrentProxyCard = () => {
state.selection.group,
sortType,
delaySortRefresh,
defaultLatencyTimeout,
]);
// 获取排序图标
+4 -4
View File
@@ -33,7 +33,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
stack: "mixed",
device: OS === "macos" ? "utun1024" : "Mihomo",
autoRoute: true,
autoRedirect: OS === "linux",
autoRedirect: false,
autoDetectInterface: true,
dnsHijack: ["any:53"],
strictRoute: false,
@@ -44,7 +44,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
open: () => {
setOpen(true);
const nextAutoRoute = clash?.tun["auto-route"] ?? true;
const rawAutoRedirect = clash?.tun["auto-redirect"] ?? true;
const rawAutoRedirect = clash?.tun["auto-redirect"] ?? false;
const computedAutoRedirect =
OS === "linux" ? (nextAutoRoute ? rawAutoRedirect : false) : false;
setValues({
@@ -118,7 +118,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
"auto-route": true,
...(OS === "linux"
? {
"auto-redirect": true,
"auto-redirect": false,
}
: {}),
"auto-detect-interface": true,
@@ -130,7 +130,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
stack: "gvisor",
device: OS === "macos" ? "utun1024" : "Mihomo",
autoRoute: true,
autoRedirect: OS === "linux" ? true : false,
autoRedirect: false,
autoDetectInterface: true,
dnsHijack: ["any:53"],
strictRoute: false,
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "메뉴 재정렬 모드",
"restoreDefaultOrder": "Restore default order",
"unlock": "메뉴 순서 잠금 해제",
"lock": "메뉴 순서 잠금"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "菜单排序模式",
"restoreDefaultOrder": "恢复默认排序",
"unlock": "解锁菜单排序",
"lock": "锁定菜单排序"
}
+1
View File
@@ -13,6 +13,7 @@
},
"menu": {
"reorderMode": "選單排序模式",
"restoreDefaultOrder": "恢復預設排序",
"unlock": "解鎖選單排序",
"lock": "鎖定選單排序"
}
+6
View File
@@ -101,3 +101,9 @@ window.addEventListener("beforeunload", () => {
// Clean up all WebSocket instances to prevent memory leaks
MihomoWebSocket.cleanupAll();
});
// Page loaded event
window.addEventListener("DOMContentLoaded", () => {
// Clean up all WebSocket instances to prevent memory leaks
MihomoWebSocket.cleanupAll();
});
+37 -112
View File
@@ -1,6 +1,5 @@
import {
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
closestCenter,
@@ -9,7 +8,6 @@ import {
} from "@dnd-kit/core";
import {
SortableContext,
arrayMove,
sortableKeyboardCoordinates,
useSortable,
} from "@dnd-kit/sortable";
@@ -25,14 +23,7 @@ import {
} from "@mui/material";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties } from "react";
import { useTranslation } from "react-i18next";
import { Outlet, useNavigate } from "react-router";
@@ -58,6 +49,7 @@ import {
useAppInitialization,
useLayoutEvents,
useLoadingOverlay,
useNavMenuOrder,
} from "./_layout/hooks";
import { handleNoticeMessage } from "./_layout/utils";
import { navItems } from "./_routers";
@@ -69,52 +61,7 @@ export const portableFlag = false;
type NavItem = (typeof navItems)[number];
const createNavLookup = (items: NavItem[]) => {
const map = new Map(items.map((item) => [item.path, item]));
const defaultOrder = items.map((item) => item.path);
return { map, defaultOrder };
};
const resolveMenuOrder = (
order: string[] | null | undefined,
defaultOrder: string[],
map: Map<string, NavItem>,
) => {
const seen = new Set<string>();
const resolved: string[] = [];
if (Array.isArray(order)) {
for (const path of order) {
if (map.has(path) && !seen.has(path)) {
resolved.push(path);
seen.add(path);
}
}
}
for (const path of defaultOrder) {
if (!seen.has(path)) {
resolved.push(path);
seen.add(path);
}
}
return resolved;
};
const areOrdersEqual = (a: string[], b: string[]) =>
a.length === b.length && a.every((value, index) => value === b[index]);
type MenuContextPosition = { top: number; left: number };
type MenuOrderAction = { type: "sync"; payload: string[] };
const menuOrderReducer = (state: string[], action: MenuOrderAction) => {
const next = action.payload;
if (areOrdersEqual(state, next)) {
return state;
}
return [...next];
};
interface SortableNavMenuItemProps {
item: NavItem;
@@ -192,69 +139,35 @@ const Layout = () => {
}),
);
const { map: navItemMap, defaultOrder: defaultMenuOrder } = useMemo(
() => createNavLookup(navItems),
[],
);
const configMenuOrder = useMemo(
() => resolveMenuOrder(verge?.menu_order, defaultMenuOrder, navItemMap),
[verge?.menu_order, defaultMenuOrder, navItemMap],
);
const [menuOrder, dispatchMenuOrder] = useReducer(
menuOrderReducer,
configMenuOrder,
);
useEffect(() => {
dispatchMenuOrder({ type: "sync", payload: configMenuOrder });
}, [configMenuOrder]);
const handleMenuDragEnd = useCallback(
async (event: DragEndEvent) => {
if (!menuUnlocked) {
return;
}
const { active, over } = event;
if (!over || active.id === over.id) {
return;
}
const activeId = String(active.id);
const overId = String(over.id);
const oldIndex = menuOrder.indexOf(activeId);
const newIndex = menuOrder.indexOf(overId);
if (oldIndex === -1 || newIndex === -1) {
return;
}
const previousOrder = [...menuOrder];
const nextOrder = arrayMove(menuOrder, oldIndex, newIndex);
dispatchMenuOrder({ type: "sync", payload: nextOrder });
const handleMenuOrderOptimisticUpdate = useCallback(
(order: string[]) => {
mutateVerge(
(prev) => (prev ? { ...prev, menu_order: nextOrder } : prev),
(prev) => (prev ? { ...prev, menu_order: order } : prev),
false,
);
try {
await patchVerge({ menu_order: nextOrder });
} catch (error) {
console.error("Failed to update menu order:", error);
dispatchMenuOrder({ type: "sync", payload: previousOrder });
mutateVerge(
(prev) => (prev ? { ...prev, menu_order: previousOrder } : prev),
false,
);
}
},
[menuUnlocked, menuOrder, mutateVerge, patchVerge],
[mutateVerge],
);
const handleMenuOrderPersist = useCallback(
(order: string[]) => patchVerge({ menu_order: order }),
[patchVerge],
);
const {
menuOrder,
navItemMap,
handleMenuDragEnd,
isDefaultOrder,
resetMenuOrder,
} = useNavMenuOrder({
enabled: menuUnlocked,
items: navItems,
storedOrder: verge?.menu_order,
onOptimisticUpdate: handleMenuOrderOptimisticUpdate,
onPersist: handleMenuOrderPersist,
});
const handleMenuContextMenu = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
@@ -268,6 +181,11 @@ const Layout = () => {
setMenuContextPosition(null);
}, []);
const handleResetMenuOrder = useCallback(() => {
setMenuContextPosition(null);
void resetMenuOrder();
}, [resetMenuOrder]);
const handleUnlockMenu = useCallback(() => {
setMenuUnlocked(true);
setMenuContextPosition(null);
@@ -520,6 +438,13 @@ const Layout = () => {
? t("layout.components.navigation.menu.lock")
: t("layout.components.navigation.menu.unlock")}
</MenuItem>
<MenuItem
onClick={handleResetMenuOrder}
dense
disabled={isDefaultOrder}
>
{t("layout.components.navigation.menu.restoreDefaultOrder")}
</MenuItem>
</Menu>
<div className="the-traffic">
+1
View File
@@ -1,3 +1,4 @@
export { useAppInitialization } from "./use-app-initialization";
export { useLayoutEvents } from "./use-layout-events";
export { useLoadingOverlay } from "./use-loading-overlay";
export { useNavMenuOrder } from "./use-nav-menu-order";
@@ -0,0 +1,155 @@
import type { DragEndEvent } from "@dnd-kit/core";
import { arrayMove } from "@dnd-kit/sortable";
import { useCallback, useEffect, useMemo, useReducer } from "react";
type MenuOrderAction = { type: "sync"; payload: string[] };
const areOrdersEqual = (a: string[], b: string[]) =>
a.length === b.length && a.every((value, index) => value === b[index]);
const menuOrderReducer = (state: string[], action: MenuOrderAction) => {
const next = action.payload;
if (areOrdersEqual(state, next)) {
return state;
}
return [...next];
};
const createNavLookup = <T extends { path: string }>(items: readonly T[]) => {
const map = new Map(items.map((item) => [item.path, item] as const));
const defaultOrder = items.map((item) => item.path);
return { map, defaultOrder };
};
const resolveMenuOrder = <T extends { path: string }>(
order: string[] | null | undefined,
defaultOrder: string[],
map: Map<string, T>,
) => {
const seen = new Set<string>();
const resolved: string[] = [];
if (Array.isArray(order)) {
for (const path of order) {
if (map.has(path) && !seen.has(path)) {
resolved.push(path);
seen.add(path);
}
}
}
for (const path of defaultOrder) {
if (!seen.has(path)) {
resolved.push(path);
seen.add(path);
}
}
return resolved;
};
interface UseNavMenuOrderOptions<T extends { path: string }> {
enabled: boolean;
items: readonly T[];
storedOrder: string[] | null | undefined;
onOptimisticUpdate?: (order: string[]) => void;
onPersist: (order: string[]) => Promise<void>;
}
export const useNavMenuOrder = <T extends { path: string }>({
enabled,
items,
storedOrder,
onOptimisticUpdate,
onPersist,
}: UseNavMenuOrderOptions<T>) => {
const { map: navItemMap, defaultOrder } = useMemo(
() => createNavLookup(items),
[items],
);
const configMenuOrder = useMemo(
() => resolveMenuOrder(storedOrder, defaultOrder, navItemMap),
[storedOrder, defaultOrder, navItemMap],
);
const [menuOrder, dispatchMenuOrder] = useReducer(
menuOrderReducer,
configMenuOrder,
);
useEffect(() => {
dispatchMenuOrder({ type: "sync", payload: configMenuOrder });
}, [configMenuOrder]);
const isDefaultOrder = useMemo(
() => areOrdersEqual(menuOrder, defaultOrder),
[menuOrder, defaultOrder],
);
const handleMenuDragEnd = useCallback(
async (event: DragEndEvent) => {
if (!enabled) {
return;
}
const { active, over } = event;
if (!over || active.id === over.id) {
return;
}
const activeId = String(active.id);
const overId = String(over.id);
const oldIndex = menuOrder.indexOf(activeId);
const newIndex = menuOrder.indexOf(overId);
if (oldIndex === -1 || newIndex === -1) {
return;
}
const previousOrder = [...menuOrder];
const nextOrder = arrayMove(menuOrder, oldIndex, newIndex);
dispatchMenuOrder({ type: "sync", payload: nextOrder });
onOptimisticUpdate?.(nextOrder);
try {
await onPersist(nextOrder);
} catch (error) {
console.error("Failed to update menu order:", error);
dispatchMenuOrder({ type: "sync", payload: previousOrder });
onOptimisticUpdate?.(previousOrder);
}
},
[enabled, menuOrder, onOptimisticUpdate, onPersist],
);
const resetMenuOrder = useCallback(async () => {
if (isDefaultOrder) {
return;
}
const previousOrder = [...menuOrder];
const nextOrder = [...defaultOrder];
dispatchMenuOrder({ type: "sync", payload: nextOrder });
onOptimisticUpdate?.(nextOrder);
try {
await onPersist(nextOrder);
} catch (error) {
console.error("Failed to reset menu order:", error);
dispatchMenuOrder({ type: "sync", payload: previousOrder });
onOptimisticUpdate?.(previousOrder);
}
}, [defaultOrder, isDefaultOrder, menuOrder, onOptimisticUpdate, onPersist]);
return {
menuOrder,
navItemMap,
handleMenuDragEnd,
isDefaultOrder,
resetMenuOrder,
};
};
+1
View File
@@ -105,6 +105,7 @@ export const translationKeys = [
"layout.components.navigation.tabs.unlock",
"layout.components.navigation.tabs.settings",
"layout.components.navigation.menu.reorderMode",
"layout.components.navigation.menu.restoreDefaultOrder",
"layout.components.navigation.menu.unlock",
"layout.components.navigation.menu.lock",
"logs.page.title",
+1
View File
@@ -181,6 +181,7 @@ export interface TranslationResources {
menu: {
lock: string;
reorderMode: string;
restoreDefaultOrder: string;
unlock: string;
};
tabs: {
+9 -48
View File
@@ -1,53 +1,9 @@
import { KeyboardEvent } from "react";
import { debugLog } from "@/utils/debug";
import getSystem from "./get-system";
const KEY_MAP: Record<string, string> = {
// Option + 特殊字符映射
"–": "Minus", // Option + -
"≠": "Equal", // Option + =
"\u201C": "BracketLeft", // Option + [
"\u2019": "BracketRight", // Option + ]
"«": "Backslash", // Option + \
"…": "Semicolon", // Option + ;
æ: "Quote", // Option + '
"≤": "Comma", // Option + ,
"≥": "Period", // Option + .
"÷": "Slash", // Option + /
const OS = getSystem();
// Option组合键映射
Å: "A",
"∫": "B",
Ç: "C",
"∂": "D",
"´": "E",
ƒ: "F",
"©": "G",
"˙": "H",
ˆ: "I",
"∆": "J",
"˚": "K",
"¬": "L",
µ: "M",
"˜": "N",
Ø: "O",
π: "P",
Œ: "Q",
"®": "R",
ß: "S",
"†": "T",
"¨": "U",
"√": "V",
"∑": "W",
"≈": "X",
"¥": "Y",
Ω: "Z",
};
const mapKeyCombination = (key: string): string => {
const mappedKey = KEY_MAP[key] || key;
return `${mappedKey}`;
};
export const parseHotkey = (keyEvent: KeyboardEvent) => {
const nativeEvent = keyEvent.nativeEvent;
const key = nativeEvent.code;
@@ -64,16 +20,21 @@ export const parseHotkey = (keyEvent: KeyboardEvent) => {
} else if (temp.endsWith("RIGHT")) {
temp = temp.slice(0, -5);
}
debugLog(temp, mapKeyCombination(temp));
switch (temp) {
case "CONTROL":
return "CTRL";
case "ALT":
if (OS === "macos") {
return "OPTION";
} else {
return "ALT";
}
case "META":
return "CMD";
case " ":
return "SPACE";
default:
return KEY_MAP[temp] || temp;
return temp;
}
};