mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-13 22:32:15 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c576122a4c |
@@ -1,3 +1,2 @@
|
||||
avoid-breaking-exported-api = true
|
||||
allow-unwrap-in-tests = true
|
||||
cognitive-complexity-threshold = 25
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "26.13.1",
|
||||
"version": "26.13.0",
|
||||
"pnpmVersion": "12.18.0"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.100.0",
|
||||
"version": "1.99.1",
|
||||
"profile": "default"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git:1": {},
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.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.100.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
@@ -240,7 +240,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.100.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
@@ -262,7 +262,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
@@ -373,7 +373,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.100.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
@@ -479,7 +479,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6.1.1
|
||||
name: Install pnpm
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Install gh-aw extension
|
||||
uses: github/gh-aw-actions/setup-cli@53d0749e19424a918acd82b4d140264ce48f2f14 # v1.2.2
|
||||
with:
|
||||
version: v1.2.2
|
||||
version: v1.0.1
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
- name: Install frontend dependencies
|
||||
env:
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.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.100.0
|
||||
uses: dtolnay/rust-toolchain@1.99.1
|
||||
|
||||
- name: Rust Cache
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
@@ -121,7 +121,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.100.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
if: github.event.action != 'closed'
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
- name: Validate source and publish the approved snapshot
|
||||
id: source
|
||||
env:
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.100.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
@@ -308,7 +308,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.100.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
@@ -330,7 +330,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
@@ -439,7 +439,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.100.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
@@ -461,7 +461,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
@@ -542,7 +542,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
@@ -565,7 +565,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -593,7 +593,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -661,7 +661,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.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.100.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
node-version: '26.13.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: '26.13.1'
|
||||
node-version: '26.13.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: '26.13.1'
|
||||
node-version: '26.13.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: '26.13.1'
|
||||
node-version: '26.13.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
nodejs 26.13.1
|
||||
nodejs 26.13.0
|
||||
pnpm 12.18.0
|
||||
rust 1.100.0
|
||||
rust 1.99.1
|
||||
|
||||
Generated
+7
-6
@@ -1123,6 +1123,7 @@ dependencies = [
|
||||
"rfd 0.18.0",
|
||||
"runas",
|
||||
"rustls",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
@@ -7072,9 +7073,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.40.3"
|
||||
version = "0.40.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a86bac2f7d06e6384984118ad727f5694110f0ec44d924d05c8fdec3d030bee6"
|
||||
checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
@@ -9087,9 +9088,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.12"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7265c9301c16a8711c992c2d8da90ee77ef54b1ed51482d6dfeb00dc756b1297"
|
||||
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -10125,9 +10126,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.5.1"
|
||||
version = "9.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3476fd497ac8af5b1ec7b51acab7bef380f7e8dd8e0487595bafd839417a302"
|
||||
checksum = "0700e6c427aff64acdbf02e85fbfd946c2ee12319d1af013092f3a2178914ff8"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
|
||||
@@ -132,7 +132,6 @@ large_const_arrays = "warn"
|
||||
large_futures = "deny"
|
||||
large_stack_arrays = "warn"
|
||||
large_types_passed_by_value = "deny"
|
||||
map_unwrap_or = "deny"
|
||||
missing_const_for_fn = "deny"
|
||||
mutex_atomic = "deny"
|
||||
mutex_integer = "deny"
|
||||
|
||||
+1
-3
@@ -29,20 +29,17 @@
|
||||
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
|
||||
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
|
||||
- 修复订阅中唯一的代理组名为 `GLOBAL` 或没有代理组时,规则模式下代理页面显示「未获取到代理信息」
|
||||
- 修复窗口较窄时,首页「当前节点」卡片中过长的节点名把延迟标签挤出卡片
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
- 修复 macOS TUN 模式下原有 DNS 被清除、关闭后 DNS 未恢复及联网后设置未生效
|
||||
- 修复 macOS 断网时误报代理已回退直连,恢复联网后自动重试系统代理
|
||||
- 修复 macOS 切换主题后标题栏保留原有深浅色
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 新增连接页面按网络协议(TCP/UDP)筛选连接
|
||||
|
||||
</details>
|
||||
|
||||
@@ -51,6 +48,7 @@
|
||||
|
||||
- 优化连接、规则和日志筛选:切换页面后保留过滤条件
|
||||
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
|
||||
- 优化代理页面和首页节点卡片,减少延迟更新时的卡顿
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
|
||||
@@ -206,10 +206,10 @@ impl Logger {
|
||||
.map(|s| (*s).to_string())
|
||||
.or_else(|| info.payload().downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "Unknown panic payload".to_string());
|
||||
let location = info.location().map_or_else(
|
||||
|| "Unknown location".to_string(),
|
||||
|loc| format!("{}:{}", loc.file(), loc.line()),
|
||||
);
|
||||
let location = info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
|
||||
.unwrap_or_else(|| "Unknown location".to_string());
|
||||
logging!(error, Type::System, "Panic occurred at {}: {}", location, payload);
|
||||
Self::global().flush_logs();
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
@@ -13,13 +13,14 @@ pub(super) async fn check_chatgpt(client: &Client) -> UnlockItem {
|
||||
|
||||
let web_status = get_text(client, "https://api.openai.com/compliance/cookie_requirements")
|
||||
.await
|
||||
.map_or("Failed", |body| {
|
||||
.map(|body| {
|
||||
if body.to_ascii_lowercase().contains("unsupported_country") {
|
||||
"Unsupported Country/Region"
|
||||
} else {
|
||||
"Yes"
|
||||
}
|
||||
});
|
||||
})
|
||||
.unwrap_or("Failed");
|
||||
|
||||
UnlockItem::checked(CHATGPT_WEB_NAME, web_status, region)
|
||||
}
|
||||
|
||||
@@ -157,10 +157,11 @@ async fn is_unavailable(client: &Client) -> bool {
|
||||
.get("https://disneyplus.com")
|
||||
.send()
|
||||
.await
|
||||
.map_or(true, |response| {
|
||||
.map(|response| {
|
||||
let url = response.url().as_str();
|
||||
url.contains("preview") || url.contains("unavailable")
|
||||
})
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
async fn fallback_region(client: &Client) -> Option<UnlockItem> {
|
||||
|
||||
@@ -114,10 +114,11 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
}
|
||||
|
||||
match response.json::<FastResponse>().await {
|
||||
Ok(data) => data.targets.first().map_or_else(
|
||||
|| item("Unknown"),
|
||||
|target| UnlockItem::checked_region(NETFLIX_NAME, "Yes", &target.location.country),
|
||||
),
|
||||
Ok(data) => data
|
||||
.targets
|
||||
.first()
|
||||
.map(|target| UnlockItem::checked_region(NETFLIX_NAME, "Yes", &target.location.country))
|
||||
.unwrap_or_else(|| item("Unknown")),
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "解析 Fast.com API 响应失败: {e}");
|
||||
item("Failed (解析错误)")
|
||||
|
||||
@@ -12,7 +12,7 @@ parking_lot = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
# sysinfo 0.38.2 conflicts with dark-light
|
||||
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623
|
||||
sysinfo = { version = "0.40.3", default-features = false, features = ["network", "system"] }
|
||||
sysinfo = { version = "0.40.1", default-features = false, features = ["network", "system"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc = "0.2.191"
|
||||
|
||||
@@ -118,7 +118,8 @@ fn is_binary_admin() -> bool {
|
||||
#[cfg(windows)]
|
||||
Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.is_ok_and(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.7.0",
|
||||
"knip": "6.45.1",
|
||||
"knip": "6.44.2",
|
||||
"lint-staged": "^17.5.1",
|
||||
"sass": "^1.104.1",
|
||||
"tar": "^7.5.22",
|
||||
|
||||
Generated
+10
-9
@@ -396,8 +396,8 @@ importers:
|
||||
specifier: ^2.7.0
|
||||
version: 2.7.0
|
||||
knip:
|
||||
specifier: 6.45.1
|
||||
version: 6.45.1
|
||||
specifier: 6.44.2
|
||||
version: 6.44.2
|
||||
lint-staged:
|
||||
specifier: ^17.5.1
|
||||
version: 17.5.1
|
||||
@@ -3856,8 +3856,8 @@ packages:
|
||||
keyv@5.6.0:
|
||||
resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
|
||||
|
||||
knip@6.45.1:
|
||||
resolution: {integrity: sha512-zp3zXKy2cZWiIttdlWU5fisKxVQaLSU6471ej0KGz1GdHdaHxpzTAO/LIyFNc3gU3v1W7LJaqvYumoO3l/u+6A==}
|
||||
knip@6.44.2:
|
||||
resolution: {integrity: sha512-6URO7pnqtOnyvSsXfW6+8C9j1lfcicxUCysXO0RgMtHTZ96HqCXh29op8iabeQW5JeIc8dJWwzSdJVWDokeScQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
@@ -4804,8 +4804,8 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
unbash@6.0.4:
|
||||
resolution: {integrity: sha512-t3OptUArewg9OpYGDskTSlxhyLDahfmZ4d36hE44Efb1y3HRLLpzsVqwH+AwBVymP4t7pP71xN+nnduudn2awA==}
|
||||
unbash@6.0.1:
|
||||
resolution: {integrity: sha512-0db9eS5iqybux41zLkX69nk90KTci7F7eZ+gXzEG/Q7E0tAqEBhRh5fkZvk1N7jT7QkGWqq34i8SFGPFEe1V8A==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
undici-types@6.21.0:
|
||||
@@ -8684,7 +8684,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@keyv/serialize': 1.1.1
|
||||
|
||||
knip@6.45.1:
|
||||
knip@6.44.2:
|
||||
dependencies:
|
||||
formatly: 0.11.4
|
||||
get-tsconfig: 4.14.3
|
||||
@@ -8695,8 +8695,9 @@ snapshots:
|
||||
smol-toml: 1.9.1
|
||||
strip-json-comments: 5.0.3
|
||||
tinyglobby: 0.2.17
|
||||
unbash: 6.0.4
|
||||
unbash: 6.0.1
|
||||
yaml: 2.9.1
|
||||
zod: 4.6.5
|
||||
|
||||
lazystream@1.0.1:
|
||||
dependencies:
|
||||
@@ -9786,7 +9787,7 @@ snapshots:
|
||||
|
||||
typescript@6.0.3: {}
|
||||
|
||||
unbash@6.0.4: {}
|
||||
unbash@6.0.1: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.100.0"
|
||||
channel = "1.99.1"
|
||||
components = ["rustfmt", "clippy"]
|
||||
|
||||
@@ -8,7 +8,7 @@ repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||
default-run = "clash-verge"
|
||||
build = "build.rs"
|
||||
edition = "2024"
|
||||
rust-version = "1.100"
|
||||
rust-version = "1.99"
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
@@ -77,7 +77,7 @@ tauri-plugin-dialog = "2.8.4"
|
||||
tauri-plugin-fs = "2.7.1"
|
||||
tauri-plugin-deep-link = "2.6.3"
|
||||
tauri-plugin-window-state = "2.5.0"
|
||||
zip = "9.5.1"
|
||||
zip = "9.5.0"
|
||||
flate2 = "1.1.11"
|
||||
reqwest_dav = "0.3.3"
|
||||
aes-gcm = { version = "0.11.1" }
|
||||
@@ -86,6 +86,7 @@ sha2 = "0.11.1"
|
||||
getrandom = "0.4.3"
|
||||
futures = "0.3.34"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.8.0"
|
||||
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||
tauri-plugin-http = "2.8.3"
|
||||
@@ -99,7 +100,7 @@ clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verg
|
||||
arc-swap = "1.9.5"
|
||||
tokio-rustls = "0.26.6"
|
||||
rustls = { version = "0.23.46", features = ["ring"] }
|
||||
webpki-roots = "1.0.12"
|
||||
webpki-roots = "1.0.11"
|
||||
dark-light = "3.0.0"
|
||||
bytes = "1.12.1"
|
||||
tokio-util = { version = "0.7.20", features = ["time"] }
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"core:window:allow-set-theme",
|
||||
"core:window:allow-set-background-color",
|
||||
"core:webview:allow-set-webview-background-color",
|
||||
"notification:default",
|
||||
"http:default",
|
||||
"http:allow-fetch",
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// 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.next().map_or_else(
|
||||
|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"),
|
||||
std::path::PathBuf::from,
|
||||
);
|
||||
let output = args
|
||||
.next()
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"));
|
||||
if args.next().is_some() {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "expected at most one output path").into());
|
||||
}
|
||||
|
||||
@@ -18,11 +18,9 @@ use crate::{
|
||||
};
|
||||
use clash_verge_draft::{Draft, SharedDraft};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
mem::DropGuard,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -402,9 +400,9 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
let _switching_profile = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
});
|
||||
}
|
||||
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let profiles = IProfiles {
|
||||
|
||||
@@ -83,7 +83,8 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
.get("exclude-type")
|
||||
.and_then(|types| types.as_str())
|
||||
.filter(|types| !types.is_empty())
|
||||
.map_or_default(|types| types.split('|').map(str::to_owned).collect());
|
||||
.map(|types| types.split('|').map(str::to_owned).collect())
|
||||
.unwrap_or_default();
|
||||
Some((
|
||||
name,
|
||||
GroupScope {
|
||||
|
||||
@@ -191,7 +191,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error_msg = config_result.err().map_or_default(|err| err.to_string());
|
||||
let error_msg = config_result.err().map(|err| err.to_string()).unwrap_or_default();
|
||||
logging!(warn, Type::Config, "生成配置文件失败,使用默认配置: {error_msg}");
|
||||
CoreManager::global()
|
||||
.use_default_config(NoticeStatus::ConfigValidateError, "")
|
||||
|
||||
@@ -371,10 +371,9 @@ impl PrfItem {
|
||||
|
||||
let uid = help::get_uid("R").into();
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let name = name.map_or_else(
|
||||
|| filename.map_or_else(|| "Remote File".into(), |s| s.into()),
|
||||
|s| s.to_owned(),
|
||||
);
|
||||
let name = name
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into()));
|
||||
let data = resp.text();
|
||||
|
||||
let data = data.trim_start_matches('\u{feff}');
|
||||
|
||||
@@ -279,7 +279,8 @@ async fn cleanup_auto_backups() -> Result<()> {
|
||||
.and_then(|meta| meta.modified())
|
||||
.ok()
|
||||
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
|
||||
.map_or(0, |dur| dur.as_secs());
|
||||
.map(|dur| dur.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
files.push((path, modified));
|
||||
}
|
||||
|
||||
@@ -539,16 +539,18 @@ fn bind_socket(claim: &BindClaim) -> io::Result<Socket> {
|
||||
}
|
||||
|
||||
fn is_bind_conflict(error: &io::Error) -> bool {
|
||||
if error.kind() == io::ErrorKind::AddrInUse {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use windows_sys::Win32::Networking::WinSock::WSAEACCES;
|
||||
|
||||
error.kind() == io::ErrorKind::AddrInUse || error.raw_os_error() == Some(WSAEACCES)
|
||||
error.raw_os_error() == Some(WSAEACCES)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
error.kind() == io::ErrorKind::AddrInUse
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
||||
@@ -14,10 +14,10 @@ use anyhow::{Result, anyhow};
|
||||
use clash_verge_draft::DraftTransaction;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use clash_verge_service_ipc::StageRuntimeOutcome;
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
mem::DropGuard,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -191,9 +191,9 @@ impl CoreManager {
|
||||
logging!(debug, Type::Core, "Configuration update is already running");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
let _config_update = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
self.finish_config_update();
|
||||
});
|
||||
}
|
||||
|
||||
if !force && !self.should_update_config() {
|
||||
logging!(debug, Type::Core, "Skipping config update due to debounce");
|
||||
@@ -314,9 +314,9 @@ impl CoreManager {
|
||||
logging!(debug, Type::Core, "Configuration update is already running");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
let _config_update = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
self.finish_config_update();
|
||||
});
|
||||
}
|
||||
|
||||
let runtime = Config::runtime().await;
|
||||
let transaction = DraftTransaction::begin(vec![&runtime])?;
|
||||
|
||||
@@ -6,8 +6,9 @@ use crate::core::proxy_control::{self, SysproxyFailure};
|
||||
use crate::core::service::{SERVICE_MANAGER, ServiceStatus};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::{mem::DropGuard, path::Path};
|
||||
use std::path::Path;
|
||||
#[cfg(target_os = "windows")]
|
||||
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
||||
|
||||
@@ -355,9 +356,9 @@ impl CoreManager {
|
||||
if !self.try_start_config_update() {
|
||||
anyhow::bail!("configuration update is already running");
|
||||
}
|
||||
let _config_update = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
self.finish_config_update();
|
||||
});
|
||||
}
|
||||
self.start_core_during_config_update().await
|
||||
}
|
||||
|
||||
@@ -382,9 +383,9 @@ impl CoreManager {
|
||||
service_rejection,
|
||||
));
|
||||
}
|
||||
let _config_update = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
self.finish_config_update();
|
||||
});
|
||||
}
|
||||
// Lock order is config then lifecycle. disable_tun_and_persist commits the shared Verge
|
||||
// draft without claiming it, so without this it can commit another transaction's staged patch.
|
||||
let config_write = Config::lock_config_write().await;
|
||||
@@ -462,9 +463,9 @@ impl CoreManager {
|
||||
if !self.try_start_config_update() {
|
||||
anyhow::bail!("configuration update is already running");
|
||||
}
|
||||
let _config_update = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
self.finish_config_update();
|
||||
});
|
||||
}
|
||||
// Lock order is config then lifecycle, for the unclaimed draft write below.
|
||||
let _config_write = Config::lock_config_write().await;
|
||||
let _life = self.lifecycle_lock.lock().await;
|
||||
@@ -794,9 +795,9 @@ impl CoreManager {
|
||||
if !self.try_start_config_update() {
|
||||
anyhow::bail!("configuration update is already running");
|
||||
}
|
||||
let _config_update = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
self.finish_config_update();
|
||||
});
|
||||
}
|
||||
self.restart_core_during_config_update().await
|
||||
}
|
||||
|
||||
@@ -965,9 +966,9 @@ impl CoreManager {
|
||||
if !self.try_start_config_update() {
|
||||
return HandoffOutcome::NotReady;
|
||||
}
|
||||
let _config_update = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
self.finish_config_update();
|
||||
});
|
||||
}
|
||||
|
||||
// Lock order is config then lifecycle.
|
||||
let _life = self.lifecycle_lock.lock().await;
|
||||
|
||||
@@ -331,7 +331,9 @@ fn build_provider_records(
|
||||
providers: Option<ProxyProviders>,
|
||||
records: &mut BTreeMap<String, ProxyNodeView>,
|
||||
) -> (Vec<ProxyProviderView>, ProviderCandidates) {
|
||||
let providers = providers.map_or_default(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>());
|
||||
let providers = providers
|
||||
.map(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>())
|
||||
.unwrap_or_default();
|
||||
let mut views = Vec::new();
|
||||
let mut candidates = BTreeMap::new();
|
||||
|
||||
|
||||
@@ -126,7 +126,8 @@ fn allocate_provider_destination(destination: &str, url: &str, reserved: &mut Ha
|
||||
let path = Path::new(destination);
|
||||
let extension = path
|
||||
.extension()
|
||||
.map_or_default(|ext| format!(".{}", ext.to_string_lossy()));
|
||||
.map(|ext| format!(".{}", ext.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
let mut suffix = String::new();
|
||||
let mut sequence = 0;
|
||||
loop {
|
||||
|
||||
@@ -1470,7 +1470,10 @@ async fn create_sync_temp(target: &Path) -> Result<(PathBuf, tokio::fs::File)> {
|
||||
.await
|
||||
.with_context(|| format!("failed to create provider cache directory {}", parent.display()))?;
|
||||
}
|
||||
let name = target.file_name().map_or_default(|name| name.to_string_lossy());
|
||||
let name = target
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
use crate::utils::retry::{RetryError, RetryPolicy, retry};
|
||||
retry(
|
||||
RetryPolicy::fixed(std::num::NonZeroUsize::MIN.saturating_add(7), std::time::Duration::ZERO),
|
||||
|
||||
@@ -7,9 +7,9 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::RwLock;
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
mem::DropGuard,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -541,9 +541,9 @@ impl Sysopt {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let _reset_sysproxy = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
self.reset_sysproxy.store(false, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
let mut proxy_state = self.proxy_state_lock.lock().await;
|
||||
let _guard_operation = self.guard_operation_lock.lock().await;
|
||||
let drained = self.stop_proxy_guard_locked().await;
|
||||
|
||||
@@ -210,7 +210,8 @@ impl Tray {
|
||||
.latest_arc()
|
||||
.0
|
||||
.get("mode")
|
||||
.map_or("rule", |val| val.as_str().unwrap_or("rule"))
|
||||
.map(|val| val.as_str().unwrap_or("rule"))
|
||||
.unwrap_or("rule")
|
||||
.to_owned()
|
||||
};
|
||||
let profiles_config = Config::profiles().await;
|
||||
@@ -413,23 +414,26 @@ impl Tray {
|
||||
}
|
||||
|
||||
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<&str, &str> {
|
||||
hotkeys.as_ref().map_or_default(|h| {
|
||||
h.iter()
|
||||
.filter_map(|item| {
|
||||
let mut parts = item.split(',');
|
||||
match (parts.next(), parts.next()) {
|
||||
(Some(func), Some(key)) => {
|
||||
// 托盘菜单的 `accelerator` 在 Linux/Windows 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`)
|
||||
let uppercase_key = key.to_uppercase();
|
||||
let unsupported =
|
||||
uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD"));
|
||||
if unsupported { None } else { Some((func, key)) }
|
||||
hotkeys
|
||||
.as_ref()
|
||||
.map(|h| {
|
||||
h.iter()
|
||||
.filter_map(|item| {
|
||||
let mut parts = item.split(',');
|
||||
match (parts.next(), parts.next()) {
|
||||
(Some(func), Some(key)) => {
|
||||
// 托盘菜单的 `accelerator` 在 Linux/Windows 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`)
|
||||
let uppercase_key = key.to_uppercase();
|
||||
let unsupported =
|
||||
uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD"));
|
||||
if unsupported { None } else { Some((func, key)) }
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<&str, &str>>()
|
||||
})
|
||||
})
|
||||
.collect::<HashMap<&str, &str>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn create_profile_menu_item(
|
||||
@@ -489,14 +493,12 @@ fn create_subcreate_proxy_menu_item(
|
||||
.proxies
|
||||
.get(proxy_str)
|
||||
.and_then(|h| h.history.last())
|
||||
.map_or_else(
|
||||
|| "-ms".into(),
|
||||
|h| match h.delay {
|
||||
0 => "-ms".into(),
|
||||
delay if delay >= 10000 => "-ms".into(),
|
||||
_ => format!("{}ms", h.delay),
|
||||
},
|
||||
);
|
||||
.map(|h| match h.delay {
|
||||
0 => "-ms".into(),
|
||||
delay if delay >= 10000 => "-ms".into(),
|
||||
_ => format!("{}ms", h.delay),
|
||||
})
|
||||
.unwrap_or_else(|| "-ms".into());
|
||||
|
||||
let display_text = format!("{} | {}", proxy_str, delay_text);
|
||||
|
||||
@@ -609,7 +611,7 @@ async fn create_tray_menu(
|
||||
config
|
||||
.get("proxy-groups")
|
||||
.and_then(|groups| groups.as_sequence())
|
||||
.map_or_default(|groups| {
|
||||
.map(|groups| {
|
||||
groups
|
||||
.iter()
|
||||
.filter_map(|group| group.get("name"))
|
||||
@@ -618,6 +620,7 @@ async fn create_tray_menu(
|
||||
.map(|(index, name)| (name.into(), index))
|
||||
.collect::<HashMap<String, usize>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
(proxy_nodes_data, runtime_proxy_groups_order)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use scopeguard::defer;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
fmt,
|
||||
hash::{DefaultHasher, Hash as _, Hasher as _},
|
||||
mem::DropGuard,
|
||||
sync::LazyLock,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
@@ -421,9 +421,9 @@ impl CoreConfigValidator {
|
||||
logging!(info, Type::Validate, "验证已在进行中,跳过新的验证请求");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
let _validation = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
self.finish();
|
||||
});
|
||||
}
|
||||
logging!(info, Type::Validate, "生成临时配置文件用于验证");
|
||||
|
||||
// The test run reads the app dir, which never sees updates made by a service core.
|
||||
@@ -449,7 +449,8 @@ fn has_ext<P: AsRef<std::path::Path>>(path: P, ext: &str) -> bool {
|
||||
path.as_ref()
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.is_some_and(|s| s.eq_ignore_ascii_case(ext))
|
||||
.map(|s| s.eq_ignore_ascii_case(ext))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn contains_any_keyword<'a>(buf: &'a [u8], keywords: &'a [&str]) -> bool {
|
||||
|
||||
@@ -634,7 +634,7 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
|
||||
let proxy_names = config
|
||||
.get("proxies")
|
||||
.and_then(|v| v.as_sequence())
|
||||
.map_or_default(|seq| {
|
||||
.map(|seq| {
|
||||
seq.iter()
|
||||
.filter_map(|item| match item {
|
||||
Value::Mapping(map) => map
|
||||
@@ -645,12 +645,13 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
|
||||
_ => None,
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
});
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let group_names = config
|
||||
.get("proxy-groups")
|
||||
.and_then(|v| v.as_sequence())
|
||||
.map_or_default(|seq| {
|
||||
.map(|seq| {
|
||||
seq.iter()
|
||||
.filter_map(|item| {
|
||||
item.as_mapping()
|
||||
@@ -659,17 +660,19 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
|
||||
.map(std::convert::Into::into)
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
});
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let provider_names = config
|
||||
.get("proxy-providers")
|
||||
.and_then(Value::as_mapping)
|
||||
.map_or_default(|map| {
|
||||
.map(|map| {
|
||||
map.keys()
|
||||
.filter_map(Value::as_str)
|
||||
.map(std::convert::Into::into)
|
||||
.collect::<HashSet<String>>()
|
||||
});
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut allowed_names = proxy_names;
|
||||
allowed_names.extend(group_names);
|
||||
@@ -713,13 +716,15 @@ fn ensure_fake_ip_range6(dns: &mut Mapping) {
|
||||
let is_fake_ip = dns
|
||||
.get("enhanced-mode")
|
||||
.and_then(|v| v.as_str())
|
||||
.is_none_or(|m| m == "fake-ip");
|
||||
.map(|m| m == "fake-ip")
|
||||
.unwrap_or(true);
|
||||
|
||||
// Hand-edited YAML may leave the key present but empty.
|
||||
let range6_missing = dns
|
||||
.get("fake-ip-range6")
|
||||
.and_then(|v| v.as_str())
|
||||
.is_none_or(|s| s.trim().is_empty());
|
||||
.map(|s| s.trim().is_empty())
|
||||
.unwrap_or(true);
|
||||
|
||||
if ipv6_enabled && is_fake_ip && range6_missing {
|
||||
dns.insert(Value::from("fake-ip-range6"), Value::from("2001:2::0/64"));
|
||||
@@ -742,7 +747,8 @@ fn take_mapping(config: &mut Mapping, key: &str) -> Mapping {
|
||||
config
|
||||
.get_mut(key)
|
||||
.and_then(Value::as_mapping_mut)
|
||||
.map_or_default(std::mem::take)
|
||||
.map(std::mem::take)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn merge_dns_config(mut config: Mapping, mut dns_config: Mapping) -> (Mapping, Mapping) {
|
||||
|
||||
@@ -20,10 +20,14 @@ fn collect_proxy_names(seq: &Sequence) -> Vec<String> {
|
||||
}
|
||||
|
||||
fn is_selector_group(group_map: &Mapping) -> bool {
|
||||
group_map.get("type").and_then(Value::as_str).is_some_and(|value| {
|
||||
let value = value.to_ascii_lowercase();
|
||||
value == "select" || value == "selector"
|
||||
})
|
||||
group_map
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.map(|value| {
|
||||
let value = value.to_ascii_lowercase();
|
||||
value == "select" || value == "selector"
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
@@ -151,6 +155,7 @@ mod tests {
|
||||
use serde_yaml_ng::Value;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn test_delete_proxy_and_references() {
|
||||
let config_str = r#"
|
||||
@@ -228,6 +233,7 @@ proxy-groups:
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn test_add_new_proxies_to_first_selector_group() {
|
||||
let config_str = r#"
|
||||
@@ -297,6 +303,7 @@ proxy-groups:
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn test_preserve_non_sequence_proxy_groups() {
|
||||
let config_str = r#"
|
||||
|
||||
@@ -235,7 +235,8 @@ pub async fn import_local_backup(source: String) -> Result<String> {
|
||||
let ext = source_path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.map_or_default(|ext| ext.to_ascii_lowercase());
|
||||
.map(|ext| ext.to_ascii_lowercase())
|
||||
.unwrap_or_default();
|
||||
if ext != "zip" {
|
||||
return Err(anyhow!("Only .zip backup files are supported"));
|
||||
}
|
||||
@@ -313,7 +314,8 @@ pub async fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
|
||||
};
|
||||
let last_modified = metadata
|
||||
.modified()
|
||||
.map_or_default(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339());
|
||||
.map(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339())
|
||||
.unwrap_or_default();
|
||||
backups.push(LocalBackupFile {
|
||||
filename: file_name.into(),
|
||||
path: path.to_string_lossy().into(),
|
||||
|
||||
@@ -487,8 +487,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn package_urls_pin_the_resolved_version() {
|
||||
let release = package_url(false, "v1.19.30").unwrap();
|
||||
let alpha = package_url(true, "alpha-c0e43eb").unwrap();
|
||||
let release = package_url(false, "v1.19.30").unwrap_or_default();
|
||||
let alpha = package_url(true, "alpha-c0e43eb").unwrap_or_default();
|
||||
assert!(release.contains("/releases/download/v1.19.30/"), "{release}");
|
||||
assert!(
|
||||
release.ends_with("-v1.19.30.gz") || release.ends_with("-v1.19.30.zip"),
|
||||
|
||||
@@ -44,7 +44,7 @@ fn ensure_icon_cache_target(icon_cache_dir: &Path, file_name: &str) -> CmdResult
|
||||
}
|
||||
|
||||
fn normalized_text_prefix(content: &[u8]) -> std::string::String {
|
||||
let content = content.trim_prefix(&[0xEF, 0xBB, 0xBF]);
|
||||
let content = content.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(content);
|
||||
let start = content
|
||||
.iter()
|
||||
.position(|byte| !byte.is_ascii_whitespace())
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::{
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use clash_verge_draft::DraftTransaction;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use std::mem::DropGuard;
|
||||
use scopeguard::{ScopeGuard, defer, guard};
|
||||
|
||||
pub async fn probe_listener(request: ListenerProbe) -> Result<ListenerProbeOutcome> {
|
||||
AsyncHandler::spawn_blocking(move || probe_listener_sync(&request))
|
||||
@@ -35,9 +35,9 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
|
||||
if !manager.try_start_config_update() {
|
||||
bail!("configuration update is already running");
|
||||
}
|
||||
let _config_update = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
manager.finish_config_update();
|
||||
});
|
||||
}
|
||||
|
||||
// Read first because transaction rollback discards all claimed drafts.
|
||||
let current = current_runtime_mapping().await?;
|
||||
@@ -52,13 +52,13 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
|
||||
// Hide PAC while the staged ports are not yet serving traffic.
|
||||
manager.core_starting();
|
||||
// Recompute PAC state after every exit path.
|
||||
let _core_start = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
manager.core_start_settled();
|
||||
});
|
||||
}
|
||||
// A user-picked port ends the startup fallback, but only once the save lands: until then the
|
||||
// Core still serves the borrowed port, so every failing exit must put it back or the system
|
||||
// proxy and PAC point at a dead port. Declared after the PAC guard so it restores first.
|
||||
let borrowed_port = DropGuard::new(MixedPort::session_fallback(), restore_borrowed_port);
|
||||
let borrowed_port = guard(MixedPort::session_fallback(), restore_borrowed_port);
|
||||
MixedPort::clear_session_fallback();
|
||||
Config::generate()
|
||||
.await
|
||||
@@ -125,7 +125,7 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
|
||||
|
||||
transaction.commit();
|
||||
// The save landed, so the port the app had borrowed is now irrelevant.
|
||||
let _ = DropGuard::dismiss(borrowed_port);
|
||||
let _ = ScopeGuard::into_inner(borrowed_port);
|
||||
announce(Refresh::Clash);
|
||||
announce(Refresh::Verge);
|
||||
logging!(info, Type::Config, "Proxy port configuration applied and persisted");
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
use crate::core::notify::NoticeStatus;
|
||||
|
||||
use std::{
|
||||
mem::DropGuard,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use scopeguard::defer;
|
||||
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
@@ -63,9 +61,9 @@ pub async fn reconcile_tun_availability() {
|
||||
|
||||
// The flag coalesces newly spawned tasks while the config lock serializes writers.
|
||||
DISABLING_TUN.store(true, Ordering::Release);
|
||||
let _disabling_tun = DropGuard::new((), |()| {
|
||||
defer! {
|
||||
DISABLING_TUN.store(false, Ordering::Release);
|
||||
});
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
|
||||
@@ -15,7 +15,7 @@ fn main() -> ExitCode {
|
||||
|
||||
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
|
||||
fn main() -> ExitCode {
|
||||
let default_parallelism = std::thread::available_parallelism().map_or(1, |n| n.get());
|
||||
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
let worker_limit = std::cmp::min(default_parallelism, 4);
|
||||
let blocking_limit = 2 * worker_limit;
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ use crate::{config::with_encryption, process::AsyncHandler};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use nanoid::nanoid;
|
||||
use scopeguard::{ScopeGuard, guard};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use std::{
|
||||
mem::DropGuard,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
@@ -168,7 +168,7 @@ pub async fn save_yaml_str(path: &Path, yaml_str: &str) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
let temporary = DropGuard::new(temporary, |path| {
|
||||
let temporary = guard(temporary, |path| {
|
||||
let _ = std::fs::remove_file(path);
|
||||
});
|
||||
let mut file = tokio::fs::File::from_std(file);
|
||||
@@ -195,7 +195,7 @@ pub async fn save_yaml_str(path: &Path, yaml_str: &str) -> Result<()> {
|
||||
|
||||
std::fs::rename(temporary.as_path(), path)
|
||||
.with_context(|| format!("failed to replace file \"{}\"", path.display()))?;
|
||||
let _ = DropGuard::dismiss(temporary);
|
||||
let _ = ScopeGuard::into_inner(temporary);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -234,15 +234,15 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
pub fn is_main_window_visible(window: Option<&WebviewWindow<Wry>>) -> bool {
|
||||
window.is_some_and(|w| w.is_visible().unwrap_or(false))
|
||||
window.map(|w| w.is_visible().unwrap_or(false)).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_main_window_focused(window: Option<&WebviewWindow<Wry>>) -> bool {
|
||||
window.is_some_and(|w| w.is_focused().unwrap_or(false))
|
||||
window.map(|w| w.is_focused().unwrap_or(false)).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_main_window_minimized(window: Option<&WebviewWindow<Wry>>) -> bool {
|
||||
window.is_some_and(|w| w.is_minimized().unwrap_or(false))
|
||||
window.map(|w| w.is_minimized().unwrap_or(false)).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Keep new windows hidden until the frontend overlay renders, avoiding a theme flash.
|
||||
|
||||
@@ -605,12 +605,14 @@ export const CurrentProxyCard = () => {
|
||||
[getProfileStorageKey],
|
||||
)
|
||||
|
||||
const { select } = useProxySelection({
|
||||
onError: (error) => {
|
||||
const onSelectionError = useCallback(
|
||||
(error: unknown) => {
|
||||
console.error('代理切换失败', error)
|
||||
refreshProxy()
|
||||
},
|
||||
})
|
||||
[refreshProxy],
|
||||
)
|
||||
const { select } = useProxySelection({ onError: onSelectionError })
|
||||
|
||||
const mode = clashConfig?.mode?.toLowerCase() || 'rule'
|
||||
const isGlobalMode = mode === 'global'
|
||||
@@ -673,7 +675,6 @@ export const CurrentProxyCard = () => {
|
||||
if (proxyView && lastGroupName !== selectedGroupName) {
|
||||
setLastGroupName(selectedGroupName)
|
||||
}
|
||||
const delays = useGroupDelays(selectedGroupName || null)
|
||||
|
||||
const selectedGroup = useMemo<ProxyGroupView | null>(() => {
|
||||
if (!proxyView || isDirectMode) return null
|
||||
@@ -767,10 +768,17 @@ export const CurrentProxyCard = () => {
|
||||
const currentProxy = currentMember ? memberDetails(currentMember) : undefined
|
||||
const selectedProxyName = currentMember?.ref.name ?? ''
|
||||
|
||||
const delays = useGroupDelays(
|
||||
selectedGroupName,
|
||||
currentMember,
|
||||
openSelect === 'proxy',
|
||||
)
|
||||
const currentDelay =
|
||||
currentMember && selectedGroupName
|
||||
? delayManager.getDelayFix(currentMember, selectedGroupName)
|
||||
: -1
|
||||
typeof delays === 'number'
|
||||
? delays
|
||||
: currentMember
|
||||
? delays.of(currentMember)
|
||||
: -1
|
||||
|
||||
const signalInfo =
|
||||
currentProxy && selectedGroupName
|
||||
@@ -892,7 +900,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
const proxyOptions = useMemo(
|
||||
() =>
|
||||
isDirectMode || openSelect !== 'proxy'
|
||||
isDirectMode || openSelect !== 'proxy' || typeof delays === 'number'
|
||||
? []
|
||||
: sortProxyOptions(
|
||||
unsortedProxyOptions,
|
||||
@@ -1008,13 +1016,8 @@ export const CurrentProxyCard = () => {
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
noWrap
|
||||
title={currentProxy?.name}
|
||||
sx={{ fontWeight: 'medium' }}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 'medium' }}>
|
||||
{currentProxy?.name ??
|
||||
t('home.components.currentProxy.labels.noActiveNode')}
|
||||
</Typography>
|
||||
@@ -1068,7 +1071,6 @@ export const CurrentProxyCard = () => {
|
||||
size="small"
|
||||
label={delayManager.formatDelay(currentDelay)}
|
||||
color={convertDelayColor(currentDelay)}
|
||||
sx={{ flexShrink: 0, ml: 1 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1106,9 +1106,10 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
|
||||
{tooltipData.visible && (
|
||||
<Box
|
||||
style={{ left: tooltipData.x + 8, top: tooltipData.y - 8 }}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: tooltipData.x + 8,
|
||||
top: tooltipData.y - 8,
|
||||
bgcolor: theme.palette.background.paper,
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
|
||||
@@ -469,12 +469,14 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
}
|
||||
}, [handleScroll])
|
||||
|
||||
const { select } = useProxySelection({
|
||||
onError: (error) => {
|
||||
const onSelectionError = useCallback(
|
||||
(error: unknown) => {
|
||||
console.error('代理切换失败', error)
|
||||
onProxies()
|
||||
},
|
||||
})
|
||||
[onProxies],
|
||||
)
|
||||
const { select } = useProxySelection({ onError: onSelectionError })
|
||||
|
||||
const handleChangeProxy = useCallback(
|
||||
(group: ProxyGroupView, member: ResolvedProxyMember) => {
|
||||
|
||||
@@ -29,6 +29,9 @@ export function filterSort(
|
||||
const regex1 = /delay([=<>])(\d+|timeout|error)/i
|
||||
const regex2 = /type=(.*)/i
|
||||
|
||||
export const isDelayFilter = (filterText: string) =>
|
||||
regex1.test(filterText.trim())
|
||||
|
||||
function filterProxies(
|
||||
proxies: ResolvedMemberOccurrence[],
|
||||
groupName: string,
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
|
||||
import type { DelaySnapshot } from '@/services/delay'
|
||||
import { resolveMember, type ProxyViewV1 } from '@/types/proxy-view'
|
||||
|
||||
import { filterSort } from './use-filter-sort'
|
||||
import { useRenderList as readRenderList } from './use-render-list'
|
||||
|
||||
const hooks = vi.hoisted(() => {
|
||||
const slots: { value: unknown; deps?: unknown[] }[] = []
|
||||
let cursor = 0
|
||||
return {
|
||||
reset: () => {
|
||||
cursor = 0
|
||||
},
|
||||
memo: (factory: () => unknown, deps: unknown[]) => {
|
||||
const index = cursor++
|
||||
const old = slots[index]
|
||||
if (!old || deps.some((dep, i) => !Object.is(dep, old.deps?.[i]))) {
|
||||
slots[index] = { value: factory(), deps }
|
||||
}
|
||||
return slots[index].value
|
||||
},
|
||||
ref: (current: unknown) => {
|
||||
const index = cursor++
|
||||
slots[index] ??= { value: { current } }
|
||||
return slots[index].value
|
||||
},
|
||||
}
|
||||
})
|
||||
const state = vi.hoisted(() => ({
|
||||
view: null as ProxyViewV1 | null,
|
||||
delays: new Map<string, DelaySnapshot>(),
|
||||
}))
|
||||
|
||||
vi.mock('react', () => ({
|
||||
useMemo: hooks.memo,
|
||||
useRef: hooks.ref,
|
||||
useEffect: () => {},
|
||||
}))
|
||||
vi.mock('@/hooks/use-profiles', () => ({}))
|
||||
vi.mock('@/hooks/use-clash', () => ({
|
||||
useRuntimeConfig: () => ({ data: null }),
|
||||
}))
|
||||
vi.mock('@/hooks/use-verge', () => ({
|
||||
useVerge: () => ({ verge: { proxy_layout_column: 1 } }),
|
||||
}))
|
||||
vi.mock('@/providers/app-data-context', () => ({
|
||||
useProxiesData: () => ({ proxyView: state.view }),
|
||||
useAppRefreshers: () => ({ refreshProxy: () => {} }),
|
||||
}))
|
||||
vi.mock('@/hooks/use-group-delays', () => ({
|
||||
useGroupsDelays: (names: string[]) =>
|
||||
new Map(names.map((name) => [name, state.delays.get(name)])),
|
||||
}))
|
||||
vi.mock('./use-window-width', () => ({
|
||||
useWindowWidth: () => ({ width: 800 }),
|
||||
}))
|
||||
vi.mock('./use-head-state', async (original) => {
|
||||
const actual = await original<typeof import('./use-head-state')>()
|
||||
const heads = {
|
||||
plain: { ...actual.DEFAULT_STATE, open: true },
|
||||
sorted: { ...actual.DEFAULT_STATE, open: true, sortType: 1 },
|
||||
}
|
||||
return { ...actual, useHeadStateNew: () => [heads, () => {}] }
|
||||
})
|
||||
vi.mock('./use-filter-sort', async (original) => {
|
||||
const actual = await original<typeof import('./use-filter-sort')>()
|
||||
return { ...actual, filterSort: vi.fn(actual.filterSort) }
|
||||
})
|
||||
vi.mock('@/types/proxy-view', async (original) => {
|
||||
const actual = await original<typeof import('@/types/proxy-view')>()
|
||||
return { ...actual, resolveMember: vi.fn(actual.resolveMember) }
|
||||
})
|
||||
|
||||
it('skips delay-independent projections and reuses members for delay sorting', () => {
|
||||
const group = (name: string) => ({
|
||||
name,
|
||||
type: 'Selector',
|
||||
history: [],
|
||||
udp: false,
|
||||
xudp: false,
|
||||
tfo: false,
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
members: Array.from({ length: 500 }, (_, i) => ({
|
||||
kind: 'unresolved' as const,
|
||||
name: `${i}`,
|
||||
reason: 'missing' as const,
|
||||
})),
|
||||
})
|
||||
state.view = {
|
||||
schemaVersion: 1,
|
||||
orderSource: 'runtime',
|
||||
providerState: 'ready',
|
||||
global: null,
|
||||
direct: null,
|
||||
groups: [group('plain'), group('sorted')],
|
||||
records: {},
|
||||
standalone: [],
|
||||
providers: [],
|
||||
}
|
||||
const render = () => {
|
||||
hooks.reset()
|
||||
return readRenderList('rule')
|
||||
}
|
||||
state.delays.set('plain', { of: () => 42 })
|
||||
state.delays.set('sorted', { of: () => 42 })
|
||||
render()
|
||||
vi.mocked(filterSort).mockClear()
|
||||
vi.mocked(resolveMember).mockClear()
|
||||
state.delays.set('plain', { of: () => 43 })
|
||||
render()
|
||||
expect(filterSort).toHaveBeenCalledTimes(0)
|
||||
state.delays.set('sorted', { of: () => 43 })
|
||||
render()
|
||||
expect(filterSort).toHaveBeenCalledTimes(1)
|
||||
expect(resolveMember).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '@/types/proxy-view'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
|
||||
import { filterSort } from './use-filter-sort'
|
||||
import { filterSort, isDelayFilter } from './use-filter-sort'
|
||||
import {
|
||||
DEFAULT_STATE,
|
||||
useHeadStateNew,
|
||||
@@ -60,14 +60,14 @@ export const hasRenderableItems = (
|
||||
): boolean => renderList.some((item) => item.type !== 0 || !item.group.hidden)
|
||||
|
||||
type GroupCache = {
|
||||
now: string | undefined
|
||||
members: ProxyGroupView['members']
|
||||
group: ProxyGroupView
|
||||
headState: HeadState
|
||||
col: number
|
||||
latencyTimeout: number | undefined
|
||||
/// This group's own delays. Compared by identity so that a test settling in one group
|
||||
/// does not throw away every other group's sorted order.
|
||||
delays: DelaySnapshot | undefined
|
||||
occurrences: ResolvedMemberOccurrence[] | undefined
|
||||
items: IRenderItem[]
|
||||
}
|
||||
|
||||
@@ -222,19 +222,51 @@ export const useRenderList = (
|
||||
return current?.kind === 'group' ? [global, current.group] : [global]
|
||||
}, [mode, proxyView])
|
||||
|
||||
// Every group this list draws, so a test settling in any of them re-sorts that group.
|
||||
const repeatedNameGroups = useMemo(() => {
|
||||
const repeated = new Set<string>()
|
||||
if (isChainMode) return repeated
|
||||
for (const group of renderGroups) {
|
||||
const names = new Set<string>()
|
||||
if (
|
||||
group.members.some(({ name }) => {
|
||||
if (names.has(name)) return true
|
||||
names.add(name)
|
||||
return false
|
||||
})
|
||||
) {
|
||||
repeated.add(group.name)
|
||||
}
|
||||
}
|
||||
return repeated
|
||||
}, [isChainMode, renderGroups])
|
||||
|
||||
// Rows own delay updates unless repeated names share the current item listener.
|
||||
const renderedGroupNames = useMemo(() => {
|
||||
if (!proxyView) return []
|
||||
if (isChainMode)
|
||||
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
|
||||
return renderGroups.map(({ name }) => name)
|
||||
}, [isChainMode, proxyView, renderGroups, selectedGroup])
|
||||
if (!proxyView || isChainMode) return []
|
||||
return renderGroups
|
||||
.filter((group) => {
|
||||
const state = headStates[group.name] || DEFAULT_STATE
|
||||
return (
|
||||
(state.open || group === proxyView.global) &&
|
||||
(state.sortType === 1 ||
|
||||
isDelayFilter(state.filterText) ||
|
||||
repeatedNameGroups.has(group.name))
|
||||
)
|
||||
})
|
||||
.map(({ name }) => name)
|
||||
}, [headStates, isChainMode, proxyView, renderGroups, repeatedNameGroups])
|
||||
const groupDelays = useGroupsDelays(renderedGroupNames)
|
||||
|
||||
const groupCacheRef = useRef<Map<string, GroupCache>>(new Map())
|
||||
const groupCacheRef = useRef<Map<string, GroupCache> | null>(null)
|
||||
const cachedViewRef = useRef(proxyView)
|
||||
const prevListRef = useRef<IRenderItem[]>([])
|
||||
|
||||
const renderList = useMemo<IRenderItem[]>(() => {
|
||||
const cache = (groupCacheRef.current ??= new Map())
|
||||
if (cachedViewRef.current !== proxyView) {
|
||||
cache.clear()
|
||||
cachedViewRef.current = proxyView
|
||||
}
|
||||
if (!proxyView) return []
|
||||
|
||||
if (isChainMode) {
|
||||
@@ -269,16 +301,13 @@ export const useRenderList = (
|
||||
}))
|
||||
}
|
||||
|
||||
const cache = groupCacheRef.current
|
||||
|
||||
const retList = renderGroups.flatMap((group) => {
|
||||
const isFlat = group === proxyView.global
|
||||
const headState = headStates[group.name] || DEFAULT_STATE
|
||||
const cached = cache.get(group.name)
|
||||
if (
|
||||
cached &&
|
||||
cached.now === group.now &&
|
||||
cached.members === group.members &&
|
||||
cached.group === group &&
|
||||
cached.headState === headState &&
|
||||
cached.col === col &&
|
||||
cached.latencyTimeout === latencyTimeout &&
|
||||
@@ -300,9 +329,18 @@ export const useRenderList = (
|
||||
},
|
||||
]
|
||||
|
||||
// Duplicate names share one item listener until the delay-store fix lands.
|
||||
const refreshRepeated =
|
||||
repeatedNameGroups.has(group.name) &&
|
||||
cached?.delays !== groupDelays.get(group.name)
|
||||
let resolved =
|
||||
cached?.group === group && !refreshRepeated
|
||||
? cached.occurrences
|
||||
: undefined
|
||||
if (headState.open || isFlat) {
|
||||
resolved ??= resolveOccurrences(proxyView, group)
|
||||
const occurrences = filterSort(
|
||||
resolveOccurrences(proxyView, group),
|
||||
resolved,
|
||||
group.name,
|
||||
headState.filterText,
|
||||
headState.sortType,
|
||||
@@ -337,15 +375,17 @@ export const useRenderList = (
|
||||
})),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
resolved = undefined
|
||||
}
|
||||
|
||||
cache.set(group.name, {
|
||||
now: group.now!,
|
||||
members: group.members,
|
||||
group,
|
||||
headState,
|
||||
col,
|
||||
latencyTimeout: latencyTimeout!,
|
||||
delays: groupDelays.get(group.name),
|
||||
occurrences: resolved,
|
||||
items: ret,
|
||||
})
|
||||
return ret
|
||||
@@ -371,6 +411,7 @@ export const useRenderList = (
|
||||
mode,
|
||||
proxyView,
|
||||
renderGroups,
|
||||
repeatedNameGroups,
|
||||
selectedGroup,
|
||||
])
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
|
||||
import delayManager from '@/services/delay'
|
||||
import type { ResolvedProxyMember } from '@/types/proxy-view'
|
||||
|
||||
import { useGroupDelays } from './use-group-delays'
|
||||
|
||||
vi.mock('react', () => ({
|
||||
useCallback: (callback: unknown) => callback,
|
||||
useSyncExternalStore: vi.fn((_subscribe, read) => read()),
|
||||
}))
|
||||
|
||||
it('keeps the closed-menu snapshot unchanged when another member settles', () => {
|
||||
const member: ResolvedProxyMember = {
|
||||
kind: 'node',
|
||||
ref: { kind: 'node', name: 'current', recordId: 'current' },
|
||||
node: {
|
||||
recordId: 'current',
|
||||
name: 'current',
|
||||
type: 'Shadowsocks',
|
||||
source: { kind: 'core', proxyName: 'current' },
|
||||
history: [],
|
||||
udp: false,
|
||||
xudp: false,
|
||||
tfo: false,
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
},
|
||||
}
|
||||
vi.spyOn(delayManager, 'getDelayFix').mockReturnValue(42)
|
||||
const snapshot = vi.spyOn(delayManager, 'groupDelays')
|
||||
snapshot.mockReturnValue({ of: () => 42 })
|
||||
useGroupDelays('group', member, false)
|
||||
const [, read] = vi.mocked(useSyncExternalStore).mock.calls.at(-1)!
|
||||
let previous = read()
|
||||
let changes = 0
|
||||
for (let i = 0; i < 120; i++) {
|
||||
snapshot.mockReturnValue({ of: () => 42 })
|
||||
const next = read()
|
||||
if (!Object.is(next, previous)) changes++
|
||||
previous = next
|
||||
}
|
||||
expect(changes).toBe(0)
|
||||
})
|
||||
@@ -1,19 +1,27 @@
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
|
||||
import delayManager, { type DelaySnapshot } from '@/services/delay'
|
||||
import type { ResolvedProxyMember } from '@/types/proxy-view'
|
||||
|
||||
const NO_DELAYS: DelaySnapshot = { of: () => -1 }
|
||||
|
||||
/** Exposes the external delay store to React, updating sort order only after a test settles. */
|
||||
export const useGroupDelays = (group: string | null): DelaySnapshot => {
|
||||
/** A closed menu observes only its current member, not every settled group delay. */
|
||||
export const useGroupDelays = (
|
||||
group: string | null,
|
||||
member: ResolvedProxyMember | undefined,
|
||||
menuOpen: boolean,
|
||||
): DelaySnapshot | number => {
|
||||
const subscribe = useCallback(
|
||||
(onSettle: () => void) =>
|
||||
group ? delayManager.addGroupListener(group, onSettle) : () => {},
|
||||
[group],
|
||||
)
|
||||
const read = useCallback(
|
||||
() => (group ? delayManager.groupDelays(group) : NO_DELAYS),
|
||||
[group],
|
||||
() =>
|
||||
group && menuOpen
|
||||
? delayManager.groupDelays(group)
|
||||
: group && member
|
||||
? delayManager.getDelayFix(member, group)
|
||||
: -1,
|
||||
[group, member, menuOpen],
|
||||
)
|
||||
|
||||
return useSyncExternalStore(subscribe, read, read)
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useEffect, useMemo } from 'react'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { defaultDarkTheme, defaultTheme } from '@/pages/_theme'
|
||||
import { useSetThemeMode, useThemeMode } from '@/services/states'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
const CSS_INJECTION_SCOPE_ROOT = '[data-css-injection-root]'
|
||||
const CSS_INJECTION_SCOPE_LIMIT =
|
||||
@@ -140,12 +139,6 @@ export const useCustomTheme = () => {
|
||||
console.error(`Failed to set window theme to ${mode}:`, err)
|
||||
})
|
||||
}
|
||||
if (getSystem() === 'macos' && mode) {
|
||||
const background = mode === 'light' ? defaultTheme : defaultDarkTheme
|
||||
appWindow.setBackgroundColor(background.background_color).catch((err) => {
|
||||
console.error('Failed to set window background:', err)
|
||||
})
|
||||
}
|
||||
}, [mode, appWindow, theme_mode])
|
||||
|
||||
const theme = useMemo(() => {
|
||||
|
||||
@@ -71,10 +71,6 @@ const ORDER_OPTIONS = [
|
||||
|
||||
type OrderKey = (typeof ORDER_OPTIONS)[number]['id']
|
||||
|
||||
type NetworkFilter = 'all' | 'tcp' | 'udp'
|
||||
|
||||
let savedNetworkFilter: NetworkFilter = 'all'
|
||||
|
||||
const orderFunctionMap = ORDER_OPTIONS.reduce<Record<OrderKey, OrderFunc>>(
|
||||
(acc, option) => {
|
||||
acc[option.id] = option.fn
|
||||
@@ -90,9 +86,6 @@ const ConnectionsPage = () => {
|
||||
const { searchState, match, onSearch } = usePageSearch('connections')
|
||||
const hasSearch = searchState.text.length > 0
|
||||
const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>('default')
|
||||
const [networkFilter, setNetworkFilter] =
|
||||
useState<NetworkFilter>(savedNetworkFilter)
|
||||
const hasFilter = networkFilter !== 'all'
|
||||
const [connectionsType, setConnectionsType] = useState<'active' | 'closed'>(
|
||||
'active',
|
||||
)
|
||||
@@ -119,30 +112,18 @@ const ConnectionsPage = () => {
|
||||
const filterConn = useMemo(() => {
|
||||
const orderFunc = orderFunctionMap[curOrderOpt]
|
||||
|
||||
if (isTableLayout && !hasSearch && !hasFilter) return selectedConnections
|
||||
if (!hasSearch && !hasFilter) return orderFunc([...selectedConnections])
|
||||
if (isTableLayout && !hasSearch) return selectedConnections
|
||||
if (!hasSearch) return orderFunc([...selectedConnections])
|
||||
|
||||
const matchConns = selectedConnections.filter((conn) => {
|
||||
const { host, destinationIP, process, network } = conn.metadata
|
||||
if (networkFilter !== 'all' && network !== networkFilter) return false
|
||||
const { host, destinationIP, process } = conn.metadata
|
||||
return (
|
||||
!hasSearch ||
|
||||
match(host || '') ||
|
||||
match(destinationIP || '') ||
|
||||
match(process || '')
|
||||
match(host || '') || match(destinationIP || '') || match(process || '')
|
||||
)
|
||||
})
|
||||
|
||||
return orderFunc ? orderFunc(matchConns) : matchConns
|
||||
}, [
|
||||
selectedConnections,
|
||||
isTableLayout,
|
||||
hasSearch,
|
||||
hasFilter,
|
||||
networkFilter,
|
||||
match,
|
||||
curOrderOpt,
|
||||
])
|
||||
}, [selectedConnections, isTableLayout, hasSearch, match, curOrderOpt])
|
||||
|
||||
const displayRows = useConnectionRowViews(
|
||||
isTableLayout ? EMPTY_CONNECTIONS : filterConn,
|
||||
@@ -274,24 +255,6 @@ const ConnectionsPage = () => {
|
||||
))}
|
||||
</BaseStyledSelect>
|
||||
)}
|
||||
<BaseStyledSelect
|
||||
style={{ width: 80 }}
|
||||
value={networkFilter}
|
||||
onChange={(e) => {
|
||||
savedNetworkFilter = e.target.value as NetworkFilter
|
||||
setNetworkFilter(savedNetworkFilter)
|
||||
}}
|
||||
>
|
||||
<MenuItem value="all">
|
||||
<span style={{ fontSize: 14 }}>All</span>
|
||||
</MenuItem>
|
||||
<MenuItem value="tcp">
|
||||
<span style={{ fontSize: 14 }}>TCP</span>
|
||||
</MenuItem>
|
||||
<MenuItem value="udp">
|
||||
<span style={{ fontSize: 14 }}>UDP</span>
|
||||
</MenuItem>
|
||||
</BaseStyledSelect>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
|
||||
+11
-8
@@ -39,18 +39,21 @@ const LogPage = () => {
|
||||
return []
|
||||
}
|
||||
|
||||
const hasSearch = searchState.text.length > 0
|
||||
if (!hasSearch && logState === 'all') return logData
|
||||
|
||||
// Server-side filtering handles level filtering via query parameters
|
||||
// We only need to apply search filtering here
|
||||
return logData.filter((data) => {
|
||||
if (logState !== 'all' && !data.type.includes(logState)) return false
|
||||
if (!hasSearch) return true
|
||||
|
||||
// 构建完整的搜索文本,包含时间、类型和内容
|
||||
const searchText =
|
||||
`${data.time || ''} ${data.type} ${data.payload}`.toLowerCase()
|
||||
return match(searchText)
|
||||
|
||||
const matchesSearch = match(searchText)
|
||||
|
||||
return (
|
||||
(logState == 'all' ? true : data.type.includes(logState)) &&
|
||||
matchesSearch
|
||||
)
|
||||
})
|
||||
}, [logData, logState, match, searchState.text])
|
||||
}, [logData, logState, match])
|
||||
|
||||
const filteredLogs = useMemo(
|
||||
() => (isDescending ? [...filterLogs].reverse() : filterLogs),
|
||||
|
||||
Reference in New Issue
Block a user