mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-20 17:22:08 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71429f9a42 |
@@ -4,11 +4,11 @@
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "24.22.0",
|
||||
"pnpmVersion": "12.14.0"
|
||||
"version": "24.21.0",
|
||||
"pnpmVersion": "12.11.2"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.99.1",
|
||||
"version": "1.99.0",
|
||||
"profile": "default"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git:1": {},
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.1'
|
||||
toolchain: '1.99.0'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.1'
|
||||
toolchain: '1.99.0'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -347,7 +347,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -426,7 +426,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -524,7 +524,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6.1.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
|
||||
- name: Install Rust Stable
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: dtolnay/rust-toolchain@1.99.1
|
||||
uses: dtolnay/rust-toolchain@1.99.0
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Restore pnpm cache
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.1'
|
||||
toolchain: '1.99.0'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -311,7 +311,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.1'
|
||||
toolchain: '1.99.0'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -331,7 +331,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
@@ -389,7 +389,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -446,7 +446,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.1'
|
||||
toolchain: '1.99.0'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -466,7 +466,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -487,7 +487,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -547,7 +547,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -573,7 +573,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -638,7 +638,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -22,13 +22,13 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.1'
|
||||
toolchain: '1.99.0'
|
||||
targets: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.21.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
nodejs 24.22.0
|
||||
pnpm 12.14.0
|
||||
rust 1.99.1
|
||||
nodejs 24.21.0
|
||||
pnpm 12.11.2
|
||||
rust 1.99.0
|
||||
|
||||
Generated
+254
-471
File diff suppressed because it is too large
Load Diff
+14
-5
@@ -49,7 +49,15 @@ split-debuginfo = "unpacked"
|
||||
[patch.crates-io]
|
||||
# Backport Windows menu command ID recycling on the muda 0.20 compatibility branch.
|
||||
muda = { git = "https://github.com/wonfen/muda.git", rev = "547de49352791653fbdfa785f20f3c7ece8468e2" }
|
||||
|
||||
# tauri-specta has no tauri 3 release yet; ride the tauri team's port and the
|
||||
# unpublished specta rc.26 line it pins. Drop all six entries once both ship
|
||||
# on crates.io.
|
||||
tauri-specta = { git = "https://github.com/tauri-apps/tauri-specta", branch = "tauri-v3" }
|
||||
specta = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||
specta-macros = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||
specta-serde = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||
specta-typescript = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||
specta-util = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||
[workspace.dependencies]
|
||||
clash-verge-draft = { path = "crates/clash-verge-draft" }
|
||||
clash-verge-media-unlock = { path = "crates/clash-verge-media-unlock" }
|
||||
@@ -61,13 +69,14 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
|
||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||
tracing = "0.1.44"
|
||||
|
||||
tauri = { version = "2.12.1" }
|
||||
tauri-plugin-clipboard-manager = "2.4.1"
|
||||
tauri = { version = "3.0.0-alpha.4" }
|
||||
tauri-runtime-wry = "3.0.0-alpha.4"
|
||||
tauri-plugin-clipboard-manager = "3.0.0-alpha.2"
|
||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||
anyhow = "1.0.104"
|
||||
chrono = "0.4.45"
|
||||
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
||||
tokio = { version = "1.53.3", features = [
|
||||
tokio = { version = "1.53.2", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
@@ -88,7 +97,7 @@ rust_iso3166 = "0.2.0"
|
||||
smartstring = { version = "1.0.1" }
|
||||
|
||||
serde = { version = "1.0.229" }
|
||||
serde_json = { version = "1.0.154" }
|
||||
serde_json = { version = "1.0.151" }
|
||||
serde_yaml_ng = { version = "0.10.0" }
|
||||
bitflags = { version = "2.13.2" }
|
||||
socket2 = "0.6.5"
|
||||
|
||||
+1
-9
@@ -3,13 +3,7 @@
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
||||
|
||||
**🐧 Linux**
|
||||
|
||||
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
|
||||
|
||||
</details>
|
||||
|
||||
@@ -22,8 +16,6 @@
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
|
||||
- 优化「订阅更新」:在刷新订阅自身内容的同时,一并刷新订阅配置文件名称
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
|
||||
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
|
||||
clash-verge-logging = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
//! Linux-only termination signal bridge.
|
||||
//!
|
||||
//! Upstream Tauri delivers session-end and termination events to the app on
|
||||
//! Windows and macOS, but has no equivalent on Linux: a desktop session may
|
||||
//! end without any window or run event reaching the process. Linux builds
|
||||
//! keep this crate to listen for SIGTERM, SIGINT and SIGHUP and forward them
|
||||
//! to the app quit flow, so system proxy and TUN cleanup still runs before
|
||||
//! the process dies. Non-Linux builds compile this crate to nothing and must
|
||||
//! not depend on it.
|
||||
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::sync::{
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -17,13 +5,20 @@ use std::sync::{
|
||||
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
|
||||
pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new();
|
||||
|
||||
/// Serializes signal-triggered cleanups: a signal arriving while a quit
|
||||
/// attempt is still in flight is dropped instead of overlapping it.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ShutdownOutcome {
|
||||
Committed,
|
||||
Canceled,
|
||||
}
|
||||
|
||||
pub(crate) struct ShutdownLatch {
|
||||
is_cleaning_up: AtomicBool,
|
||||
}
|
||||
@@ -41,18 +36,17 @@ impl ShutdownLatch {
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn finish(&self) {
|
||||
self.is_cleaning_up.store(false, Ordering::Release);
|
||||
fn finish(&self, outcome: ShutdownOutcome) {
|
||||
if matches!(outcome, ShutdownOutcome::Canceled) {
|
||||
self.is_cleaning_up.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the Linux signal loop on a dedicated runtime so it survives the
|
||||
/// teardown of any app-owned runtime. The handler runs once per accepted
|
||||
/// signal; the quit flow it invokes owns the exit-or-cancel decision.
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||
{
|
||||
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => Some(rt),
|
||||
@@ -67,22 +61,36 @@ where
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(unix)]
|
||||
unix::register(f);
|
||||
|
||||
#[cfg(windows)]
|
||||
windows::register(f);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ShutdownLatch;
|
||||
use super::{ShutdownLatch, ShutdownOutcome};
|
||||
|
||||
#[test]
|
||||
fn finished_attempt_releases_latch_for_the_next_signal() {
|
||||
fn canceled_shutdown_releases_latch_and_permits_next_attempt() {
|
||||
let latch = ShutdownLatch::new();
|
||||
|
||||
assert!(latch.try_begin());
|
||||
assert!(!latch.try_begin());
|
||||
|
||||
latch.finish();
|
||||
latch.finish(ShutdownOutcome::Canceled);
|
||||
|
||||
assert!(latch.try_begin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_shutdown_retains_latch_and_suppresses_repeats() {
|
||||
let latch = ShutdownLatch::new();
|
||||
|
||||
assert!(latch.try_begin());
|
||||
latch.finish(ShutdownOutcome::Committed);
|
||||
|
||||
assert!(!latch.try_begin());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
|
||||
use crate::{RUNTIME, SHUTDOWN_LATCH};
|
||||
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
|
||||
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||
{
|
||||
if let Some(Some(rt)) = RUNTIME.get() {
|
||||
rt.spawn(async move {
|
||||
@@ -60,8 +60,7 @@ where
|
||||
}
|
||||
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
|
||||
|
||||
f().await;
|
||||
SHUTDOWN_LATCH.finish();
|
||||
SHUTDOWN_LATCH.finish(f().await);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use tokio::signal::windows;
|
||||
|
||||
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
|
||||
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||
{
|
||||
if let Some(Some(rt)) = RUNTIME.get() {
|
||||
rt.spawn(async move {
|
||||
let mut ctrl_c = match windows::ctrl_c() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+C: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctrl_close = match windows::ctrl_close() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Close: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctrl_shutdown = match windows::ctrl_shutdown() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Shutdown: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctrl_logoff = match windows::ctrl_logoff() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Logoff: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
let signal_name;
|
||||
tokio::select! {
|
||||
_ = ctrl_c.recv() => {
|
||||
signal_name = "Ctrl+C";
|
||||
}
|
||||
_ = ctrl_close.recv() => {
|
||||
signal_name = "Ctrl+Close";
|
||||
}
|
||||
_ = ctrl_shutdown.recv() => {
|
||||
signal_name = "Ctrl+Shutdown";
|
||||
}
|
||||
_ = ctrl_logoff.recv() => {
|
||||
signal_name = "Ctrl+Logoff";
|
||||
}
|
||||
}
|
||||
|
||||
if !SHUTDOWN_LATCH.try_begin() {
|
||||
logging!(
|
||||
info,
|
||||
Type::SystemSignal,
|
||||
"Already shutting down, ignoring repeated signal: {}",
|
||||
signal_name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
logging!(info, Type::SystemSignal, "Caught Windows signal: {}", signal_name);
|
||||
|
||||
SHUTDOWN_LATCH.finish(f().await);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"register shutdown signal failed, RUNTIME is not available"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
rust-version = "1.91"
|
||||
|
||||
[dependencies]
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
|
||||
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
@@ -15,7 +15,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
sysinfo = { version = "0.39.6", features = ["network", "system"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc = "0.2.191"
|
||||
libc = "0.2.190"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
deelevate = { workspace = true }
|
||||
|
||||
+14
-14
@@ -54,13 +54,13 @@
|
||||
"@mui/icons-material": "^9.4.0",
|
||||
"@mui/material": "^9.4.0",
|
||||
"@tanstack/react-virtual": "^3.14.13",
|
||||
"@tauri-apps/api": "2.12.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.4.0",
|
||||
"@tauri-apps/plugin-dialog": "2.8.0",
|
||||
"@tauri-apps/plugin-fs": "2.6.0",
|
||||
"@tauri-apps/plugin-http": "2.8.0",
|
||||
"@tauri-apps/plugin-opener": "2.7.0",
|
||||
"@tauri-apps/plugin-updater": "2.13.1",
|
||||
"@tauri-apps/api": "3.0.0-alpha.2",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^3.0.0-alpha.2",
|
||||
"@tauri-apps/plugin-dialog": "^3.0.0-alpha.3",
|
||||
"@tauri-apps/plugin-fs": "^3.0.0-alpha.2",
|
||||
"@tauri-apps/plugin-http": "~3.0.0-alpha.2",
|
||||
"@tauri-apps/plugin-opener": "3.0.0-alpha.2",
|
||||
"@tauri-apps/plugin-updater": "3.0.0-alpha.2",
|
||||
"ahooks": "^3.10.0",
|
||||
"cidr-block": "^2.3.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
@@ -75,11 +75,11 @@
|
||||
"monaco-yaml": "^5.5.1",
|
||||
"nanoid": "^6.0.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "19.3.1",
|
||||
"react-dom": "19.3.1",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0",
|
||||
"react-error-boundary": "6.1.6",
|
||||
"react-hook-form": "^7.88.0",
|
||||
"react-i18next": "17.0.19",
|
||||
"react-i18next": "17.0.15",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "^8.4.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
@@ -95,7 +95,7 @@
|
||||
"@biomejs/biome": "^2.5.14",
|
||||
"@eslint-react/eslint-plugin": "^5.20.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tauri-apps/cli": "2.12.1",
|
||||
"@tauri-apps/cli": "3.0.0-alpha.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.13.6",
|
||||
"@types/react": "19.3.0",
|
||||
@@ -123,7 +123,7 @@
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.7.0",
|
||||
"knip": "6.41.0",
|
||||
"knip": "6.39.0",
|
||||
"lint-staged": "^17.5.1",
|
||||
"sass": "^1.104.1",
|
||||
"tar": "^7.5.22",
|
||||
@@ -133,7 +133,7 @@
|
||||
"vite": "^8.3.0",
|
||||
"vite-plugin-svgr": "^5.2.0",
|
||||
"vitest": "^5.0.1",
|
||||
"webdriverio": "10.0.2",
|
||||
"webdriverio": "10.0.0",
|
||||
"ws": "8.22.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -146,5 +146,5 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.14.0"
|
||||
"packageManager": "pnpm@12.11.2"
|
||||
}
|
||||
|
||||
Generated
+401
-400
File diff suppressed because it is too large
Load Diff
@@ -10,3 +10,15 @@ allowBuilds:
|
||||
unrs-resolver: true
|
||||
minimumReleaseAgeExclude:
|
||||
- dayjs@1.11.21
|
||||
- '@tauri-apps/cli-darwin-arm64@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-darwin-x64@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-linux-arm-gnueabihf@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-linux-arm64-gnu@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-linux-arm64-musl@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-linux-riscv64-gnu@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-linux-x64-gnu@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-linux-x64-musl@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-win32-arm64-msvc@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-win32-ia32-msvc@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli-win32-x64-msvc@3.0.0-alpha.4'
|
||||
- '@tauri-apps/cli@3.0.0-alpha.4'
|
||||
|
||||
@@ -45,17 +45,6 @@
|
||||
"matchManagers": ["npm"],
|
||||
"groupName": "npm dependencies"
|
||||
},
|
||||
{
|
||||
"description": "Update Tauri npm packages and Cargo crates together so frontend and backend stay in sync",
|
||||
"matchPackageNames": [
|
||||
"@tauri-apps/**",
|
||||
"@wdio/tauri-service",
|
||||
"tauri",
|
||||
"tauri-*"
|
||||
],
|
||||
"groupName": "tauri",
|
||||
"addLabels": ["tauri"]
|
||||
},
|
||||
{
|
||||
"description": "Group all GitHub Actions updates into a single PR",
|
||||
"matchManagers": ["github-actions"],
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.99.1"
|
||||
channel = "1.99.0"
|
||||
components = ["rustfmt", "clippy"]
|
||||
|
||||
@@ -22,7 +22,7 @@ try {
|
||||
'run',
|
||||
'-p',
|
||||
'clash-verge',
|
||||
'--example',
|
||||
'--bin',
|
||||
'frontend-contract',
|
||||
'--locked',
|
||||
'-q',
|
||||
|
||||
+27
-29
@@ -23,30 +23,33 @@ tauri-dev = ["dep:tauri-plugin-devtools"]
|
||||
tokio-trace = ["clash-verge-logging/tokio-trace"]
|
||||
chrome-trace = ["clash-verge-logging/chrome-trace"]
|
||||
clippy = ["tauri/test"]
|
||||
perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
|
||||
# The perf-harness feature and its main.rs wiring are parked until
|
||||
# tauri-plugin-wdio-webdriver publishes a tauri 3 release; restore both
|
||||
# from git history together with the crate dependency.
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.7.1", features = [] }
|
||||
tauri-build = { version = "3.0.0-alpha.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
clash-verge-draft = { workspace = true }
|
||||
clash-verge-media-unlock = { workspace = true }
|
||||
clash-verge-logging = { workspace = true }
|
||||
clash-verge-signal = { workspace = true }
|
||||
clash-verge-i18n = { workspace = true }
|
||||
clash-verge-limiter = { workspace = true }
|
||||
tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
tauri = { workspace = true, features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tracing",
|
||||
"tray-icon",
|
||||
"image-ico",
|
||||
"image-png",
|
||||
] }
|
||||
tauri-runtime-wry = { workspace = true, features = ["devtools"] }
|
||||
parking_lot = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -72,55 +75,50 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
|
||||
"guard",
|
||||
] }
|
||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.4.0"
|
||||
tauri-plugin-opener = "2.7.0"
|
||||
tauri-plugin-dialog = "2.8.1"
|
||||
tauri-plugin-fs = "2.6.0"
|
||||
tauri-plugin-process = "2.4.0"
|
||||
tauri-plugin-deep-link = "2.6.1"
|
||||
tauri-plugin-window-state = "2.5.0"
|
||||
zip = "9.0.4"
|
||||
tauri-plugin-shell = "3.0.0-alpha.2"
|
||||
tauri-plugin-opener = "3.0.0-alpha.2"
|
||||
tauri-plugin-dialog = "3.0.0-alpha.3"
|
||||
tauri-plugin-fs = "3.0.0-alpha.2"
|
||||
tauri-plugin-process = "3.0.0-alpha.2"
|
||||
tauri-plugin-deep-link = "3.0.0-alpha.2"
|
||||
tauri-plugin-window-state = "3.0.0-alpha.2"
|
||||
zip = "9.0.1"
|
||||
flate2 = "1.1.10"
|
||||
reqwest_dav = "0.3.3"
|
||||
aes-gcm = { version = "0.11.1" }
|
||||
base64 = "0.23.1"
|
||||
minisign-verify = "0.3.0"
|
||||
sha2 = "0.11.1"
|
||||
sha2 = "0.11"
|
||||
getrandom = "0.4.3"
|
||||
futures = "0.3.34"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.5.1"
|
||||
tauri-plugin-notification = "3.0.0-alpha.2"
|
||||
tokio-stream = "0.1.19"
|
||||
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||
tauri-plugin-http = "2.8.0"
|
||||
tauri-plugin-http = "3.0.0-alpha.2"
|
||||
tracing = { workspace = true }
|
||||
tauri-plugin-devtools = { version = "2.2.2", optional = true }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
||||
tauri-plugin-devtools = { version = "3.0.0-alpha.0", optional = true }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "feat/tauri-v3" }
|
||||
async-trait = "0.1.92"
|
||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [
|
||||
"client",
|
||||
] }
|
||||
arc-swap = "1.9.2"
|
||||
tokio-rustls = "0.26.6"
|
||||
rustls = { version = "0.23.46", features = ["ring"] }
|
||||
rustls = { version = "0.23.45", features = ["ring"] }
|
||||
webpki-roots = "1.0.9"
|
||||
dark-light = "3.0.0"
|
||||
bytes = "1.12.1"
|
||||
tokio-util = { version = "0.7.20", features = ["time"] }
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
|
||||
tokio-util = { version = "0.7.19", features = ["time"] }
|
||||
specta = { version = "=2.0.0-rc.26", features = ["derive", "function", "serde_json"] }
|
||||
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
|
||||
specta-typescript = "=0.0.12"
|
||||
specta-typescript = "=0.0.13"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.191"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
|
||||
clash-verge-signal = { workspace = true }
|
||||
libc = "0.2.190"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
|
||||
objc2 = "0.6.5"
|
||||
objc2-foundation = { version = "0.3.2", features = [
|
||||
"NSString",
|
||||
@@ -152,9 +150,9 @@ windows = { version = "0.62.2", features = ["Win32_Globalization"] }
|
||||
windows-sys.workspace = true
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.7.0"
|
||||
tauri-plugin-global-shortcut = "2.4.0"
|
||||
tauri-plugin-updater = "2.13.1"
|
||||
tauri-plugin-autostart = "3.0.0-alpha.2"
|
||||
tauri-plugin-global-shortcut = "3.0.0-alpha.2"
|
||||
tauri-plugin-updater = "3.0.0-alpha.2"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"dialog:allow-open",
|
||||
"global-shortcut:allow-is-registered",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-register-all",
|
||||
"global-shortcut:allow-unregister",
|
||||
"global-shortcut:allow-unregister-all",
|
||||
"process:allow-exit",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Dev-only target: Tauri ships everything under src/bin/ to /usr/bin in deb/rpm
|
||||
// bundles (PR #8131), so this generator must stay an example, never a bin.
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args_os().skip(1);
|
||||
let output = args
|
||||
@@ -282,9 +282,7 @@ async fn handle_validation_failure(
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", outcome);
|
||||
discard_and_restore(current_profile).await?;
|
||||
if !matches!(outcome, ValidationOutcome::Busy) {
|
||||
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
|
||||
}
|
||||
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
@@ -313,6 +311,9 @@ async fn perform_config_update(
|
||||
current_value: Option<&String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let update_result = run_profile_config_update_transition(|| CoreManager::global().update_config_forced()).await;
|
||||
|
||||
match update_result {
|
||||
@@ -408,9 +409,6 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let profiles = IProfiles {
|
||||
|
||||
@@ -294,7 +294,6 @@ impl IProfiles {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
each.home = item.home.to_owned();
|
||||
each.name = item.name.clone();
|
||||
each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref());
|
||||
if let Some(file_data) = item.file_data.take() {
|
||||
let file = each.file.take();
|
||||
|
||||
@@ -24,7 +24,7 @@ pub async fn update_launch() -> Result<()> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let app_handle = Handle::app_handle();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
let autostart_manager = app_handle.autostart();
|
||||
if is_enable {
|
||||
logging_error!(Type::System, "{:?}", autostart_manager.enable());
|
||||
} else {
|
||||
|
||||
@@ -23,8 +23,9 @@ use smartstring::alias::String;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tauri::Manager as _;
|
||||
use tauri::{
|
||||
AppHandle, Wry,
|
||||
AppHandle, DynRuntime,
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
};
|
||||
|
||||
@@ -36,7 +37,7 @@ use menu_def::{MenuIds, MenuTexts};
|
||||
|
||||
// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑)
|
||||
|
||||
type ProxyMenuItem = (Option<Submenu<Wry>>, Vec<Box<dyn IsMenuItem<Wry>>>);
|
||||
type ProxyMenuItem = (Option<Submenu<DynRuntime>>, Vec<Box<dyn IsMenuItem<DynRuntime>>>);
|
||||
|
||||
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
||||
pub const TRAY_ID: &str = "clash-verge-rev-tray";
|
||||
@@ -57,14 +58,14 @@ enum IconKind {
|
||||
}
|
||||
|
||||
struct LatencyMenuItem {
|
||||
item: MenuItem<Wry>,
|
||||
item: MenuItem<DynRuntime>,
|
||||
running: bool,
|
||||
proxies: Vec<ProxyLatencyItem>,
|
||||
provider_state: ProxyViewProviderState,
|
||||
}
|
||||
|
||||
struct ProxyLatencyItem {
|
||||
item: CheckMenuItem<Wry>,
|
||||
item: CheckMenuItem<DynRuntime>,
|
||||
name: std::string::String,
|
||||
provider_name: Option<std::string::String>,
|
||||
text: std::string::String,
|
||||
@@ -566,7 +567,7 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<&str, &str> {
|
||||
fn create_profile_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
profiles_preview: Vec<IProfilePreview<'_>>,
|
||||
) -> Result<Vec<CheckMenuItem<Wry>>> {
|
||||
) -> Result<Vec<CheckMenuItem<DynRuntime>>> {
|
||||
profiles_preview
|
||||
.into_iter()
|
||||
.map(|profile| {
|
||||
@@ -677,7 +678,7 @@ fn create_subcreate_proxy_menu_item(
|
||||
texts: &MenuTexts,
|
||||
verge: &IVerge,
|
||||
profiles: &crate::config::IProfiles,
|
||||
) -> Vec<Submenu<Wry>> {
|
||||
) -> Vec<Submenu<DynRuntime>> {
|
||||
let Some(view) = view else { return Vec::new() };
|
||||
view.groups
|
||||
.iter()
|
||||
@@ -739,8 +740,8 @@ fn create_subcreate_proxy_menu_item(
|
||||
},
|
||||
);
|
||||
let separator = PredefinedMenuItem::separator(app_handle).ok()?;
|
||||
let mut items: Vec<&dyn IsMenuItem<Wry>> = vec![&test_item, &separator];
|
||||
items.extend(group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>));
|
||||
let mut items: Vec<&dyn IsMenuItem<DynRuntime>> = vec![&test_item, &separator];
|
||||
items.extend(group_items.iter().map(|item| item as &dyn IsMenuItem<DynRuntime>));
|
||||
Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group.name),
|
||||
@@ -757,7 +758,7 @@ fn create_subcreate_proxy_menu_item(
|
||||
fn create_proxy_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
show_proxy_groups_inline: bool,
|
||||
proxy_submenus: Vec<Submenu<Wry>>,
|
||||
proxy_submenus: Vec<Submenu<DynRuntime>>,
|
||||
proxies_text: &str,
|
||||
) -> Result<ProxyMenuItem> {
|
||||
let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline {
|
||||
@@ -765,13 +766,13 @@ fn create_proxy_menu_item(
|
||||
None,
|
||||
proxy_submenus
|
||||
.into_iter()
|
||||
.map(|submenu| Box::new(submenu) as Box<dyn IsMenuItem<Wry>>)
|
||||
.map(|submenu| Box::new(submenu) as Box<dyn IsMenuItem<DynRuntime>>)
|
||||
.collect(),
|
||||
)
|
||||
} else if !proxy_submenus.is_empty() {
|
||||
let proxy_submenu_refs: Vec<&dyn IsMenuItem<Wry>> = proxy_submenus
|
||||
let proxy_submenu_refs: Vec<&dyn IsMenuItem<DynRuntime>> = proxy_submenus
|
||||
.iter()
|
||||
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
|
||||
.map(|submenu| submenu as &dyn IsMenuItem<DynRuntime>)
|
||||
.collect();
|
||||
|
||||
(
|
||||
@@ -798,7 +799,10 @@ async fn create_tray_menu(
|
||||
tun_mode_available: bool,
|
||||
profiles_preview: Vec<IProfilePreview<'_>>,
|
||||
options: TrayMenuOptions,
|
||||
) -> Result<(tauri::menu::Menu<Wry>, HashMap<std::string::String, LatencyMenuItem>)> {
|
||||
) -> Result<(
|
||||
tauri::menu::Menu<DynRuntime>,
|
||||
HashMap<std::string::String, LatencyMenuItem>,
|
||||
)> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
|
||||
let mut verge_settings = Config::verge().await.latest_arc();
|
||||
@@ -836,12 +840,12 @@ async fn create_tray_menu(
|
||||
|
||||
let hotkeys = create_hotkeys(&verge_settings.hotkeys);
|
||||
|
||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = create_profile_menu_item(app_handle, profiles_preview)?;
|
||||
let profile_menu_items: Vec<CheckMenuItem<DynRuntime>> = create_profile_menu_item(app_handle, profiles_preview)?;
|
||||
|
||||
let texts = MenuTexts::new();
|
||||
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
let profile_menu_items_refs: Vec<&dyn IsMenuItem<DynRuntime>> = profile_menu_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
.map(|item| item as &dyn IsMenuItem<DynRuntime>)
|
||||
.collect();
|
||||
|
||||
let open_window = &MenuItem::with_id(
|
||||
@@ -894,9 +898,9 @@ async fn create_tray_menu(
|
||||
outbound_modes_label.as_str(),
|
||||
true,
|
||||
&[
|
||||
rule_mode as &dyn IsMenuItem<Wry>,
|
||||
global_mode as &dyn IsMenuItem<Wry>,
|
||||
direct_mode as &dyn IsMenuItem<Wry>,
|
||||
rule_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
global_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
direct_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
],
|
||||
)?)
|
||||
};
|
||||
@@ -1009,7 +1013,7 @@ async fn create_tray_menu(
|
||||
&texts.more,
|
||||
true,
|
||||
&[
|
||||
copy_env as &dyn IsMenuItem<Wry>,
|
||||
copy_env as &dyn IsMenuItem<DynRuntime>,
|
||||
close_all_connections,
|
||||
restart_clash,
|
||||
restart_app,
|
||||
@@ -1026,13 +1030,13 @@ async fn create_tray_menu(
|
||||
|
||||
let separator = &PredefinedMenuItem::separator(app_handle)?;
|
||||
|
||||
let mut menu_items: Vec<&dyn IsMenuItem<Wry>> = vec![open_window, separator];
|
||||
let mut menu_items: Vec<&dyn IsMenuItem<DynRuntime>> = vec![open_window, separator];
|
||||
|
||||
if show_outbound_modes_inline {
|
||||
menu_items.extend_from_slice(&[
|
||||
rule_mode as &dyn IsMenuItem<Wry>,
|
||||
global_mode as &dyn IsMenuItem<Wry>,
|
||||
direct_mode as &dyn IsMenuItem<Wry>,
|
||||
rule_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
global_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
direct_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
]);
|
||||
} else if let Some(ref outbound_modes) = outbound_modes {
|
||||
menu_items.push(outbound_modes);
|
||||
@@ -1052,14 +1056,14 @@ async fn create_tray_menu(
|
||||
|
||||
menu_items.extend_from_slice(&[
|
||||
separator,
|
||||
system_proxy as &dyn IsMenuItem<Wry>,
|
||||
tun_mode as &dyn IsMenuItem<Wry>,
|
||||
system_proxy as &dyn IsMenuItem<DynRuntime>,
|
||||
tun_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
separator,
|
||||
lightweight_mode as &dyn IsMenuItem<Wry>,
|
||||
open_dir as &dyn IsMenuItem<Wry>,
|
||||
more as &dyn IsMenuItem<Wry>,
|
||||
lightweight_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
open_dir as &dyn IsMenuItem<DynRuntime>,
|
||||
more as &dyn IsMenuItem<DynRuntime>,
|
||||
separator,
|
||||
quit as &dyn IsMenuItem<Wry>,
|
||||
quit as &dyn IsMenuItem<DynRuntime>,
|
||||
]);
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
|
||||
|
||||
@@ -132,7 +132,7 @@ impl TraySpeedController {
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
if let Some(tray) = app_handle.tray_by_id(super::TRAY_ID) {
|
||||
let result = tray.with_inner_tray_icon(|inner| {
|
||||
let result = tray.with_inner_blocking(|inner| {
|
||||
if let Some(status_item) = inner.ns_status_item() {
|
||||
tray_speed::clear_speed_attributed_title(&status_item);
|
||||
}
|
||||
@@ -167,7 +167,7 @@ impl TraySpeedController {
|
||||
fn apply_tray_speed(up: u64, down: u64) {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
if let Some(tray) = app_handle.tray_by_id(super::TRAY_ID) {
|
||||
let result = tray.with_inner_tray_icon(move |inner| {
|
||||
let result = tray.with_inner_blocking(move |inner| {
|
||||
if let Some(status_item) = inner.ns_status_item() {
|
||||
tray_speed::set_speed_attributed_title(&status_item, up, down);
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub async fn open_or_close_dashboard() {
|
||||
logging!(info, Type::Window, "Window toggle result: {result:?}");
|
||||
}
|
||||
|
||||
pub async fn quit() {
|
||||
pub async fn quit() -> clash_verge_signal::ShutdownOutcome {
|
||||
logging!(debug, Type::System, "启动退出流程");
|
||||
// 设置退出标志
|
||||
handle::Handle::global().set_is_exiting();
|
||||
@@ -165,11 +165,13 @@ pub async fn quit() {
|
||||
NoticeStatus::AppQuitCoreStopFailed,
|
||||
cleanup_result.stop_error.unwrap_or_default(),
|
||||
);
|
||||
return clash_verge_signal::ShutdownOutcome::Canceled;
|
||||
}
|
||||
|
||||
utils::server::shutdown_embedded_server();
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
|
||||
clash_verge_signal::ShutdownOutcome::Committed
|
||||
}
|
||||
|
||||
pub async fn clean_async() -> CleanupResult {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::cmd;
|
||||
|
||||
pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
pub fn builder() -> tauri_specta::Builder<tauri::DynRuntime> {
|
||||
tauri_specta::Builder::new()
|
||||
.types(&platform_types())
|
||||
.commands(tauri_specta::collect_commands![
|
||||
tauri_plugin_clash_verge_sysinfo::commands::get_system_info,
|
||||
tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime,
|
||||
tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info::<tauri::Wry>,
|
||||
tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info::<tauri::DynRuntime>,
|
||||
cmd::probe_listener,
|
||||
cmd::save_proxy_ports,
|
||||
cmd::get_sys_proxy,
|
||||
|
||||
@@ -40,7 +40,7 @@ mod app_init {
|
||||
}
|
||||
|
||||
/// Setup plugins for the Tauri builder
|
||||
pub fn setup_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
pub fn setup_plugins(builder: tauri::Builder) -> tauri::Builder {
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = builder
|
||||
.plugin(tauri_plugin_clash_verge_sysinfo::init())
|
||||
@@ -124,7 +124,7 @@ mod app_init {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static {
|
||||
{
|
||||
let builder = crate::ipc::builder();
|
||||
move |invoke| builder.invoke_handler()(invoke)
|
||||
@@ -181,7 +181,7 @@ pub fn run() -> std::process::ExitCode {
|
||||
#[cfg(target_os = "linux")]
|
||||
utils::linux::workarounds::apply_wayland_webkit_fix();
|
||||
|
||||
let builder = app_init::setup_plugins(tauri::Builder::default())
|
||||
let builder = app_init::setup_plugins(tauri::Builder::default().runtime(tauri_runtime_wry::Wry::default()))
|
||||
.setup(|app| {
|
||||
// Logger may not be ready yet, so mirror setup panics to stderr.
|
||||
fn log_setup_panic(stage: &str, panic: Box<dyn std::any::Any + Send>) {
|
||||
@@ -230,7 +230,6 @@ pub fn run() -> std::process::ExitCode {
|
||||
if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
resolve::resolve_setup_async();
|
||||
resolve::resolve_setup_sync();
|
||||
#[cfg(target_os = "linux")]
|
||||
resolve::init_signal();
|
||||
})) {
|
||||
log_setup_panic("window-core", panic);
|
||||
|
||||
+1
-12
@@ -1,22 +1,11 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::process::ExitCode;
|
||||
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[cfg(all(feature = "perf-harness", target_os = "macos"))]
|
||||
#[path = "../../scripts/perf/app.rs"]
|
||||
mod perf;
|
||||
|
||||
#[cfg(all(feature = "perf-harness", target_os = "macos"))]
|
||||
fn main() -> ExitCode {
|
||||
perf::run()
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
|
||||
fn main() -> ExitCode {
|
||||
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
let worker_limit = std::cmp::min(default_parallelism, 4);
|
||||
let worker_limit = std::cmp::min(default_parallelism, 8);
|
||||
let blocking_limit = 2 * worker_limit;
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::{
|
||||
utils::{init, server, window_manager::WindowManager},
|
||||
};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use clash_verge_signal;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod dns;
|
||||
@@ -188,8 +189,6 @@ async fn init_silent_updater() {
|
||||
logging!(info, Type::Setup, "Silent updater initialized");
|
||||
}
|
||||
|
||||
// Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn init_signal() {
|
||||
logging!(debug, Type::Setup, "Initializing signal handlers...");
|
||||
clash_verge_signal::register(feat::quit);
|
||||
|
||||
@@ -132,7 +132,10 @@ pub fn take_webview_needs_reload() -> bool {
|
||||
/// reload visible webviews immediately, and defer a hidden main-window reload until activation.
|
||||
/// Registering this callback replaces Tauri's default automatic reload.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn on_web_content_process_terminated(webview: &tauri::Webview) {
|
||||
pub fn on_web_content_process_terminated(
|
||||
webview: &tauri::Webview,
|
||||
_termination: &tauri::webview::WebContentProcessTermination,
|
||||
) {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ static INSTANCE_LOCK: OnceLock<std::fs::File> = OnceLock::new();
|
||||
const PAC_INITIAL_AVAILABLE: bool = false;
|
||||
static PAC_AVAILABLE: AtomicBool = AtomicBool::new(PAC_INITIAL_AVAILABLE);
|
||||
static COMMANDS_READY: AtomicBool = AtomicBool::new(false);
|
||||
#[cfg(feature = "verge-dev")]
|
||||
static DEV_QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SingletonDisposition {
|
||||
@@ -162,12 +164,55 @@ fn instance_auth(token: String) -> impl warp::Filter<Extract = (), Error = warp:
|
||||
.untuple_one()
|
||||
}
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
fn dev_quit_route<F>(
|
||||
token: String,
|
||||
commands_ready: &'static AtomicBool,
|
||||
quit_requested: &'static AtomicBool,
|
||||
trigger: F,
|
||||
) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone
|
||||
where
|
||||
F: Fn() + Clone + Send + Sync + 'static,
|
||||
{
|
||||
instance_auth(token)
|
||||
.and(warp::path!("commands" / "dev" / "quit"))
|
||||
.and(warp::post())
|
||||
.and_then(move || {
|
||||
let trigger = trigger.clone();
|
||||
async move {
|
||||
if !commands_ready.load(Ordering::Acquire) {
|
||||
return Ok::<_, warp::Rejection>(warp::reply::with_status(
|
||||
String::from("starting"),
|
||||
warp::http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
));
|
||||
}
|
||||
if quit_requested
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok()
|
||||
{
|
||||
trigger();
|
||||
}
|
||||
Ok(warp::reply::with_status(
|
||||
String::from("accepted"),
|
||||
warp::http::StatusCode::ACCEPTED,
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
fn release_dev_quit_latch(quit_requested: &AtomicBool, outcome: clash_verge_signal::ShutdownOutcome) {
|
||||
if matches!(outcome, clash_verge_signal::ShutdownOutcome::Canceled) {
|
||||
quit_requested.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let _ = SHUTDOWN_SENDER.set(Mutex::new(Some(shutdown_tx)));
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let auth = instance_auth(token);
|
||||
let auth = instance_auth(token.clone());
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
let auth = instance_auth(token);
|
||||
|
||||
@@ -233,6 +278,12 @@ fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
|
||||
});
|
||||
|
||||
let commands = auth.clone().and(visible).or(auth.and(scheme)).or(pac);
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let commands = commands.or(dev_quit_route(token, &COMMANDS_READY, &DEV_QUIT_REQUESTED, || {
|
||||
AsyncHandler::spawn(|| async {
|
||||
release_dev_quit_latch(&DEV_QUIT_REQUESTED, crate::feat::quit().await);
|
||||
});
|
||||
}));
|
||||
AsyncHandler::spawn(move || async move {
|
||||
warp::serve(commands)
|
||||
.incoming(listener)
|
||||
|
||||
@@ -27,12 +27,8 @@ const TRAY_STATUS_ITEM_EXTRA_WIDTH: f64 = 30.0;
|
||||
const TRAY_STATUS_ITEM_MIN_LENGTH: f64 = 58.0;
|
||||
const NS_VARIABLE_STATUS_ITEM_LENGTH: f64 = -1.0;
|
||||
|
||||
/// (button_height, mono_advance, attributes).
|
||||
type CachedAttrs = (f64, f64, Retained<NSDictionary<NSString, AnyObject>>);
|
||||
|
||||
thread_local! {
|
||||
static LAST_DISPLAY_STR: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
static CACHED_ATTRS: RefCell<Option<CachedAttrs>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
fn format_tray_speed(up: u64, down: u64) -> String {
|
||||
@@ -78,26 +74,8 @@ fn create_attributed_string(
|
||||
}
|
||||
}
|
||||
|
||||
/// Attributes and per-glyph advance, rebuilt only when the button height changes.
|
||||
fn cached_attributes(button_height: f64) -> (f64, Retained<NSDictionary<NSString, AnyObject>>) {
|
||||
CACHED_ATTRS.with(|cache| {
|
||||
if let Some((height, advance, attrs)) = cache.borrow().as_ref()
|
||||
&& *height == button_height
|
||||
{
|
||||
return (*advance, attrs.clone());
|
||||
}
|
||||
let attrs = build_attributes(button_height);
|
||||
let probe = NSString::from_str("0");
|
||||
let advance = create_attributed_string(&probe, Some(&attrs)).size().width;
|
||||
*cache.borrow_mut() = Some((button_height, advance, attrs.clone()));
|
||||
(advance, attrs)
|
||||
})
|
||||
}
|
||||
|
||||
/// Longest line times the monospaced advance; matches `size().width` for the ASCII-only speed text.
|
||||
fn arithmetic_width(text: &str, advance: f64) -> f64 {
|
||||
let glyphs = text.split('\n').map(str::len).max().unwrap_or(0) as f64;
|
||||
glyphs * advance
|
||||
fn status_item_length_for_speed(attr_str: &NSAttributedString) -> f64 {
|
||||
(attr_str.size().width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH)
|
||||
}
|
||||
|
||||
fn sync_click_target_frame(button: &NSStatusBarButton) {
|
||||
@@ -111,7 +89,7 @@ fn sync_click_target_frame(button: &NSStatusBarButton) {
|
||||
}
|
||||
|
||||
/// `with_inner_tray_icon` should run this on the main thread; fail safely if it does not.
|
||||
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, show_speed: bool) {
|
||||
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSString, show_speed: bool) {
|
||||
let Some(mtm) = MainThreadMarker::new() else {
|
||||
logging!(warn, Type::Tray, "托盘速率富文本设置跳过:非主线程调用");
|
||||
return;
|
||||
@@ -119,19 +97,17 @@ fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, sh
|
||||
let Some(button) = status_item.button(mtm) else {
|
||||
return;
|
||||
};
|
||||
let ns_string = NSString::from_str(text);
|
||||
let attr_str = if show_speed {
|
||||
let (advance, attrs) = cached_attributes(button.bounds().size.height);
|
||||
let attrs = build_attributes(button.bounds().size.height);
|
||||
let attrs: &NSDictionary<NSString, AnyObject> = &attrs;
|
||||
let attr_str = create_attributed_string(&ns_string, Some(attrs));
|
||||
let width = arithmetic_width(text, advance);
|
||||
let length = (width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH);
|
||||
let attr_str = create_attributed_string(text, Some(attrs));
|
||||
let length = status_item_length_for_speed(&attr_str);
|
||||
if status_item.length() != length {
|
||||
status_item.setLength(length);
|
||||
}
|
||||
attr_str
|
||||
} else {
|
||||
create_attributed_string(&ns_string, None)
|
||||
create_attributed_string(text, None)
|
||||
};
|
||||
button.setAttributedTitle(&attr_str);
|
||||
sync_click_target_frame(&button);
|
||||
@@ -145,7 +121,8 @@ pub fn set_speed_attributed_title(status_item: &NSStatusItem, up: u64, down: u64
|
||||
return;
|
||||
}
|
||||
*last = speed_text;
|
||||
apply_status_item_attributed_title(status_item, last.as_str(), true);
|
||||
let ns_string = NSString::from_str(last.as_str());
|
||||
apply_status_item_attributed_title(status_item, &ns_string, true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,6 +130,7 @@ pub fn clear_speed_attributed_title(status_item: &NSStatusItem) {
|
||||
LAST_DISPLAY_STR.with(|last| {
|
||||
last.borrow_mut().clear();
|
||||
});
|
||||
let empty = NSString::from_str("");
|
||||
status_item.setLength(NS_VARIABLE_STATUS_ITEM_LENGTH);
|
||||
apply_status_item_attributed_title(status_item, "", false);
|
||||
apply_status_item_attributed_title(status_item, &empty, false);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use clash_verge_logging::{Type, logging};
|
||||
use std::pin::Pin;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
use tauri::{Manager as _, WebviewWindow, Wry};
|
||||
use tauri::{Manager as _, WebviewWindow};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum WindowOperationResult {
|
||||
@@ -50,7 +50,7 @@ impl WindowManager {
|
||||
handle::Handle::global().set_activation_policy_regular();
|
||||
}
|
||||
|
||||
fn get_main_window_with_state() -> (Option<WebviewWindow<Wry>>, WindowState) {
|
||||
fn get_main_window_with_state() -> (Option<WebviewWindow>, WindowState) {
|
||||
let Some(window) = Self::get_main_window() else {
|
||||
return (None, WindowState::NotExist);
|
||||
};
|
||||
@@ -76,7 +76,7 @@ impl WindowManager {
|
||||
Self::get_main_window_with_state().1
|
||||
}
|
||||
|
||||
pub fn get_main_window() -> Option<WebviewWindow<Wry>> {
|
||||
pub fn get_main_window() -> Option<WebviewWindow> {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle.get_webview_window("main")
|
||||
}
|
||||
@@ -147,7 +147,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_main_window(window: Option<&WebviewWindow<Wry>>) -> WindowOperationResult {
|
||||
fn hide_main_window(window: Option<&WebviewWindow>) -> WindowOperationResult {
|
||||
logging!(debug, Type::Window, "窗口可见,将隐藏窗口");
|
||||
if let Some(window) = window {
|
||||
match window.close() {
|
||||
@@ -166,7 +166,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_existing_main_window(window: Option<&WebviewWindow<Wry>>) -> WindowOperationResult {
|
||||
fn activate_existing_main_window(window: Option<&WebviewWindow>) -> WindowOperationResult {
|
||||
logging!(debug, Type::Window, "窗口存在但被隐藏或最小化,将激活窗口");
|
||||
if let Some(window) = window {
|
||||
Self::activate_window(window)
|
||||
@@ -176,7 +176,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult {
|
||||
fn activate_window(window: &WebviewWindow) -> WindowOperationResult {
|
||||
logging!(debug, Type::Window, "开始激活窗口");
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::set_macos_activation_policy_regular();
|
||||
@@ -233,15 +233,15 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_main_window_visible(window: Option<&WebviewWindow<Wry>>) -> bool {
|
||||
pub fn is_main_window_visible(window: Option<&WebviewWindow>) -> bool {
|
||||
window.map(|w| w.is_visible().unwrap_or(false)).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_main_window_focused(window: Option<&WebviewWindow<Wry>>) -> bool {
|
||||
pub fn is_main_window_focused(window: Option<&WebviewWindow>) -> bool {
|
||||
window.map(|w| w.is_focused().unwrap_or(false)).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_main_window_minimized(window: Option<&WebviewWindow<Wry>>) -> bool {
|
||||
fn is_main_window_minimized(window: Option<&WebviewWindow>) -> bool {
|
||||
window.map(|w| w.is_minimized().unwrap_or(false)).unwrap_or(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export const SystemInfoCard = () => {
|
||||
try {
|
||||
const result = await triggerCheckUpdate()
|
||||
const info = result.data
|
||||
if (!info?.available) {
|
||||
if (!info) {
|
||||
showNotice.success(
|
||||
'settings.components.verge.advanced.notifications.latestVersion',
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ export const UpdateButton = (props: Props) => {
|
||||
|
||||
const { updateInfo } = useUpdate()
|
||||
|
||||
if (!updateInfo?.available) return null
|
||||
if (!updateInfo) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -75,7 +75,6 @@ interface ProxyChainProps {
|
||||
onMarkUnsavedChanges?: () => void
|
||||
mode?: string
|
||||
selectedGroup?: string | null
|
||||
onExitChainMode: () => void
|
||||
}
|
||||
|
||||
interface ProxyChainItemProps {
|
||||
@@ -332,7 +331,6 @@ export const ProxyChain = ({
|
||||
onMarkUnsavedChanges,
|
||||
mode,
|
||||
selectedGroup,
|
||||
onExitChainMode,
|
||||
}: ProxyChainProps) => {
|
||||
const theme = useTheme()
|
||||
const { t } = useTranslation()
|
||||
@@ -698,18 +696,13 @@ export const ProxyChain = ({
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
height: '100%',
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<Typography>{t('proxies.page.chain.empty')}</Typography>
|
||||
<Button size="small" variant="outlined" onClick={onExitChainMode}>
|
||||
{t('shared.actions.close')}
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<DragDropProvider
|
||||
|
||||
@@ -77,7 +77,6 @@ interface GroupSelectMenuProps {
|
||||
interface ProxyGroupsChainProps {
|
||||
mode: string
|
||||
chainConfigData?: string | null
|
||||
onExitChainMode: () => void
|
||||
availableGroups: any[]
|
||||
activeSelectedGroup: string | null
|
||||
showScrollTop: boolean
|
||||
@@ -306,7 +305,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
const {
|
||||
mode,
|
||||
chainConfigData,
|
||||
onExitChainMode,
|
||||
availableGroups,
|
||||
activeSelectedGroup,
|
||||
showScrollTop,
|
||||
@@ -523,7 +521,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
chainConfigData={chainConfigData}
|
||||
mode={mode}
|
||||
selectedGroup={activeSelectedGroup}
|
||||
onExitChainMode={onExitChainMode}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
ProxyGroupNavigator,
|
||||
} from './proxy-group-navigator'
|
||||
import { ProxyRender } from './proxy-render'
|
||||
import type { HeadState } from './use-head-state'
|
||||
import {
|
||||
PROXY_GROUP_HEADER_SENSORS,
|
||||
useProxyGroupHeaderLayout,
|
||||
@@ -71,7 +70,6 @@ interface Props {
|
||||
mode: string
|
||||
isChainMode?: boolean
|
||||
chainConfigData?: string | null
|
||||
onExitChainMode: () => void
|
||||
}
|
||||
|
||||
function useEmptyRenderList() {
|
||||
@@ -92,11 +90,11 @@ function useProxyRenderState(
|
||||
) {
|
||||
const { verge } = useVerge()
|
||||
const { proxyView } = useProxiesData()
|
||||
const {
|
||||
renderList,
|
||||
onProxies,
|
||||
onHeadState: setListHeadState,
|
||||
} = useRenderList(mode, isChainMode, activeSelectedGroup)
|
||||
const { renderList, onProxies, onHeadState } = useRenderList(
|
||||
mode,
|
||||
isChainMode,
|
||||
activeSelectedGroup,
|
||||
)
|
||||
const scrollPositionKey = useMemo(
|
||||
() =>
|
||||
isChainMode
|
||||
@@ -105,14 +103,6 @@ function useProxyRenderState(
|
||||
[activeSelectedGroup, isChainMode, mode],
|
||||
)
|
||||
|
||||
const onHeadState = useCallback(
|
||||
(groupName: string, patch: Partial<HeadState>) => {
|
||||
if (patch.open) onProxies()
|
||||
setListHeadState(groupName, patch)
|
||||
},
|
||||
[onProxies, setListHeadState],
|
||||
)
|
||||
|
||||
const timeout = verge?.default_latency_timeout || 10000
|
||||
|
||||
const handleCheckAll = useStableCallback(
|
||||
@@ -200,9 +190,8 @@ function useProxyRenderState(
|
||||
function ChainProxyGroups(props: {
|
||||
mode: string
|
||||
chainConfigData?: string | null
|
||||
onExitChainMode: () => void
|
||||
}) {
|
||||
const { mode, chainConfigData, onExitChainMode } = props
|
||||
const { mode, chainConfigData } = props
|
||||
const { proxyView } = useProxiesData()
|
||||
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
|
||||
|
||||
@@ -357,7 +346,6 @@ function ChainProxyGroups(props: {
|
||||
<ProxyGroupsChain
|
||||
mode={mode}
|
||||
chainConfigData={chainConfigData}
|
||||
onExitChainMode={onExitChainMode}
|
||||
availableGroups={availableGroups}
|
||||
activeSelectedGroup={activeSelectedGroup}
|
||||
showScrollTop={showScrollTop}
|
||||
@@ -629,7 +617,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
}
|
||||
|
||||
export const ProxyGroups = (props: Props) => {
|
||||
const { mode, isChainMode = false, chainConfigData, onExitChainMode } = props
|
||||
const { mode, isChainMode = false, chainConfigData } = props
|
||||
const { profiles, isLoading: isProfilesLoading } = useProfiles()
|
||||
const { isProxyViewPending } = useProxiesData()
|
||||
const { isRunningModePending } = useSystemData()
|
||||
@@ -651,11 +639,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
return <ProxyEmptyState reason={listState.reason} />
|
||||
case 'render':
|
||||
return isChainMode ? (
|
||||
<ChainProxyGroups
|
||||
mode={mode}
|
||||
chainConfigData={chainConfigData}
|
||||
onExitChainMode={onExitChainMode}
|
||||
/>
|
||||
<ChainProxyGroups mode={mode} chainConfigData={chainConfigData} />
|
||||
) : (
|
||||
<NormalProxyGroups mode={mode} />
|
||||
)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { Button, Grid, Stack, TextField } from '@mui/material'
|
||||
import Visibility from '@mui/icons-material/Visibility'
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
Grid,
|
||||
Stack,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { useState, useRef, memo, useEffect } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { createWebdavBackup, saveWebdavConfig } from '@/services/cmds'
|
||||
import { saveWebdavConfig, createWebdavBackup } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { errorDetail, showNotice } from '@/services/notice-service'
|
||||
import {
|
||||
@@ -34,6 +43,7 @@ export const BackupConfigViewer = memo(
|
||||
const { t } = useTranslation()
|
||||
const { verge, mutateVerge } = useVerge()
|
||||
const { webdav_url, webdav_username, webdav_password } = verge || {}
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const usernameRef = useRef<HTMLInputElement>(null)
|
||||
const passwordRef = useRef<HTMLInputElement>(null)
|
||||
const urlRef = useRef<HTMLInputElement>(null)
|
||||
@@ -58,6 +68,10 @@ export const BackupConfigViewer = memo(
|
||||
const webdavStatus = getWebdavStatus(webdavSignature)
|
||||
const shouldAutoInit = webdavStatus !== 'failed'
|
||||
|
||||
const handleClickShowPassword = () => {
|
||||
setShowPassword((prev) => !prev)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!webdav_url || !webdav_username || !webdav_password) {
|
||||
return
|
||||
@@ -201,7 +215,7 @@ export const BackupConfigViewer = memo(
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t('shared.labels.password')}
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
autoCorrect="off"
|
||||
@@ -209,6 +223,20 @@ export const BackupConfigViewer = memo(
|
||||
spellCheck="false"
|
||||
inputRef={passwordRef}
|
||||
{...register('password')}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleClickShowPassword}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -48,7 +48,7 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
|
||||
try {
|
||||
const info = await checkUpdate()
|
||||
updateLastCheckTime()
|
||||
if (!info?.available) {
|
||||
if (!info) {
|
||||
showNotice.success(
|
||||
'settings.components.verge.advanced.notifications.latestVersion',
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LanguageRounded } from '@mui/icons-material'
|
||||
import { alpha, Box, Divider, Menu, MenuItem, styled } from '@mui/material'
|
||||
import { Box, Divider, MenuItem, Menu, styled, alpha } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -29,14 +29,8 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
|
||||
|
||||
const onDelay = useCallback(async () => {
|
||||
setDelay(-2)
|
||||
try {
|
||||
const result = await cmdTestDelay(url!)
|
||||
setDelay(result)
|
||||
} catch (err) {
|
||||
// A failed test must not leave the item stuck in the loading state.
|
||||
setDelay(-1)
|
||||
showNotice.error(err)
|
||||
}
|
||||
const result = await cmdTestDelay(url!)
|
||||
setDelay(result)
|
||||
}, [url])
|
||||
|
||||
const onEditTest = () => {
|
||||
|
||||
@@ -7,8 +7,10 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog } from '@/components/base'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import type { IVergeTestItem_Serialize } from '@/services/bindings'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
|
||||
interface Props {
|
||||
onChange: (uid: string, patch?: Partial<IVergeTestItem>) => void
|
||||
}
|
||||
@@ -77,7 +79,12 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
|
||||
|
||||
if (openType === 'new') {
|
||||
const uid = nanoid()
|
||||
const item = { ...form, uid }
|
||||
const item: IVergeTestItem_Serialize = {
|
||||
uid,
|
||||
name: form.name ?? null,
|
||||
icon: form.icon ?? null,
|
||||
url: form.url ?? null,
|
||||
}
|
||||
const newList = [...testList, item]
|
||||
await patchVerge({ test_list: newList })
|
||||
onChange(uid)
|
||||
|
||||
@@ -51,7 +51,6 @@ export const useProfiles = () => {
|
||||
try {
|
||||
const result = await mutate(() => patchProfilesConfig(value), {
|
||||
id: 'patch-profiles-config',
|
||||
errorNotice: false,
|
||||
onFulfilled: (outcome) => {
|
||||
if (outcome.status === 'valid') {
|
||||
void setCacheData<IProfilesConfig>(profilesQueryKey, (current) =>
|
||||
@@ -61,8 +60,12 @@ export const useProfiles = () => {
|
||||
},
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
// Backend Busy keeps local state untouched, as before.
|
||||
return result.value
|
||||
}
|
||||
const outcome = result.value
|
||||
if (outcome.status !== 'valid') {
|
||||
if (outcome.status !== 'valid' && outcome.status !== 'busy') {
|
||||
await mutateProfiles()
|
||||
}
|
||||
return outcome
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Proxy-Kette konfigurieren",
|
||||
"warning": "Eine Proxy-Kette verringert die Netzwerkgeschwindigkeit deutlich. Wenn Sie keinen klaren Bedarf haben oder diese Konfiguration nicht verstehen, lassen Sie sie deaktiviert.",
|
||||
"confirmEnable": "Verstanden, aktivieren",
|
||||
"keepDisabled": "Deaktiviert lassen",
|
||||
"empty": "Eine Proxy-Kette verringert die Netzwerkgeschwindigkeit deutlich. Wenn Sie keinen klaren Bedarf haben oder diese Konfiguration nicht verstehen, lassen Sie sie deaktiviert.",
|
||||
"instruction": "Klicken Sie die Knoten nacheinander an, um sie zur Proxy-Kette hinzuzufügen",
|
||||
"minimumNodes": "Eine Proxy-Kette braucht mindestens 2 Knoten",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "체인 프록시 구성",
|
||||
"warning": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.",
|
||||
"confirmEnable": "이해했습니다, 활성화",
|
||||
"keepDisabled": "비활성화 유지",
|
||||
"empty": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.",
|
||||
"instruction": "프록시 체인에 추가하려면 순서대로 노드를 클릭하세요",
|
||||
"minimumNodes": "체인 프록시는 최소 2개의 노드가 필요합니다",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Configuração do proxy em cadeia",
|
||||
"warning": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.",
|
||||
"confirmEnable": "Entendi, ativar",
|
||||
"keepDisabled": "Manter desativado",
|
||||
"empty": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.",
|
||||
"instruction": "Clique nos nós em ordem para adicioná-los à cadeia de proxy",
|
||||
"minimumNodes": "O proxy em cadeia requer pelo menos 2 nós",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Цепочка прокси",
|
||||
"warning": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.",
|
||||
"confirmEnable": "Понятно, включить",
|
||||
"keepDisabled": "Оставить выключенным",
|
||||
"empty": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.",
|
||||
"instruction": "Нажимайте на узлы по порядку, чтобы добавить их в цепочку",
|
||||
"minimumNodes": "Для цепочки требуется минимум 2 узла",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Zincir Vekil Yapılandırması",
|
||||
"warning": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.",
|
||||
"confirmEnable": "Anladım, etkinleştir",
|
||||
"keepDisabled": "Devre dışı kalsın",
|
||||
"empty": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.",
|
||||
"instruction": "Vekil zincirine eklemek için düğümlere sırayla tıklayın",
|
||||
"minimumNodes": "Zincir vekil en az 2 düğüm gerektirir",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "代理链配置",
|
||||
"warning": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭",
|
||||
"confirmEnable": "我明白,开启",
|
||||
"keepDisabled": "保持关闭",
|
||||
"empty": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭",
|
||||
"instruction": "顺序点击节点添加到代理链中",
|
||||
"minimumNodes": "链式代理至少需要 2 个节点",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"chain": {
|
||||
"header": "鏈式代理設定",
|
||||
"warning": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉",
|
||||
"confirmEnable": "我明白,開啟",
|
||||
"keepDisabled": "保持關閉",
|
||||
"empty": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉",
|
||||
"instruction": "依序點擊節點新增到鏈式代理中",
|
||||
"minimumNodes": "鏈式代理至少需要 2 個節點",
|
||||
|
||||
+24
-43
@@ -1,10 +1,10 @@
|
||||
import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material'
|
||||
import { Box, Button, ButtonGroup, Typography } from '@mui/material'
|
||||
import { Box, Button, ButtonGroup } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, BasePage, TooltipIcon } from '@/components/base'
|
||||
import { BasePage, TooltipIcon } from '@/components/base'
|
||||
import { ProviderButton } from '@/components/proxy/provider-button'
|
||||
import { ProxyGroups } from '@/components/proxy/proxy-groups'
|
||||
import {
|
||||
@@ -38,7 +38,6 @@ const ProxyPage = () => {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const [chainConfirmOpen, setChainConfirmOpen] = useState(false)
|
||||
|
||||
const [chainConfigData, dispatchChainConfigData] = useReducer(
|
||||
(_: string | null, action: string | null) => action,
|
||||
@@ -69,34 +68,32 @@ const ProxyPage = () => {
|
||||
}
|
||||
})
|
||||
|
||||
const applyChainMode = (enabled: boolean) => {
|
||||
setIsChainMode(enabled)
|
||||
localStorage.setItem('proxy-chain-mode-enabled', enabled.toString())
|
||||
}
|
||||
const onToggleChainMode = useLockFn(async () => {
|
||||
const newChainMode = !isChainMode
|
||||
|
||||
const onExitChainMode = useLockFn(async () => {
|
||||
// 退出链式代理模式时,清除链式代理配置
|
||||
try {
|
||||
debugLog('Exiting chain mode, clearing chain configuration')
|
||||
const result = await mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
if (!result.ok) return
|
||||
debugLog('Chain configuration cleared successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear chain configuration:', error)
|
||||
showNotice.error(error)
|
||||
return
|
||||
if (!newChainMode) {
|
||||
// 退出链式代理模式时,清除链式代理配置
|
||||
try {
|
||||
debugLog('Exiting chain mode, clearing chain configuration')
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(null),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
debugLog('Chain configuration cleared successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear chain configuration:', error)
|
||||
showNotice.error(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
applyChainMode(false)
|
||||
setIsChainMode(newChainMode)
|
||||
localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString())
|
||||
})
|
||||
|
||||
const onToggleChainMode = () => {
|
||||
if (isChainMode) onExitChainMode().catch(() => {})
|
||||
else setChainConfirmOpen(true)
|
||||
}
|
||||
|
||||
// 当开启链式代理模式时,获取配置数据
|
||||
useEffect(() => {
|
||||
if (!isChainMode) {
|
||||
@@ -205,23 +202,7 @@ const ProxyPage = () => {
|
||||
mode={curMode ?? 'rule'}
|
||||
isChainMode={isChainMode}
|
||||
chainConfigData={chainConfigData}
|
||||
onExitChainMode={onExitChainMode}
|
||||
/>
|
||||
<BaseDialog
|
||||
open={chainConfirmOpen}
|
||||
title={t('proxies.page.actions.toggleChain')}
|
||||
okBtn={t('proxies.page.chain.confirmEnable')}
|
||||
cancelBtn={t('proxies.page.chain.keepDisabled')}
|
||||
contentSx={{ width: { xs: 320, sm: 420 } }}
|
||||
onCancel={() => setChainConfirmOpen(false)}
|
||||
onClose={() => setChainConfirmOpen(false)}
|
||||
onOk={() => {
|
||||
setChainConfirmOpen(false)
|
||||
applyChainMode(true)
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">{chainWarning}</Typography>
|
||||
</BaseDialog>
|
||||
</BasePage>
|
||||
)
|
||||
}
|
||||
|
||||
+106
-117
@@ -53,16 +53,11 @@ export const commands = {
|
||||
/** Reads the saved mode without depending on strict mihomo `/configs` deserialization. */
|
||||
getClashMode: () => __TAURI_INVOKE<string | null>('get_clash_mode'),
|
||||
changeClashCore: (clashCore: string) =>
|
||||
__TAURI_INVOKE<{
|
||||
/** Stable frontend identifier, when classified. */
|
||||
code?: string | null
|
||||
/** Human-readable diagnostic detail. */
|
||||
detail: string
|
||||
/** Requested operation when known by the command. */
|
||||
operation?: FailedOperation | null
|
||||
} | null>('change_clash_core', { clashCore }),
|
||||
__TAURI_INVOKE<CommandFailure_Serialize | null>('change_clash_core', {
|
||||
clashCore,
|
||||
}),
|
||||
getRuntimeConfig: () =>
|
||||
__TAURI_INVOKE<{ [key in string]: JsonValue } | null>('get_runtime_config'),
|
||||
__TAURI_INVOKE<YamlMapping | null>('get_runtime_config'),
|
||||
getProxyView: () => __TAURI_INVOKE<ProxyViewV1_Serialize>('get_proxy_view'),
|
||||
getRuntimeYaml: () => __TAURI_INVOKE<string>('get_runtime_yaml'),
|
||||
getRuntimeLogs: () => __TAURI_INVOKE<RuntimeLogs>('get_runtime_logs'),
|
||||
@@ -70,7 +65,7 @@ export const commands = {
|
||||
__TAURI_INVOKE<string>('get_runtime_proxy_chain_config', {
|
||||
proxyChainExitNode,
|
||||
}),
|
||||
updateProxyChainConfigInRuntime: (proxyChainConfig: JsonValue | null) =>
|
||||
updateProxyChainConfigInRuntime: (proxyChainConfig: YamlValue | null) =>
|
||||
__TAURI_INVOKE<ValidationOutcome>('update_proxy_chain_config_in_runtime', {
|
||||
proxyChainConfig,
|
||||
}),
|
||||
@@ -100,18 +95,11 @@ export const commands = {
|
||||
takeDnsOverrideNotice: () =>
|
||||
__TAURI_INVOKE<boolean>('take_dns_override_notice'),
|
||||
takeServiceFallbackNotice: () =>
|
||||
__TAURI_INVOKE<
|
||||
| { kind: 'unavailable' }
|
||||
| { kind: 'coreRejected'; reason: string }
|
||||
| { kind: 'notAutoStarted' }
|
||||
| null
|
||||
>('take_service_fallback_notice'),
|
||||
__TAURI_INVOKE<ServiceFallbackNotice | null>(
|
||||
'take_service_fallback_notice',
|
||||
),
|
||||
getCoreStartupError: () =>
|
||||
__TAURI_INVOKE<
|
||||
| { kind: 'startFailed'; detail: string }
|
||||
| { kind: 'serviceCoreStopped'; detail: string }
|
||||
| null
|
||||
>('get_core_startup_error'),
|
||||
__TAURI_INVOKE<CoreFailure | null>('get_core_startup_error'),
|
||||
takeServiceRepairNotice: () =>
|
||||
__TAURI_INVOKE<boolean>('take_service_repair_notice'),
|
||||
takeServiceOwnerNotice: () =>
|
||||
@@ -140,7 +128,7 @@ export const commands = {
|
||||
__TAURI_INVOKE<NetworkInterfaceView[]>('get_network_interfaces_info'),
|
||||
getProfiles: () => __TAURI_INVOKE<IProfiles_Serialize>('get_profiles'),
|
||||
enhanceProfiles: () => __TAURI_INVOKE<ValidationOutcome>('enhance_profiles'),
|
||||
patchProfilesConfig: (profiles: ProfileSelection) =>
|
||||
patchProfilesConfig: (profiles: ProfileSelection_Deserialize) =>
|
||||
__TAURI_INVOKE<ValidationOutcome>('patch_profiles_config', { profiles }),
|
||||
viewProfile: (index: string) =>
|
||||
__TAURI_INVOKE<null>('view_profile', { index }),
|
||||
@@ -148,52 +136,12 @@ export const commands = {
|
||||
__TAURI_INVOKE<null>('patch_profile', { index, profile }),
|
||||
createProfile: (item: ProfileCreate_Deserialize, fileData: string | null) =>
|
||||
__TAURI_INVOKE<null>('create_profile', { item, fileData }),
|
||||
importProfile: (
|
||||
url: string,
|
||||
option: {
|
||||
user_agent?: string | null
|
||||
with_proxy?: boolean | null
|
||||
self_proxy?: boolean | null
|
||||
update_interval?: number | null
|
||||
/**
|
||||
* HTTP request timeout in seconds
|
||||
* default is 60 seconds
|
||||
*/
|
||||
timeout_seconds?: number | null
|
||||
/** default is `false` */
|
||||
danger_accept_invalid_certs?: boolean | null
|
||||
allow_auto_update?: boolean | null
|
||||
merge?: string | null
|
||||
script?: string | null
|
||||
rules?: string | null
|
||||
proxies?: string | null
|
||||
groups?: string | null
|
||||
} | null,
|
||||
) => __TAURI_INVOKE<null>('import_profile', { url, option }),
|
||||
importProfile: (url: string, option: PrfOption_Deserialize | null) =>
|
||||
__TAURI_INVOKE<null>('import_profile', { url, option }),
|
||||
reorderProfile: (activeId: string, overId: string) =>
|
||||
__TAURI_INVOKE<null>('reorder_profile', { activeId, overId }),
|
||||
updateProfile: (
|
||||
index: string,
|
||||
option: {
|
||||
user_agent?: string | null
|
||||
with_proxy?: boolean | null
|
||||
self_proxy?: boolean | null
|
||||
update_interval?: number | null
|
||||
/**
|
||||
* HTTP request timeout in seconds
|
||||
* default is 60 seconds
|
||||
*/
|
||||
timeout_seconds?: number | null
|
||||
/** default is `false` */
|
||||
danger_accept_invalid_certs?: boolean | null
|
||||
allow_auto_update?: boolean | null
|
||||
merge?: string | null
|
||||
script?: string | null
|
||||
rules?: string | null
|
||||
proxies?: string | null
|
||||
groups?: string | null
|
||||
} | null,
|
||||
) => __TAURI_INVOKE<null>('update_profile', { index, option }),
|
||||
updateProfile: (index: string, option: PrfOption_Deserialize | null) =>
|
||||
__TAURI_INVOKE<null>('update_profile', { index, option }),
|
||||
deleteProfile: (index: string) =>
|
||||
__TAURI_INVOKE<null>('delete_profile', { index }),
|
||||
readProfileFile: (index: string) =>
|
||||
@@ -263,11 +211,11 @@ export type CommandFailure_Deserialize = {
|
||||
/** Structured command failure returned to the frontend. */
|
||||
export type CommandFailure_Serialize = {
|
||||
/** Stable frontend identifier, when classified. */
|
||||
code?: string | null
|
||||
code?: string
|
||||
/** Human-readable diagnostic detail. */
|
||||
detail: string
|
||||
/** Requested operation when known by the command. */
|
||||
operation?: FailedOperation | null
|
||||
operation?: FailedOperation
|
||||
}
|
||||
|
||||
/** Why the Core is not running, kept until a Core starts again. */
|
||||
@@ -330,14 +278,27 @@ export type IProfiles_Serialize = {
|
||||
/** ### `verge.yaml` schema */
|
||||
export type IVerge = IVerge_Serialize | IVerge_Deserialize
|
||||
|
||||
export type IVergeTestItem = {
|
||||
export type IVergeTestItem =
|
||||
| IVergeTestItem_Serialize
|
||||
| IVergeTestItem_Deserialize
|
||||
|
||||
export type IVergeTestItem_Deserialize = {
|
||||
uid?: string | null
|
||||
name?: string | null
|
||||
icon?: string | null
|
||||
url?: string | null
|
||||
}
|
||||
|
||||
export type IVergeTheme = {
|
||||
export type IVergeTestItem_Serialize = {
|
||||
uid: string | null
|
||||
name: string | null
|
||||
icon: string | null
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export type IVergeTheme = IVergeTheme_Serialize | IVergeTheme_Deserialize
|
||||
|
||||
export type IVergeTheme_Deserialize = {
|
||||
primary_color?: string | null
|
||||
secondary_color?: string | null
|
||||
primary_text?: string | null
|
||||
@@ -350,6 +311,19 @@ export type IVergeTheme = {
|
||||
css_injection?: string | null
|
||||
}
|
||||
|
||||
export type IVergeTheme_Serialize = {
|
||||
primary_color: string | null
|
||||
secondary_color: string | null
|
||||
primary_text: string | null
|
||||
secondary_text: string | null
|
||||
info_color: string | null
|
||||
error_color: string | null
|
||||
warning_color: string | null
|
||||
success_color: string | null
|
||||
font_family: string | null
|
||||
css_injection: string | null
|
||||
}
|
||||
|
||||
/** ### `verge.yaml` schema */
|
||||
export type IVerge_Deserialize = {
|
||||
/** silent | error | warn | info | debug | trace */
|
||||
@@ -392,7 +366,7 @@ export type IVerge_Deserialize = {
|
||||
proxy_auto_config?: boolean | null
|
||||
pac_file_content?: string | null
|
||||
proxy_host?: string | null
|
||||
theme_setting?: IVergeTheme | null
|
||||
theme_setting?: IVergeTheme_Deserialize | null
|
||||
web_ui_list?: string[] | null
|
||||
clash_core?: string | null
|
||||
/** format: {func},{key} */
|
||||
@@ -408,7 +382,7 @@ export type IVerge_Deserialize = {
|
||||
auto_delay_detection_interval_minutes?: number | null
|
||||
enable_builtin_enhanced?: boolean | null
|
||||
proxy_layout_column?: number | null
|
||||
test_list?: IVergeTestItem[] | null
|
||||
test_list?: IVergeTestItem_Deserialize[] | null
|
||||
/** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */
|
||||
auto_log_clean?: number | null
|
||||
enable_auto_backup_schedule?: boolean | null
|
||||
@@ -457,13 +431,13 @@ export type IVerge_Serialize = {
|
||||
traffic_graph: boolean | null
|
||||
/** show memory info (only for Clash Meta) */
|
||||
enable_memory_usage: boolean | null
|
||||
enable_group_icon?: boolean | null
|
||||
enable_group_icon?: boolean
|
||||
pause_render_traffic_stats_on_blur: boolean | null
|
||||
common_tray_icon?: boolean | null
|
||||
menu_icon?: string | null
|
||||
menu_order?: string[] | null
|
||||
proxy_group_tools_position?: string | null
|
||||
notice_position?: string | null
|
||||
common_tray_icon?: boolean
|
||||
menu_icon?: string
|
||||
menu_order?: string[]
|
||||
proxy_group_tools_position?: string
|
||||
notice_position?: string
|
||||
collapse_navbar: boolean | null
|
||||
sysproxy_tray_icon: boolean | null
|
||||
tun_tray_icon: boolean | null
|
||||
@@ -482,11 +456,11 @@ export type IVerge_Serialize = {
|
||||
proxy_auto_config: boolean | null
|
||||
pac_file_content: string | null
|
||||
proxy_host: string | null
|
||||
theme_setting: IVergeTheme | null
|
||||
theme_setting: IVergeTheme_Serialize | null
|
||||
web_ui_list: string[] | null
|
||||
clash_core?: string | null
|
||||
clash_core?: string
|
||||
/** format: {func},{key} */
|
||||
hotkeys?: string[] | null
|
||||
hotkeys?: string[]
|
||||
enable_global_hotkey: boolean | null
|
||||
home_cards: JsonValue | null
|
||||
auto_close_connection: boolean | null
|
||||
@@ -498,7 +472,7 @@ export type IVerge_Serialize = {
|
||||
auto_delay_detection_interval_minutes: number | null
|
||||
enable_builtin_enhanced: boolean | null
|
||||
proxy_layout_column: number | null
|
||||
test_list: IVergeTestItem[] | null
|
||||
test_list: IVergeTestItem_Serialize[] | null
|
||||
/** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */
|
||||
auto_log_clean: number | null
|
||||
enable_auto_backup_schedule: boolean | null
|
||||
@@ -510,10 +484,10 @@ export type IVerge_Serialize = {
|
||||
verge_socks_enabled: boolean | null
|
||||
verge_port: number | null
|
||||
verge_http_enabled: boolean | null
|
||||
webdav_url?: string | null
|
||||
webdav_username?: string | null
|
||||
webdav_password?: string | null
|
||||
tray_proxy_groups_display_mode?: string | null
|
||||
webdav_url?: string
|
||||
webdav_username?: string
|
||||
webdav_password?: string
|
||||
tray_proxy_groups_display_mode?: string
|
||||
tray_inline_outbound_modes: boolean | null
|
||||
enable_auto_light_weight_mode: boolean | null
|
||||
/** 自动进入轻量模式的延迟(分钟) */
|
||||
@@ -658,7 +632,7 @@ export type PrfItem_Deserialize = {
|
||||
file?: string | null
|
||||
desc?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
selected?: PrfSelected_Deserialize[] | null
|
||||
latency_test_urls?: { [key in string]: string } | null
|
||||
extra?: PrfExtra | null
|
||||
updated?: number | null
|
||||
@@ -672,14 +646,14 @@ export type PrfItem_Serialize = {
|
||||
type: string | null
|
||||
name: string | null
|
||||
file: string | null
|
||||
desc?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
latency_test_urls?: { [key in string]: string } | null
|
||||
extra?: PrfExtra | null
|
||||
desc?: string
|
||||
url?: string
|
||||
selected?: PrfSelected_Serialize[]
|
||||
latency_test_urls?: { [key in string]: string }
|
||||
extra?: PrfExtra
|
||||
updated: number | null
|
||||
option?: PrfOption_Serialize | null
|
||||
home?: string | null
|
||||
option?: PrfOption_Serialize
|
||||
home?: string
|
||||
}
|
||||
|
||||
export type PrfOption = PrfOption_Serialize | PrfOption_Deserialize
|
||||
@@ -705,18 +679,18 @@ export type PrfOption_Deserialize = {
|
||||
}
|
||||
|
||||
export type PrfOption_Serialize = {
|
||||
user_agent?: string | null
|
||||
with_proxy?: boolean | null
|
||||
self_proxy?: boolean | null
|
||||
update_interval?: number | null
|
||||
user_agent?: string
|
||||
with_proxy?: boolean
|
||||
self_proxy?: boolean
|
||||
update_interval?: number
|
||||
/**
|
||||
* HTTP request timeout in seconds
|
||||
* default is 60 seconds
|
||||
*/
|
||||
timeout_seconds?: number | null
|
||||
timeout_seconds?: number
|
||||
/** default is `false` */
|
||||
danger_accept_invalid_certs?: boolean | null
|
||||
allow_auto_update?: boolean | null
|
||||
danger_accept_invalid_certs?: boolean
|
||||
allow_auto_update?: boolean
|
||||
merge: string | null
|
||||
script: string | null
|
||||
rules: string | null
|
||||
@@ -724,11 +698,18 @@ export type PrfOption_Serialize = {
|
||||
groups: string | null
|
||||
}
|
||||
|
||||
export type PrfSelected = {
|
||||
export type PrfSelected = PrfSelected_Serialize | PrfSelected_Deserialize
|
||||
|
||||
export type PrfSelected_Deserialize = {
|
||||
name?: string | null
|
||||
now?: string | null
|
||||
}
|
||||
|
||||
export type PrfSelected_Serialize = {
|
||||
name: string | null
|
||||
now: string | null
|
||||
}
|
||||
|
||||
export type ProfileCreate = ProfileCreate_Serialize | ProfileCreate_Deserialize
|
||||
|
||||
export type ProfileCreate_Deserialize = {
|
||||
@@ -758,7 +739,7 @@ export type ProfileDnsSettings_Deserialize = {
|
||||
|
||||
export type ProfileDnsSettings_Serialize = {
|
||||
enabled: boolean
|
||||
confirmation?: string | null
|
||||
confirmation?: string
|
||||
}
|
||||
|
||||
export type ProfilePatch = ProfilePatch_Serialize | ProfilePatch_Deserialize
|
||||
@@ -769,7 +750,7 @@ export type ProfilePatch_Deserialize = {
|
||||
desc?: string | null
|
||||
file?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
selected?: PrfSelected_Deserialize[] | null
|
||||
latency_test_urls?: { [key in string]: string } | null
|
||||
extra?: PrfExtra | null
|
||||
updated?: number | null
|
||||
@@ -782,17 +763,25 @@ export type ProfilePatch_Serialize = {
|
||||
desc: string | null
|
||||
file: string | null
|
||||
url: string | null
|
||||
selected: PrfSelected[] | null
|
||||
selected: PrfSelected_Serialize[] | null
|
||||
latency_test_urls: { [key in string]: string } | null
|
||||
extra: PrfExtra | null
|
||||
updated: number | null
|
||||
option: PrfOption_Serialize | null
|
||||
}
|
||||
|
||||
export type ProfileSelection = {
|
||||
export type ProfileSelection =
|
||||
| ProfileSelection_Serialize
|
||||
| ProfileSelection_Deserialize
|
||||
|
||||
export type ProfileSelection_Deserialize = {
|
||||
current?: string | null
|
||||
}
|
||||
|
||||
export type ProfileSelection_Serialize = {
|
||||
current: string | null
|
||||
}
|
||||
|
||||
export type ProfileUpdatePayload = {
|
||||
uid: string
|
||||
}
|
||||
@@ -825,11 +814,11 @@ export type ProxyGroupView_Deserialize = {
|
||||
export type ProxyGroupView_Serialize = {
|
||||
name: string
|
||||
type: string
|
||||
now?: string | null
|
||||
fixed?: string | null
|
||||
hidden?: boolean | null
|
||||
icon?: string | null
|
||||
testUrl?: string | null
|
||||
now?: string
|
||||
fixed?: string
|
||||
hidden?: boolean
|
||||
icon?: string
|
||||
testUrl?: string
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyLatencyHistory }
|
||||
members: ProxyMemberRef[]
|
||||
@@ -874,10 +863,10 @@ export type ProxyNodeView_Serialize = {
|
||||
type: string
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyLatencyHistory }
|
||||
id?: string | null
|
||||
hidden?: boolean | null
|
||||
icon?: string | null
|
||||
testUrl?: string | null
|
||||
id?: string
|
||||
hidden?: boolean
|
||||
icon?: string
|
||||
testUrl?: string
|
||||
source: ProxyNodeSource
|
||||
} & ProxyCapabilities
|
||||
|
||||
@@ -906,8 +895,8 @@ export type ProxyProviderView_Deserialize = {
|
||||
export type ProxyProviderView_Serialize = {
|
||||
name: string
|
||||
vehicleType: ProxyProviderVehicleType
|
||||
updatedAt?: string | null
|
||||
subscriptionInfo?: ProxySubscriptionInfo | null
|
||||
updatedAt?: string
|
||||
subscriptionInfo?: ProxySubscriptionInfo
|
||||
proxyRecordIds: string[]
|
||||
}
|
||||
|
||||
|
||||
+216
-207
@@ -32,221 +32,230 @@ export const forgetShownStartupError = () => {
|
||||
* type makes a backend status without a frontend entry a compile error (and
|
||||
* contract regeneration keeps the union in sync).
|
||||
*/
|
||||
const handlers: Record<
|
||||
NoticeStatus,
|
||||
(msg: string, navigate: NavigateFunction) => void
|
||||
> = {
|
||||
info: (msg) => {
|
||||
if (msg) showNotice.info(msg)
|
||||
},
|
||||
'import_sub_url::ok': (_msg, navigate) => {
|
||||
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
|
||||
// navigate("/profile", { state: { current: msg } });
|
||||
navigate('/profile')
|
||||
showNotice.success(
|
||||
'shared.feedback.notifications.importSubscriptionSuccess',
|
||||
)
|
||||
},
|
||||
'import_sub_url::error': (msg, navigate) => {
|
||||
navigate('/profile')
|
||||
showNotice.error(msg)
|
||||
},
|
||||
'set_config::error': (msg) => showNotice.error(msg),
|
||||
'core_start::error': () => {
|
||||
const read = ++startupErrorReads
|
||||
void getCoreStartupError()
|
||||
.then(async (failure) => {
|
||||
if (read < settledStartupErrorRead) return
|
||||
settledStartupErrorRead = read
|
||||
if (!failure) {
|
||||
shownStartupError = null
|
||||
return
|
||||
}
|
||||
const window = getCurrentWindow()
|
||||
const [visible, minimized] = await Promise.all([
|
||||
window.isVisible(),
|
||||
window.isMinimized(),
|
||||
])
|
||||
const shown = `${failure.kind}:${failure.detail}`
|
||||
if (
|
||||
read === settledStartupErrorRead &&
|
||||
visible &&
|
||||
!minimized &&
|
||||
shownStartupError !== shown
|
||||
) {
|
||||
shownStartupError = shown
|
||||
showNotice.error(
|
||||
failure.kind === 'serviceCoreStopped'
|
||||
? 'settings.feedback.errors.clash.serviceCoreStopped'
|
||||
: 'settings.feedback.errors.clash.startFailed',
|
||||
failure.detail,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending core startup error', error)
|
||||
})
|
||||
},
|
||||
'service_core::repair_required': () => {
|
||||
void takeServiceRepairNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
requestService({ reason: 'serviceLocationRefused' })
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending service repair notice', error)
|
||||
})
|
||||
},
|
||||
'service_core::app_data_not_owned': () => {
|
||||
void takeServiceOwnerNotice()
|
||||
.then((command) => {
|
||||
if (command) {
|
||||
if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice)
|
||||
shownOwnerNotice = showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.appDataNotOwned',
|
||||
{ command },
|
||||
0,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending service owner notice', error)
|
||||
})
|
||||
},
|
||||
'service_core::sidecar_fallback': () => {
|
||||
void takeServiceFallbackNotice()
|
||||
.then((notice) => {
|
||||
if (notice?.kind === 'coreRejected') {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.permissionFallback',
|
||||
{ reason: notice.reason },
|
||||
0,
|
||||
)
|
||||
} else if (notice?.kind === 'notAutoStarted') {
|
||||
requestService({ reason: 'serviceNotAutoStarted' })
|
||||
} else if (notice) {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.sidecarFallback',
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service fallback notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'dns_override::auto_disabled': () => {
|
||||
void takeDnsOverrideNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
showNotice.info('settings.modals.dns.protection.autoDisabled')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending DNS override notice', error)
|
||||
})
|
||||
},
|
||||
'enhance::discarded_keys': () => {
|
||||
void takeDiscardedKeysNotice()
|
||||
.then((keys) => {
|
||||
if (keys) {
|
||||
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
|
||||
keys,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending discarded keys notice', error)
|
||||
})
|
||||
},
|
||||
'tun_mode::auto_disabled': () =>
|
||||
showNotice.info(
|
||||
'settings.sections.system.notifications.tunMode.autoDisabled',
|
||||
),
|
||||
'tun_mode::auto_disable_failed': () =>
|
||||
showNotice.error(
|
||||
'settings.sections.system.notifications.tunMode.autoDisableFailed',
|
||||
),
|
||||
'app_restart::core_stop_failed': (msg) =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.restartCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
'app_quit::core_stop_failed': (msg) =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.quitCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
update_with_clash_proxy: (msg) =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.updater.withClashProxySuccess',
|
||||
msg,
|
||||
),
|
||||
update_failed_even_with_clash: (msg) =>
|
||||
showNotice.error(
|
||||
'settings.feedback.notifications.updater.withClashProxyFailed',
|
||||
msg,
|
||||
),
|
||||
'reactivate_profiles::error': (msg) => showNotice.error(msg),
|
||||
update_failed: (msg) => showNotice.error(msg),
|
||||
'config_validate::boot_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.config.bootFailed', msg),
|
||||
'config_validate::error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.config.failed', msg),
|
||||
'config_validate::process_terminated': () =>
|
||||
showNotice.error('shared.feedback.validation.config.processTerminated'),
|
||||
'config_validate::script_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.fileError', msg),
|
||||
'config_validate::script_syntax_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.syntaxError', msg),
|
||||
'config_validate::script_missing_main': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.missingMain', msg),
|
||||
'config_validate::file_not_found': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.fileNotFound', msg),
|
||||
'config_validate::yaml_syntax_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.yaml.syntaxError', msg),
|
||||
'config_validate::yaml_read_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.yaml.readError', msg),
|
||||
'config_validate::yaml_mapping_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.yaml.mappingError', msg),
|
||||
'config_validate::merge_syntax_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.merge.syntaxError', msg),
|
||||
'config_validate::merge_mapping_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.merge.mappingError', msg),
|
||||
'config_core::change_success': (msg) =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.clash.changeSuccess',
|
||||
msg,
|
||||
),
|
||||
'config_core::change_error': (msg) =>
|
||||
showNotice.error('settings.feedback.notifications.clash.changeFailed', msg),
|
||||
'mixed_port::fallback': (msg) => {
|
||||
const [oldPort, newPort] = msg.split(',')
|
||||
showNotice.info('settings.modals.clashPort.messages.automaticFallback', {
|
||||
oldPort,
|
||||
newPort,
|
||||
})
|
||||
},
|
||||
'mixed_port::fallback_error': (msg) =>
|
||||
showNotice.error(
|
||||
'settings.modals.clashPort.messages.automaticFallbackFailed',
|
||||
{ error: msg },
|
||||
),
|
||||
}
|
||||
|
||||
export const handleNoticeMessage = (
|
||||
status: string,
|
||||
msg: string,
|
||||
t: TranslateFunction,
|
||||
navigate: NavigateFunction,
|
||||
) => {
|
||||
const handlers: Record<NoticeStatus, () => void> = {
|
||||
info: () => {
|
||||
if (msg) showNotice.info(msg)
|
||||
},
|
||||
'import_sub_url::ok': () => {
|
||||
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
|
||||
// navigate("/profile", { state: { current: msg } });
|
||||
navigate('/profile')
|
||||
showNotice.success(
|
||||
'shared.feedback.notifications.importSubscriptionSuccess',
|
||||
)
|
||||
},
|
||||
'import_sub_url::error': () => {
|
||||
navigate('/profile')
|
||||
showNotice.error(msg)
|
||||
},
|
||||
'set_config::error': () => showNotice.error(msg),
|
||||
'core_start::error': () => {
|
||||
const read = ++startupErrorReads
|
||||
void getCoreStartupError()
|
||||
.then(async (failure) => {
|
||||
if (read < settledStartupErrorRead) return
|
||||
settledStartupErrorRead = read
|
||||
if (!failure) {
|
||||
shownStartupError = null
|
||||
return
|
||||
}
|
||||
const window = getCurrentWindow()
|
||||
const [visible, minimized] = await Promise.all([
|
||||
window.isVisible(),
|
||||
window.isMinimized(),
|
||||
])
|
||||
const shown = `${failure.kind}:${failure.detail}`
|
||||
if (
|
||||
read === settledStartupErrorRead &&
|
||||
visible &&
|
||||
!minimized &&
|
||||
shownStartupError !== shown
|
||||
) {
|
||||
shownStartupError = shown
|
||||
showNotice.error(
|
||||
failure.kind === 'serviceCoreStopped'
|
||||
? 'settings.feedback.errors.clash.serviceCoreStopped'
|
||||
: 'settings.feedback.errors.clash.startFailed',
|
||||
failure.detail,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending core startup error', error)
|
||||
})
|
||||
},
|
||||
'service_core::repair_required': () => {
|
||||
void takeServiceRepairNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
requestService({ reason: 'serviceLocationRefused' })
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service repair notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'service_core::app_data_not_owned': () => {
|
||||
void takeServiceOwnerNotice()
|
||||
.then((command) => {
|
||||
if (command) {
|
||||
if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice)
|
||||
shownOwnerNotice = showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.appDataNotOwned',
|
||||
{ command },
|
||||
0,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service owner notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'service_core::sidecar_fallback': () => {
|
||||
void takeServiceFallbackNotice()
|
||||
.then((notice) => {
|
||||
if (notice?.kind === 'coreRejected') {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.permissionFallback',
|
||||
{ reason: notice.reason },
|
||||
0,
|
||||
)
|
||||
} else if (notice?.kind === 'notAutoStarted') {
|
||||
requestService({ reason: 'serviceNotAutoStarted' })
|
||||
} else if (notice) {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.sidecarFallback',
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service fallback notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'dns_override::auto_disabled': () => {
|
||||
void takeDnsOverrideNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
showNotice.info('settings.modals.dns.protection.autoDisabled')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending DNS override notice', error)
|
||||
})
|
||||
},
|
||||
'enhance::discarded_keys': () => {
|
||||
void takeDiscardedKeysNotice()
|
||||
.then((keys) => {
|
||||
if (keys) {
|
||||
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
|
||||
keys,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending discarded keys notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'tun_mode::auto_disabled': () =>
|
||||
showNotice.info(
|
||||
'settings.sections.system.notifications.tunMode.autoDisabled',
|
||||
),
|
||||
'tun_mode::auto_disable_failed': () =>
|
||||
showNotice.error(
|
||||
'settings.sections.system.notifications.tunMode.autoDisableFailed',
|
||||
),
|
||||
'app_restart::core_stop_failed': () =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.restartCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
'app_quit::core_stop_failed': () =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.quitCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
update_with_clash_proxy: () =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.updater.withClashProxySuccess',
|
||||
msg,
|
||||
),
|
||||
update_failed_even_with_clash: () =>
|
||||
showNotice.error(
|
||||
'settings.feedback.notifications.updater.withClashProxyFailed',
|
||||
msg,
|
||||
),
|
||||
'reactivate_profiles::error': () => showNotice.error(msg),
|
||||
update_failed: () => showNotice.error(msg),
|
||||
'config_validate::boot_error': () =>
|
||||
showNotice.error('shared.feedback.validation.config.bootFailed', msg),
|
||||
'config_validate::error': () =>
|
||||
showNotice.error('shared.feedback.validation.config.failed', msg),
|
||||
'config_validate::process_terminated': () =>
|
||||
showNotice.error('shared.feedback.validation.config.processTerminated'),
|
||||
'config_validate::script_error': () =>
|
||||
showNotice.error('shared.feedback.validation.script.fileError', msg),
|
||||
'config_validate::script_syntax_error': () =>
|
||||
showNotice.error('shared.feedback.validation.script.syntaxError', msg),
|
||||
'config_validate::script_missing_main': () =>
|
||||
showNotice.error('shared.feedback.validation.script.missingMain', msg),
|
||||
'config_validate::file_not_found': () =>
|
||||
showNotice.error('shared.feedback.validation.script.fileNotFound', msg),
|
||||
'config_validate::yaml_syntax_error': () =>
|
||||
showNotice.error('shared.feedback.validation.yaml.syntaxError', msg),
|
||||
'config_validate::yaml_read_error': () =>
|
||||
showNotice.error('shared.feedback.validation.yaml.readError', msg),
|
||||
'config_validate::yaml_mapping_error': () =>
|
||||
showNotice.error('shared.feedback.validation.yaml.mappingError', msg),
|
||||
'config_validate::merge_syntax_error': () =>
|
||||
showNotice.error('shared.feedback.validation.merge.syntaxError', msg),
|
||||
'config_validate::merge_mapping_error': () =>
|
||||
showNotice.error('shared.feedback.validation.merge.mappingError', msg),
|
||||
'config_core::change_success': () =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.clash.changeSuccess',
|
||||
msg,
|
||||
),
|
||||
'config_core::change_error': () =>
|
||||
showNotice.error(
|
||||
'settings.feedback.notifications.clash.changeFailed',
|
||||
msg,
|
||||
),
|
||||
'mixed_port::fallback': () => {
|
||||
const [oldPort, newPort] = msg.split(',')
|
||||
showNotice.info('settings.modals.clashPort.messages.automaticFallback', {
|
||||
oldPort,
|
||||
newPort,
|
||||
})
|
||||
},
|
||||
'mixed_port::fallback_error': () =>
|
||||
showNotice.error(
|
||||
'settings.modals.clashPort.messages.automaticFallbackFailed',
|
||||
{ error: msg },
|
||||
),
|
||||
}
|
||||
|
||||
const handler = handlers[status as NoticeStatus]
|
||||
if (handler) {
|
||||
handler(msg, navigate)
|
||||
handler()
|
||||
} else {
|
||||
console.warn(`未处理的通知状态: ${status}`)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ const withLoading = (
|
||||
next.delete(uid)
|
||||
}
|
||||
}
|
||||
if (next.size === state.profileUpdates.loading.size) return state
|
||||
return {
|
||||
...state,
|
||||
profileUpdates: { ...state.profileUpdates, loading: next },
|
||||
|
||||
@@ -322,8 +322,6 @@ export const translationKeys = [
|
||||
'proxies.page.empty.actions.openLogs',
|
||||
'proxies.page.chain.header',
|
||||
'proxies.page.chain.warning',
|
||||
'proxies.page.chain.confirmEnable',
|
||||
'proxies.page.chain.keepDisabled',
|
||||
'proxies.page.chain.empty',
|
||||
'proxies.page.chain.instruction',
|
||||
'proxies.page.chain.minimumNodes',
|
||||
|
||||
@@ -525,7 +525,6 @@ export interface TranslationResources {
|
||||
toggleChain: string
|
||||
}
|
||||
chain: {
|
||||
confirmEnable: string
|
||||
connectFailed: string
|
||||
disconnectFailed: string
|
||||
duplicateNode: string
|
||||
@@ -534,7 +533,6 @@ export interface TranslationResources {
|
||||
exitNode: string
|
||||
header: string
|
||||
instruction: string
|
||||
keepDisabled: string
|
||||
minimumNodes: string
|
||||
minimumNodesHint: string
|
||||
warning: string
|
||||
|
||||
Reference in New Issue
Block a user