mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-16 14:20:53 +08:00
Compare commits
11
Commits
v2.4.4-rc.1
...
v2.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
787463a226 | ||
|
|
8d5d72957b | ||
|
|
7d805cb83e | ||
|
|
a1286ad057 | ||
|
|
9713343323 | ||
|
|
b282217e5c | ||
|
|
4b29a140b5 | ||
|
|
b6af7b7440 | ||
|
|
bcb8e831c5 | ||
|
|
a9e6391417 | ||
|
|
721929a2a1 |
Generated
+3
-3
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Generated
+287
-287
File diff suppressed because it is too large
Load Diff
+1
-12
@@ -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,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"
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
// 获取排序图标
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "메뉴 재정렬 모드",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "메뉴 순서 잠금 해제",
|
||||
"lock": "메뉴 순서 잠금"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "菜单排序模式",
|
||||
"restoreDefaultOrder": "恢复默认排序",
|
||||
"unlock": "解锁菜单排序",
|
||||
"lock": "锁定菜单排序"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "選單排序模式",
|
||||
"restoreDefaultOrder": "恢復預設排序",
|
||||
"unlock": "解鎖選單排序",
|
||||
"lock": "鎖定選單排序"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,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,
|
||||
};
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -181,6 +181,7 @@ export interface TranslationResources {
|
||||
menu: {
|
||||
lock: string;
|
||||
reorderMode: string;
|
||||
restoreDefaultOrder: string;
|
||||
unlock: string;
|
||||
};
|
||||
tabs: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user