mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 02:32:09 +08:00
Compare commits
18
Commits
83a06e5716
..
v2.5.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13676d3335 | ||
|
|
2a7972fa5b | ||
|
|
19334368cd | ||
|
|
73ea0fbd80 | ||
|
|
9f97d938f5 | ||
|
|
04a2a09368 | ||
|
|
94491b4ba4 | ||
|
|
78516dc1dc | ||
|
|
8f6fd3bbf9 | ||
|
|
c599f5547e | ||
|
|
f6fdb34745 | ||
|
|
65ed8d3cf2 | ||
|
|
2baa629ac4 | ||
|
|
c7ffb21294 | ||
|
|
0d0ed72467 | ||
|
|
a36dcd319f | ||
|
|
9a879dd8d8 | ||
|
|
20141e791e |
@@ -5,7 +5,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "24.21.0",
|
||||
"pnpmVersion": "12.3.3"
|
||||
"pnpmVersion": "12.5.1"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.98.1",
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "v9.0.0",
|
||||
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
|
||||
},
|
||||
"github/gh-aw-actions/setup@v0.89.1": {
|
||||
"github/gh-aw-actions/setup@v0.89.15": {
|
||||
"repo": "github/gh-aw-actions/setup",
|
||||
"version": "v0.89.1",
|
||||
"sha": "4537e5924c9abb366dcbece06750e498e0218fae"
|
||||
"version": "v0.89.15",
|
||||
"sha": "045beb2d14bda8d0c1f2e83b41527f63770d2855"
|
||||
}
|
||||
},
|
||||
"containers": {
|
||||
@@ -22,6 +22,11 @@
|
||||
"digest": "sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98",
|
||||
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.28.14@sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98"
|
||||
},
|
||||
"ghcr.io/github/gh-aw-firewall/agent:0.28.16": {
|
||||
"image": "ghcr.io/github/gh-aw-firewall/agent:0.28.16",
|
||||
"digest": "sha256:57a3e27388a6d7d32719088581e52567727fa0bf2f0d477bf565b0c4baa12a3f",
|
||||
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.28.16@sha256:57a3e27388a6d7d32719088581e52567727fa0bf2f0d477bf565b0c4baa12a3f"
|
||||
},
|
||||
"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.13": {
|
||||
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.13",
|
||||
"digest": "sha256:61f12dcff38eb008b645d87a5541763452f409d9040eb243718764944167b1a2",
|
||||
@@ -32,6 +37,11 @@
|
||||
"digest": "sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5",
|
||||
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14@sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5"
|
||||
},
|
||||
"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.16": {
|
||||
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.16",
|
||||
"digest": "sha256:cd400948638ffe1b87ec319abf73fa29b6ac9881b015da58bba971c4cc13a400",
|
||||
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.16@sha256:cd400948638ffe1b87ec319abf73fa29b6ac9881b015da58bba971c4cc13a400"
|
||||
},
|
||||
"ghcr.io/github/gh-aw-firewall/squid:0.28.13": {
|
||||
"image": "ghcr.io/github/gh-aw-firewall/squid:0.28.13",
|
||||
"digest": "sha256:58a8dc5be3fbafeef7da0532c38bd9047f86bf1b3c4cfd7d9b7d8d28e80a2b56",
|
||||
@@ -42,10 +52,15 @@
|
||||
"digest": "sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5",
|
||||
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.14@sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5"
|
||||
},
|
||||
"ghcr.io/github/gh-aw-mcpg:v0.4.20": {
|
||||
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.20",
|
||||
"digest": "sha256:980ea7aa4fb07e444f0e2c6e3af5aff8b45e4e415d617667b8ba064a1768b684",
|
||||
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.20@sha256:980ea7aa4fb07e444f0e2c6e3af5aff8b45e4e415d617667b8ba064a1768b684"
|
||||
"ghcr.io/github/gh-aw-firewall/squid:0.28.16": {
|
||||
"image": "ghcr.io/github/gh-aw-firewall/squid:0.28.16",
|
||||
"digest": "sha256:452197f2e241b2cda8eb0b5674960aa8ca544a64dc242e24f8263c3df6452919",
|
||||
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.16@sha256:452197f2e241b2cda8eb0b5674960aa8ca544a64dc242e24f8263c3df6452919"
|
||||
},
|
||||
"ghcr.io/github/gh-aw-mcpg:v0.4.21": {
|
||||
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.21",
|
||||
"digest": "sha256:f26f665840660581510746ed84f5fc706f4ca8c48833757dfb4d72d86587201e",
|
||||
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.21@sha256:f26f665840660581510746ed84f5fc706f4ca8c48833757dfb4d72d86587201e"
|
||||
},
|
||||
"ghcr.io/github/gh-aw-node": {
|
||||
"image": "ghcr.io/github/gh-aw-node",
|
||||
|
||||
@@ -3,8 +3,8 @@ name: Auto Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
|
||||
- cron: '0 4,10 * * *'
|
||||
# UTC+8 06:00, 18:00 -> UTC 22:00, 10:00
|
||||
- cron: '0 10,22 * * *'
|
||||
permissions: write-all
|
||||
env:
|
||||
TAG_NAME: autobuild
|
||||
|
||||
@@ -33,4 +33,4 @@ jobs:
|
||||
- name: Install gh-aw extension
|
||||
uses: github/gh-aw-actions/setup-cli@045beb2d14bda8d0c1f2e83b41527f63770d2855 # v0.89.15
|
||||
with:
|
||||
version: v0.89.1
|
||||
version: v0.89.15
|
||||
|
||||
+111
-108
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,3 +1,3 @@
|
||||
nodejs 24.21.0
|
||||
pnpm 12.4.1
|
||||
pnpm 12.5.1
|
||||
rust 1.98.1
|
||||
|
||||
Generated
+440
-264
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -46,6 +46,12 @@ inherits = "fast-release"
|
||||
codegen-units = 1
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
[patch.crates-io]
|
||||
# macOS Golden Gate fix for tray-icon click reentrancy issue
|
||||
# Remove the following patch once the fix is merged upstream(tauri)
|
||||
# https://github.com/tauri-apps/tray-icon/pull/365
|
||||
tray-icon = { git = "https://github.com/Tunglies/tray-icon.git", branch = "fix/macos-27-click-reentrancy" }
|
||||
|
||||
[workspace.dependencies]
|
||||
clash-verge-draft = { path = "crates/clash-verge-draft" }
|
||||
clash-verge-media-unlock = { path = "crates/clash-verge-media-unlock" }
|
||||
@@ -57,9 +63,8 @@ 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 = "3.0.0-alpha.1" }
|
||||
tauri-runtime-wry = "3.0.0-alpha.1"
|
||||
tauri-plugin-clipboard-manager = "3.0.0-alpha.0"
|
||||
tauri = { version = "2.11.5" }
|
||||
tauri-plugin-clipboard-manager = "2.3.3"
|
||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||
anyhow = "1.0.104"
|
||||
chrono = "0.4.45"
|
||||
|
||||
+6
-3
@@ -1,11 +1,9 @@
|
||||
## v2.5.4
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 服务模式下,配置目录中的规则集、代理集合文件只是缓存副本,手动修改不会影响正在运行的内核。需要手动维护的文件请使用 `type: file`。
|
||||
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复首页代理组和节点下拉菜单:再次点击无法收起或误选、小窗口显示不全、切换模式后意外展开
|
||||
- 修复系统代理切换失败后,开关状态显示错误的问题
|
||||
- 修复代理守卫失败后反复重试且没有提示的问题
|
||||
- 修复未开启系统代理时,启动仍提示代理错误的问题
|
||||
@@ -27,10 +25,12 @@
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- 修复 Windows 管理员运行时,服务权限校验失败后无法继续使用代理的问题
|
||||
- 修复 Windows 便携版或 Scoop 安装后服务内核无法启动的问题
|
||||
- 修复 Windows 系统代理设置偶发失败的问题
|
||||
- 修复 Windows 关机被应用阻止的问题
|
||||
- 修复 Windows 服务模式下 TUN 使用 system/mixed 堆栈时无法接管流量的问题,防火墙放行规则改为自动添加
|
||||
- 修复 Windows 配置中 GEO 规则较多时,首次启动内核失败的问题
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
**🐧 Linux**
|
||||
|
||||
- 修复 Linux 安装或卸载服务时授权失败的问题
|
||||
- 修复 Fedora 等启用 SELinux 的系统更新后服务无法启动的问题
|
||||
|
||||
**🍎/🐧 macOS/Linux**
|
||||
|
||||
@@ -78,7 +79,9 @@
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 优化首页节点菜单关闭时的刷新性能
|
||||
- 优化 TUN 配置优先级:界面设置优先,其他选项仍可通过订阅、覆写和脚本调整
|
||||
- 优化 DNS 覆写:仅覆盖已填写的字段和已开启的开关,留空或关闭的项保留订阅设置
|
||||
- 优化系统代理失败后的恢复流程,保持系统设置和界面状态一致
|
||||
- 优化错误提示,支持复制完整原因,托盘和快捷键操作失败时也会提醒
|
||||
- 优化代理列表刷新、排序和页面切换速度
|
||||
|
||||
@@ -8,8 +8,8 @@ notifications:
|
||||
body: '{mode} moduna geçildi.'
|
||||
systemProxyToggled:
|
||||
title: Sistem Vekil'i
|
||||
'on': System proxy has been enabled.
|
||||
'off': System proxy has been disabled.
|
||||
'on': Sistem vekili etkinleştirildi.
|
||||
'off': Sistem vekili devre dışı bırakıldı.
|
||||
systemProxyFailed:
|
||||
title: Sistem Vekili
|
||||
body: Sistem vekil sunucusu ayarlanamadı. Nedenini görmek için Clash Verge'i açın.
|
||||
@@ -18,8 +18,8 @@ notifications:
|
||||
body: Art arda birkaç başarısızlıktan sonra durdu. Nedenini görmek için Clash Verge'i açın.
|
||||
tunModeToggled:
|
||||
title: TUN Modu
|
||||
'on': TUN mode has been enabled.
|
||||
'off': TUN mode has been disabled.
|
||||
'on': TUN modu etkinleştirildi.
|
||||
'off': TUN modu devre dışı bırakıldı.
|
||||
lightweightModeEntered:
|
||||
title: Hafif Mod
|
||||
body: Hafif moda geçildi.
|
||||
@@ -33,10 +33,10 @@ notifications:
|
||||
title: Uygulama Gizlendi
|
||||
body: Clash Verge arka planda çalışıyor.
|
||||
updateReady:
|
||||
title: Clash Verge Update
|
||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
||||
installNow: Install Now
|
||||
later: Later
|
||||
title: Clash Verge Güncellemesi
|
||||
body: Yeni bir sürüm (v{version}) indirildi ve kurulmaya hazır.
|
||||
installNow: Şimdi Kur
|
||||
later: Daha Sonra
|
||||
service:
|
||||
adminInstallPrompt: Clash Verge hizmetini kurmak için yönetici ayrıcalıkları gerekir.
|
||||
adminUninstallPrompt: Clash Verge hizmetini kaldırmak için yönetici ayrıcalıkları gerekir.
|
||||
|
||||
+9
-9
@@ -52,13 +52,13 @@
|
||||
"@mui/icons-material": "^9.0.1",
|
||||
"@mui/material": "^9.0.1",
|
||||
"@tanstack/react-virtual": "^3.13.25",
|
||||
"@tauri-apps/api": "3.0.0-alpha.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^3.0.0-alpha.0",
|
||||
"@tauri-apps/plugin-dialog": "^3.0.0-alpha.0",
|
||||
"@tauri-apps/plugin-fs": "^3.0.0-alpha.0",
|
||||
"@tauri-apps/plugin-http": "~3.0.0-alpha.0",
|
||||
"@tauri-apps/plugin-opener": "3.0.0-alpha.0",
|
||||
"@tauri-apps/plugin-updater": "3.0.0-alpha.0",
|
||||
"@tauri-apps/api": "2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"@tauri-apps/plugin-fs": "^2.5.1",
|
||||
"@tauri-apps/plugin-http": "~2.6.0",
|
||||
"@tauri-apps/plugin-opener": "2.5.5",
|
||||
"@tauri-apps/plugin-updater": "2.11.0",
|
||||
"ahooks": "^3.9.7",
|
||||
"cidr-block": "^2.3.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
@@ -93,7 +93,7 @@
|
||||
"@biomejs/biome": "^2.4.15",
|
||||
"@eslint-react/eslint-plugin": "^5.8.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tauri-apps/cli": "3.0.0-alpha.1",
|
||||
"@tauri-apps/cli": "2.11.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.12.4",
|
||||
"@types/react": "19.3.0",
|
||||
@@ -144,5 +144,5 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.4.2"
|
||||
"packageManager": "pnpm@12.5.1"
|
||||
}
|
||||
|
||||
Generated
+142
-147
@@ -7,153 +7,153 @@ importers:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.4.2
|
||||
version: 12.4.2
|
||||
specifier: 12.5.1
|
||||
version: 12.5.1
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.4.2':
|
||||
resolution: {integrity: sha512-E255MbcQ0V1577M2BV0ajWuP7KmTPYA0jqZnk3rK6Lq3kTvZWulMMb7iqRmnLsQbyWTkMEB2CfyM70loVDA8xg==}
|
||||
'@pnpm/exe.android-arm64@12.5.1':
|
||||
resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.android-x64@12.4.2':
|
||||
resolution: {integrity: sha512-J1pSeCUwuKxMG70ZzpWn8JzElxiEa8NN/3N0SlZRtU2xbztChaJCKF3HaBNIj9yPfeofWzJo/lwNvDDjraQuZw==}
|
||||
'@pnpm/exe.android-x64@12.5.1':
|
||||
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.4.2':
|
||||
resolution: {integrity: sha512-A0WDo8iErfZBXgrLseQxw8i8Y9ctUpOEl/Uu+cubnTzpD8tT9ykIB548L8YTM2WD4OS+ZOHSxy8aGZcvKq8PaQ==}
|
||||
'@pnpm/exe.darwin-arm64@12.5.1':
|
||||
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.4.2':
|
||||
resolution: {integrity: sha512-MSgJdovBWHcb5DEOvfPH9yNi/T5O1Xa4ess+E1ESGo/5yuty7S4JoiIjami+fsNXfnoQMlwsMUqYU4zAvBcNCw==}
|
||||
'@pnpm/exe.darwin-x64@12.5.1':
|
||||
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.4.2':
|
||||
resolution: {integrity: sha512-h2YumlQSNvgbRPv+RXwABohX65f9bOBZn+jMIt7bFDISZPCzQ+Nvpt6Awbp4ip5PwQgYxbu5iREJ1fHE39Fm8A==}
|
||||
'@pnpm/exe.freebsd-x64@12.5.1':
|
||||
resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.4.2':
|
||||
resolution: {integrity: sha512-LwSEtSEDTv6S51YLs3YvSkPyun/QmfMic1UGICUkPWFu6ByP43RdMlkKvmVkfGhAYCpnxO057vrmyJqtfZrPCA==}
|
||||
'@pnpm/exe.linux-arm64-musl@12.5.1':
|
||||
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.4.2':
|
||||
resolution: {integrity: sha512-2dSiDXyhx+RTHsewxex8f/jVjqQXWJ2oow4kCVHEWdZKeBpgMxvZ6fHkTAUBJXgqhbKIDHvuNlZBP7gJfUWL5A==}
|
||||
'@pnpm/exe.linux-arm64@12.5.1':
|
||||
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.4.2':
|
||||
resolution: {integrity: sha512-8Itc+jQk+MTz04LS9D1RcH+VctAWmzM4l1cJQ+Sx7pAJNo7EKHdRMbC0Tok1jyQ7eEHNJZD5EleYneZqWfZM+g==}
|
||||
'@pnpm/exe.linux-ppc64@12.5.1':
|
||||
resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.4.2':
|
||||
resolution: {integrity: sha512-hleOeqhTVpH+z9RVMGnxvU4ZnrkBUClWjCbHD9u6kwyqhGSpevoU1wTGish+CBRhmIgMAy9pAfFpqhbAKOKNfw==}
|
||||
'@pnpm/exe.linux-riscv64@12.5.1':
|
||||
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.4.2':
|
||||
resolution: {integrity: sha512-LAsQRRdP9aToENR6dtcIJ9l+e1zMYOX0tQcLGpRyLPVBQcYRLlvAPcmDshsiIHQjp05SDa9FI0czX1ZQ1a7a/A==}
|
||||
'@pnpm/exe.linux-s390x@12.5.1':
|
||||
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.4.2':
|
||||
resolution: {integrity: sha512-kzfzH2/0BWdTABK14Yj5a1xsdkTEQUp2eXEPNakaD9jKL025lq3hyaKHIz/gIZaPDMe/1bFFK/En4ztFlbBJxw==}
|
||||
'@pnpm/exe.linux-x64-musl@12.5.1':
|
||||
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-x64@12.4.2':
|
||||
resolution: {integrity: sha512-/pbt0UVTa8NMDhzOWLQRfZ6G9ROKXlJPZtx845BqyWfc7hCrWXhTLBd70yO2y8+E+IWN3oHM1s/JsIQNGk1yvg==}
|
||||
'@pnpm/exe.linux-x64@12.5.1':
|
||||
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.4.2':
|
||||
resolution: {integrity: sha512-PsW19e4dAUNpZ0cS9flaxFuAmpt2dKlH/Vvi8TZ4qyJjjQzue/CEsWm+6wKVP4CJ7IT8RdL4qh+soOPWIgF2Zw==}
|
||||
'@pnpm/exe.win32-arm64@12.5.1':
|
||||
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/exe.win32-x64@12.4.2':
|
||||
resolution: {integrity: sha512-+xGoeE0g55ztWvl8i5QqdmNfW3nIrTVcoQrNshEOwxthm9Ag48oXton3uxOA3/SANyz5AXexEjj2KO20NnOrEw==}
|
||||
'@pnpm/exe.win32-x64@12.5.1':
|
||||
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
pnpm@12.4.2:
|
||||
resolution: {integrity: sha512-CK3GYTGAJ1x8ntraOdzwjJxhrU5+rzMKTzRh8QKw+QdCNFTRF/mOctR/7wYWBwZE17/8lzpqV/UJCm18NosHyQ==}
|
||||
pnpm@12.5.1:
|
||||
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
|
||||
engines: {node: '>=18.*'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.4.2':
|
||||
'@pnpm/exe.android-arm64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.android-x64@12.4.2':
|
||||
'@pnpm/exe.android-x64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.4.2':
|
||||
'@pnpm/exe.darwin-arm64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.4.2':
|
||||
'@pnpm/exe.darwin-x64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.4.2':
|
||||
'@pnpm/exe.freebsd-x64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.4.2':
|
||||
'@pnpm/exe.linux-arm64-musl@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.4.2':
|
||||
'@pnpm/exe.linux-arm64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.4.2':
|
||||
'@pnpm/exe.linux-ppc64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.4.2':
|
||||
'@pnpm/exe.linux-riscv64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.4.2':
|
||||
'@pnpm/exe.linux-s390x@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.4.2':
|
||||
'@pnpm/exe.linux-x64-musl@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64@12.4.2':
|
||||
'@pnpm/exe.linux-x64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.4.2':
|
||||
'@pnpm/exe.win32-arm64@12.5.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-x64@12.4.2':
|
||||
'@pnpm/exe.win32-x64@12.5.1':
|
||||
optional: true
|
||||
|
||||
pnpm@12.4.2:
|
||||
pnpm@12.5.1:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.android-arm64': 12.4.2
|
||||
'@pnpm/exe.android-x64': 12.4.2
|
||||
'@pnpm/exe.darwin-arm64': 12.4.2
|
||||
'@pnpm/exe.darwin-x64': 12.4.2
|
||||
'@pnpm/exe.freebsd-x64': 12.4.2
|
||||
'@pnpm/exe.linux-arm64': 12.4.2
|
||||
'@pnpm/exe.linux-arm64-musl': 12.4.2
|
||||
'@pnpm/exe.linux-ppc64': 12.4.2
|
||||
'@pnpm/exe.linux-riscv64': 12.4.2
|
||||
'@pnpm/exe.linux-s390x': 12.4.2
|
||||
'@pnpm/exe.linux-x64': 12.4.2
|
||||
'@pnpm/exe.linux-x64-musl': 12.4.2
|
||||
'@pnpm/exe.win32-arm64': 12.4.2
|
||||
'@pnpm/exe.win32-x64': 12.4.2
|
||||
'@pnpm/exe.android-arm64': 12.5.1
|
||||
'@pnpm/exe.android-x64': 12.5.1
|
||||
'@pnpm/exe.darwin-arm64': 12.5.1
|
||||
'@pnpm/exe.darwin-x64': 12.5.1
|
||||
'@pnpm/exe.freebsd-x64': 12.5.1
|
||||
'@pnpm/exe.linux-arm64': 12.5.1
|
||||
'@pnpm/exe.linux-arm64-musl': 12.5.1
|
||||
'@pnpm/exe.linux-ppc64': 12.5.1
|
||||
'@pnpm/exe.linux-riscv64': 12.5.1
|
||||
'@pnpm/exe.linux-s390x': 12.5.1
|
||||
'@pnpm/exe.linux-x64': 12.5.1
|
||||
'@pnpm/exe.linux-x64-musl': 12.5.1
|
||||
'@pnpm/exe.win32-arm64': 12.5.1
|
||||
'@pnpm/exe.win32-x64': 12.5.1
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
@@ -200,26 +200,26 @@ importers:
|
||||
specifier: ^3.13.25
|
||||
version: 3.14.10(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
'@tauri-apps/api':
|
||||
specifier: 3.0.0-alpha.0
|
||||
version: 3.0.0-alpha.0
|
||||
specifier: 2.11.1
|
||||
version: 2.11.1
|
||||
'@tauri-apps/plugin-clipboard-manager':
|
||||
specifier: ^3.0.0-alpha.0
|
||||
version: 3.0.0-alpha.0
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: ^3.0.0-alpha.0
|
||||
version: 3.0.0-alpha.0
|
||||
specifier: ^2.7.1
|
||||
version: 2.7.2
|
||||
'@tauri-apps/plugin-fs':
|
||||
specifier: ^3.0.0-alpha.0
|
||||
version: 3.0.0-alpha.0
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
'@tauri-apps/plugin-http':
|
||||
specifier: ~3.0.0-alpha.0
|
||||
version: 3.0.0-alpha.0
|
||||
specifier: ~2.6.0
|
||||
version: 2.6.0
|
||||
'@tauri-apps/plugin-opener':
|
||||
specifier: 3.0.0-alpha.0
|
||||
version: 3.0.0-alpha.0
|
||||
specifier: 2.5.5
|
||||
version: 2.5.5
|
||||
'@tauri-apps/plugin-updater':
|
||||
specifier: 3.0.0-alpha.0
|
||||
version: 3.0.0-alpha.0
|
||||
specifier: 2.11.0
|
||||
version: 2.11.0
|
||||
ahooks:
|
||||
specifier: ^3.9.7
|
||||
version: 3.9.7(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
@@ -318,8 +318,8 @@ importers:
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))
|
||||
'@tauri-apps/cli':
|
||||
specifier: 3.0.0-alpha.1
|
||||
version: 3.0.0-alpha.1
|
||||
specifier: 2.11.4
|
||||
version: 2.11.4
|
||||
'@types/lodash-es':
|
||||
specifier: ^4.17.12
|
||||
version: 4.17.12
|
||||
@@ -2106,102 +2106,99 @@ packages:
|
||||
'@tauri-apps/api@2.11.1':
|
||||
resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==}
|
||||
|
||||
'@tauri-apps/api@3.0.0-alpha.0':
|
||||
resolution: {integrity: sha512-uyZ7uHZz1qYlj4zi7d8WC1cBdI6WLAVJjPALeKkMCBdzpMMF5sf0iY3BX52ziC4zpEOnUNoi5ctRz0GWvvExOg==}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-63uxPcMKNtRScNi1QqkqIabwBH1T7gIU2TmbUphrSLfHjGlBuT83InpIql96pfNBJk95w/aV4krZhT9hmL78tA==}
|
||||
'@tauri-apps/cli-darwin-arm64@2.11.4':
|
||||
resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tauri-apps/cli-darwin-x64@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-eSBLEBJgwHZDLCsdR2fsDoEztd48mnM8U64rSlZCqULOr3sIRtXVPdoHGBoUoU4C84j+utGumXa+frIxbq7Log==}
|
||||
'@tauri-apps/cli-darwin-x64@2.11.4':
|
||||
resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-6peZspFoNmfoqzBKCiziOsntq2Dy5WI0RLXCdffqCrsC/CplIQB1xmldmOeI8xzuTjRCUqxEO2Vurv7HVluZhA==}
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.4':
|
||||
resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-gnu@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-gp3CJqdmqPE1IEkH6sbFDI6yYhxqGn8OuHU6jkA7jAVdxJGdeGuQZE9c7XrEq8uFNNfjz6bOI3djfVOvqzgAvw==}
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.11.4':
|
||||
resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-t6lhy7kvBp5VKGoob0X8MBMAS/ah1NE3s0N9kwhMw06gAFMBvDVnq/het3CkZkbQm+uoUbDnWagZ+7buGkI6XQ==}
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.11.4':
|
||||
resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-SHjvrK8DZ+FdIgLaisMgTVtDKCoR3m4639FgO/EeJcMM73LipUqgKIpkxxutqMyLoPJ6gxIQ6HHjNvNqUCmN3w==}
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.11.4':
|
||||
resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-0nwRa3cD//pAIRm3xwA3+N9zI01WnRBVOGHN4dpnWoZZDwwbsvCHwzyl34FsJv5wwcvGLlzbjMART55IPpTdlQ==}
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.11.4':
|
||||
resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-8ljY9JY6iGhGEAm7YWfQTa5If88to4Tpk8HHtIo6bay3+IJW1XanAmykLQdHoBRxMJyuAeE7uk2Gmfu7plQifg==}
|
||||
'@tauri-apps/cli-linux-x64-musl@2.11.4':
|
||||
resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-1L79hm8E6NhauTjl3WXkptZ1K8mfkgs3nB38YopgvThZI6ncVJ1NOMcaJfdxOzCkuliDAl1PSUkQnljNTNsmww==}
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.11.4':
|
||||
resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli-win32-ia32-msvc@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-YKhX4NS2xKyDduznVVhkiNVitwBVTe8erheiHrYzWVsSuHCQVyiQAajfuAaufdeCIv0cTFo6siCWmBu1YcFUcg==}
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.11.4':
|
||||
resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli-win32-x64-msvc@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-/Bth9cwQOGgcoST8SJ5obTCYpVqqVYoRqo5RBUvbsIHRCbui1zL+giQDWdXDhCJEEXkGmSMiIxWTmg+v0Wp4uA==}
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.11.4':
|
||||
resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli@3.0.0-alpha.1':
|
||||
resolution: {integrity: sha512-7KSBFGBZk07THOp18HtMIntgxyYQNMDNcm0EEcmRmGTdcUS7X0BkrWDxtw8PRDMA9RZ1oYZKcjJjFyA8Lwr6dw==}
|
||||
'@tauri-apps/cli@2.11.4':
|
||||
resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==}
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@tauri-apps/plugin-clipboard-manager@3.0.0-alpha.0':
|
||||
resolution: {integrity: sha512-doH+SBkHXvNKAfInPIFKtAZrmmZRHOA5yf4lFeu74PRbMB7tfH/qNhfepi3C7HRBsmfyWUht/UqfrwMWKfLTKw==}
|
||||
'@tauri-apps/plugin-clipboard-manager@2.3.2':
|
||||
resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==}
|
||||
|
||||
'@tauri-apps/plugin-dialog@3.0.0-alpha.0':
|
||||
resolution: {integrity: sha512-ZnHHbwYlB/1nJqs0gZdi7eHumo3mArCSakx/7T1pqEIkqmXV4PHC+dSZLzsc20+A/HNgIQqa2uR/+yaXKWDzbw==}
|
||||
'@tauri-apps/plugin-dialog@2.7.2':
|
||||
resolution: {integrity: sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg==}
|
||||
|
||||
'@tauri-apps/plugin-fs@3.0.0-alpha.0':
|
||||
resolution: {integrity: sha512-PlpcBFbWNZZrOvd0pUJbdTGaDFXuFVtW5tmF2D2w7mf/KwRI0w7jzrR1b7Sy6fs0lOR5zI/sXQHdt7ExKoEMcQ==}
|
||||
'@tauri-apps/plugin-fs@2.5.1':
|
||||
resolution: {integrity: sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==}
|
||||
|
||||
'@tauri-apps/plugin-http@3.0.0-alpha.0':
|
||||
resolution: {integrity: sha512-cRyrWu2fcrlSmebEc3XE3UGE/Hj20REKpgbtlpAEydk5/dvu0nfvC/wCKEgD2w8h0XwTu8ewXkRG0R/j18a+5w==}
|
||||
'@tauri-apps/plugin-http@2.6.0':
|
||||
resolution: {integrity: sha512-QYXwbGb4hQ9/8Riv/ejU/kPFFnbBIrBcWwV1LIXv2xBKfoj8lkWfGkd9pkCSsBI/pljPtz+IPqfrE3t3bVl3mg==}
|
||||
|
||||
'@tauri-apps/plugin-opener@3.0.0-alpha.0':
|
||||
resolution: {integrity: sha512-jbAAtkBlJCRIujSN+oGioSSb7E+r2f4w+VNFcakwf6ZsaX1JXVawRZJmNQ/h2/tvHpMO7s6neX56HrlWc7Cnjg==}
|
||||
'@tauri-apps/plugin-opener@2.5.5':
|
||||
resolution: {integrity: sha512-xvzGai5aQds8j8R8RsUK/lW6pGG50YgOYIPLzvkqmkwAj7dfySOD7sGtejRzvVdMmv1EQfKVEFh1MvmDp8QR0g==}
|
||||
|
||||
'@tauri-apps/plugin-updater@3.0.0-alpha.0':
|
||||
resolution: {integrity: sha512-ArbrJFjrn0vtnMkHOPdEQZM6hD6EXUMNXNVcXFtxhnst1MrjO5SKjcZZIg3E6W3Y2DUBE8iIMMKJfYOLDRiBxQ==}
|
||||
'@tauri-apps/plugin-updater@2.11.0':
|
||||
resolution: {integrity: sha512-AE36XkOoSna24G40jZMY15nzAnkXEPL/73tGoseGrtGOHuI/cZwWzHpZFLjKXDPgzYZ435z1gHu28LgrsBwIxQ==}
|
||||
|
||||
'@tootallnate/quickjs-emscripten@0.23.0':
|
||||
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
|
||||
@@ -6958,76 +6955,74 @@ snapshots:
|
||||
|
||||
'@tauri-apps/api@2.11.1': {}
|
||||
|
||||
'@tauri-apps/api@3.0.0-alpha.0': {}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-darwin-arm64@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-darwin-x64@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-darwin-x64@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-gnu@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-linux-x64-musl@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-ia32-msvc@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-x64-msvc@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.11.4':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli@3.0.0-alpha.1':
|
||||
'@tauri-apps/cli@2.11.4':
|
||||
optionalDependencies:
|
||||
'@tauri-apps/cli-darwin-arm64': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-darwin-x64': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-linux-arm64-gnu': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-linux-arm64-musl': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-linux-riscv64-gnu': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-linux-x64-gnu': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-linux-x64-musl': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-win32-arm64-msvc': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-win32-x64-msvc': 3.0.0-alpha.1
|
||||
'@tauri-apps/cli-darwin-arm64': 2.11.4
|
||||
'@tauri-apps/cli-darwin-x64': 2.11.4
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4
|
||||
'@tauri-apps/cli-linux-arm64-gnu': 2.11.4
|
||||
'@tauri-apps/cli-linux-arm64-musl': 2.11.4
|
||||
'@tauri-apps/cli-linux-riscv64-gnu': 2.11.4
|
||||
'@tauri-apps/cli-linux-x64-gnu': 2.11.4
|
||||
'@tauri-apps/cli-linux-x64-musl': 2.11.4
|
||||
'@tauri-apps/cli-win32-arm64-msvc': 2.11.4
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 2.11.4
|
||||
'@tauri-apps/cli-win32-x64-msvc': 2.11.4
|
||||
|
||||
'@tauri-apps/plugin-clipboard-manager@3.0.0-alpha.0':
|
||||
'@tauri-apps/plugin-clipboard-manager@2.3.2':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
'@tauri-apps/plugin-dialog@3.0.0-alpha.0':
|
||||
'@tauri-apps/plugin-dialog@2.7.2':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
'@tauri-apps/plugin-fs@3.0.0-alpha.0':
|
||||
'@tauri-apps/plugin-fs@2.5.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
'@tauri-apps/plugin-http@3.0.0-alpha.0':
|
||||
'@tauri-apps/plugin-http@2.6.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
'@tauri-apps/plugin-opener@3.0.0-alpha.0':
|
||||
'@tauri-apps/plugin-opener@2.5.5':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
'@tauri-apps/plugin-updater@3.0.0-alpha.0':
|
||||
'@tauri-apps/plugin-updater@2.11.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
|
||||
@@ -10,15 +10,3 @@ allowBuilds:
|
||||
unrs-resolver: true
|
||||
minimumReleaseAgeExclude:
|
||||
- dayjs@1.11.21
|
||||
- '@tauri-apps/cli-darwin-arm64@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-darwin-x64@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-linux-arm-gnueabihf@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-linux-arm64-gnu@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-linux-arm64-musl@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-linux-riscv64-gnu@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-linux-x64-gnu@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-linux-x64-musl@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-win32-arm64-msvc@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-win32-ia32-msvc@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli-win32-x64-msvc@3.0.0-alpha.1'
|
||||
- '@tauri-apps/cli@3.0.0-alpha.1'
|
||||
|
||||
+17
-1
@@ -56,6 +56,12 @@
|
||||
"matchFileNames": [".github/workflows/*.lock.yml"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Ignore gh-aw dependency updates in workflow files; upgrade agentic workflows with gh aw upgrade instead",
|
||||
"matchFileNames": [".github/workflows/**"],
|
||||
"matchPackageNames": ["/gh-aw/"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Skip TypeScript 7.0 series",
|
||||
"matchManagers": ["npm"],
|
||||
@@ -78,7 +84,7 @@
|
||||
},
|
||||
{
|
||||
"description": "Group PNPM version updates across package.json, tool-versions, workflows, and devcontainer",
|
||||
"matchPackageNames": ["pnpm", "ghcr.io/devcontainers/features/pnpm"],
|
||||
"matchPackageNames": ["pnpm"],
|
||||
"groupName": "pnpm"
|
||||
},
|
||||
{
|
||||
@@ -131,6 +137,16 @@
|
||||
],
|
||||
"depNameTemplate": "rust-lang/rust",
|
||||
"datasourceTemplate": "github-releases"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update pnpm in devcontainer.json",
|
||||
"fileMatch": ["^\\.devcontainer/devcontainer\\.json$"],
|
||||
"matchStrings": [
|
||||
"\"ghcr\\.io/devcontainers/features/node:[^\"]+\":\\s*\\{[^}]*\"pnpmVersion\":\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "pnpm",
|
||||
"datasourceTemplate": "npm"
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
|
||||
+50
-15
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
|
||||
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, Cargo.lock, and src-tauri/tauri.conf.json.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm release-version <version>
|
||||
@@ -108,17 +108,20 @@ async function updatePackageVersion(newVersion) {
|
||||
'[INFO]: Current package.json version is: ',
|
||||
packageJson.version,
|
||||
)
|
||||
packageJson.version = newVersion.startsWith('v')
|
||||
const versionWithoutV = newVersion.startsWith('v')
|
||||
? newVersion.slice(1)
|
||||
: newVersion
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
'utf8',
|
||||
)
|
||||
console.log(
|
||||
`[INFO]: package.json version updated to: ${packageJson.version}`,
|
||||
const updatedData = data.replace(
|
||||
/^(\s*"version"\s*:\s*)"[^"]+"/m,
|
||||
`$1"${versionWithoutV}"`,
|
||||
)
|
||||
|
||||
if (updatedData === data) {
|
||||
throw new Error('version field was not found in package.json')
|
||||
}
|
||||
|
||||
await fs.writeFile(packageJsonPath, updatedData, 'utf8')
|
||||
console.log(`[INFO]: package.json version updated to: ${versionWithoutV}`)
|
||||
} catch (error) {
|
||||
console.error('Error updating package.json version:', error)
|
||||
throw error
|
||||
@@ -153,6 +156,34 @@ async function updateCargoVersion(newVersion) {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCargoLockVersion(newVersion) {
|
||||
const _dirname = process.cwd()
|
||||
const cargoLockPath = path.join(_dirname, 'Cargo.lock')
|
||||
const versionWithoutV = newVersion.startsWith('v')
|
||||
? newVersion.slice(1)
|
||||
: newVersion
|
||||
const packageVersionPattern =
|
||||
/(\[\[package\]\]\r?\nname = "clash-verge"\r?\nversion = )"[^"]+"/
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(cargoLockPath, 'utf8')
|
||||
const updatedData = data.replace(
|
||||
packageVersionPattern,
|
||||
`$1"${versionWithoutV}"`,
|
||||
)
|
||||
|
||||
if (updatedData === data) {
|
||||
throw new Error('clash-verge package entry was not found in Cargo.lock')
|
||||
}
|
||||
|
||||
await fs.writeFile(cargoLockPath, updatedData, 'utf8')
|
||||
console.log(`[INFO]: Cargo.lock version updated to: ${versionWithoutV}`)
|
||||
} catch (error) {
|
||||
console.error('Error updating Cargo.lock version:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTauriConfigVersion(newVersion) {
|
||||
const _dirname = process.cwd()
|
||||
const tauriConfigPath = path.join(_dirname, 'src-tauri', 'tauri.conf.json')
|
||||
@@ -168,13 +199,16 @@ async function updateTauriConfigVersion(newVersion) {
|
||||
tauriConfig.version,
|
||||
)
|
||||
|
||||
tauriConfig.version = versionWithoutV
|
||||
|
||||
await fs.writeFile(
|
||||
tauriConfigPath,
|
||||
JSON.stringify(tauriConfig, null, 2),
|
||||
'utf8',
|
||||
const updatedData = data.replace(
|
||||
/^(\s*"version"\s*:\s*)"[^"]+"/m,
|
||||
`$1"${versionWithoutV}"`,
|
||||
)
|
||||
|
||||
if (updatedData === data) {
|
||||
throw new Error('version field was not found in tauri.conf.json')
|
||||
}
|
||||
|
||||
await fs.writeFile(tauriConfigPath, updatedData, 'utf8')
|
||||
console.log(
|
||||
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
||||
)
|
||||
@@ -239,6 +273,7 @@ async function main(versionArg) {
|
||||
console.log(`[INFO]: Updating versions to: ${newVersion}`)
|
||||
await updatePackageVersion(newVersion)
|
||||
await updateCargoVersion(newVersion)
|
||||
await updateCargoLockVersion(newVersion)
|
||||
await updateTauriConfigVersion(newVersion)
|
||||
console.log('[SUCCESS]: All version updates completed successfully!')
|
||||
} catch (error) {
|
||||
|
||||
+19
-20
@@ -23,15 +23,13 @@ tauri-dev = ["dep:tauri-plugin-devtools"]
|
||||
tokio-trace = ["clash-verge-logging/tokio-trace"]
|
||||
chrome-trace = ["clash-verge-logging/chrome-trace"]
|
||||
clippy = ["tauri/test"]
|
||||
# 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.
|
||||
perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "3.0.0-alpha.0", features = [] }
|
||||
tauri-build = { version = "2.6.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
clash-verge-draft = { workspace = true }
|
||||
@@ -44,12 +42,12 @@ 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 }
|
||||
@@ -76,13 +74,13 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
|
||||
"guard",
|
||||
] }
|
||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||
tauri-plugin-shell = "3.0.0-alpha.0"
|
||||
tauri-plugin-opener = "3.0.0-alpha.0"
|
||||
tauri-plugin-dialog = "3.0.0-alpha.0"
|
||||
tauri-plugin-fs = "3.0.0-alpha.0"
|
||||
tauri-plugin-process = "3.0.0-alpha.0"
|
||||
tauri-plugin-deep-link = "3.0.0-alpha.0"
|
||||
tauri-plugin-window-state = "3.0.0-alpha.0"
|
||||
tauri-plugin-shell = "2.3.6"
|
||||
tauri-plugin-opener = "2.5.5"
|
||||
tauri-plugin-dialog = "2.7.3"
|
||||
tauri-plugin-fs = "2.5.2"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-deep-link = "2.4.10"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
zip = "8.6.0"
|
||||
flate2 = "1.1.10"
|
||||
reqwest_dav = "0.3.3"
|
||||
@@ -93,15 +91,15 @@ getrandom = "0.4.3"
|
||||
futures = "0.3.34"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "3.0.0-alpha.0"
|
||||
tauri-plugin-notification = "2.4.0"
|
||||
tokio-stream = "0.1.19"
|
||||
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||
tauri-plugin-http = "3.0.0-alpha.0"
|
||||
tauri-plugin-http = "2.6.0"
|
||||
tracing = { workspace = true }
|
||||
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" }
|
||||
tauri-plugin-devtools = { version = "2.2.1", optional = true }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" }
|
||||
async-trait = "0.1.92"
|
||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.1", version = "2.7.1", features = [
|
||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.2", version = "2.7.2", features = [
|
||||
"client",
|
||||
] }
|
||||
arc-swap = "1.9.2"
|
||||
@@ -116,6 +114,7 @@ tokio-util = { version = "0.7.19", features = ["time"] }
|
||||
libc = "0.2.189"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
|
||||
objc2 = "0.6.4"
|
||||
objc2-foundation = { version = "0.3.2", features = [
|
||||
"NSString",
|
||||
@@ -147,9 +146,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 = "3.0.0-alpha.0"
|
||||
tauri-plugin-global-shortcut = "3.0.0-alpha.0"
|
||||
tauri-plugin-updater = "3.0.0-alpha.0"
|
||||
tauri-plugin-autostart = "2.5.1"
|
||||
tauri-plugin-global-shortcut = "2.3.2"
|
||||
tauri-plugin-updater = "2.11.0"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"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",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
chmod +x /usr/bin/clash-verge-service-install
|
||||
chmod +x /usr/bin/clash-verge-service-uninstall
|
||||
chmod +x /usr/bin/clash-verge-service
|
||||
|
||||
# The enforcement file exists in both Enforcing and Permissive mode.
|
||||
if [ ! -e /sys/fs/selinux/enforce ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -x /usr/sbin/semanage ] || [ ! -x /usr/sbin/restorecon ]; then
|
||||
echo "Warning: persistent SELinux rules were not installed; install policycoreutils and policycoreutils-python-utils, then reinstall this package. The service installer repairs executable labels, but a system relabel can reset them." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Keep these local rules on removal: they may predate this package or serve a standalone development service.
|
||||
for service in clash-verge-service clash-verge-service-dev; do
|
||||
for directory in bin cores; do
|
||||
target="/var/lib/$service/$directory"
|
||||
if [ -L "/var/lib/$service" ] || [ -L "$target" ]; then
|
||||
echo "Refusing to relabel a symlinked service directory: $target" >&2
|
||||
exit 1
|
||||
fi
|
||||
spec="$target(/.*)?"
|
||||
# Older semanage versions reject -a when an exact rule already exists.
|
||||
if ! add_error="$(/usr/sbin/semanage fcontext -a -t bin_t "$spec" 2>&1)"; then
|
||||
if ! /usr/sbin/semanage fcontext -m -t bin_t "$spec"; then
|
||||
printf '%s\n' "$add_error" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ -d "$target" ]; then
|
||||
/usr/sbin/restorecon -R "$target"
|
||||
fi
|
||||
done
|
||||
done
|
||||
@@ -210,7 +210,8 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
|
||||
return Ok(ValidationOutcome::invalid_from_message("DNS config file not found"));
|
||||
}
|
||||
|
||||
CoreConfigValidator::validate_config_file_outcome(dns_path_str, None)
|
||||
// A fragment, not a runnable config; the merged result is validated on apply.
|
||||
CoreConfigValidator::validate_config_file_outcome(dns_path_str, Some(true))
|
||||
.await
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ pub async fn continue_with_sidecar() -> CmdResult {
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn take_service_fallback_notice() -> bool {
|
||||
crate::core::service::take_service_fallback_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sync_runtime_providers() {
|
||||
if matches!(*CoreManager::global().get_running_mode(), RunningMode::Service) {
|
||||
|
||||
@@ -185,6 +185,42 @@ where
|
||||
apply_proxy().await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn run_service_start_with_sidecar_fallback<Start, StartFuture, Fallback, FallbackFuture>(
|
||||
is_admin: bool,
|
||||
start_service: Start,
|
||||
start_sidecar: Fallback,
|
||||
) -> Result<()>
|
||||
where
|
||||
Start: FnOnce() -> StartFuture,
|
||||
StartFuture: std::future::Future<Output = Result<()>>,
|
||||
Fallback: FnOnce(anyhow::Error) -> FallbackFuture,
|
||||
FallbackFuture: std::future::Future<Output = Result<()>>,
|
||||
{
|
||||
let Err(error) = start_service().await else {
|
||||
return Ok(());
|
||||
};
|
||||
// A transport failure cannot establish whether the service already started the core.
|
||||
let location_refused = error
|
||||
.downcast_ref::<crate::core::service::ServiceStartRefusal>()
|
||||
.is_some_and(|refusal| {
|
||||
refusal.code == clash_verge_service_ipc::ServiceErrorCode::InvalidInstallLocation as u16
|
||||
});
|
||||
if !is_admin || !location_refused {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"service refused the core location while app is elevated; falling back to sidecar: {error:#}"
|
||||
);
|
||||
let reason = format!("{error:#}");
|
||||
start_sidecar(error)
|
||||
.await
|
||||
.map_err(|error| error.context(format!("sidecar fallback failed after service refusal: {reason}")))
|
||||
}
|
||||
|
||||
async fn run_sidecar_termination_transition<Clear, ClearFuture, Terminate>(
|
||||
clear_proxy: Clear,
|
||||
terminate_sidecar: Terminate,
|
||||
@@ -550,7 +586,30 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
let result = match startup {
|
||||
StartupDecision::Service => self.start_core_by_service().await,
|
||||
StartupDecision::Service => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
run_service_start_with_sidecar_fallback(
|
||||
crate::core::runstate::RUN_STATE.state().is_admin,
|
||||
|| self.start_core_by_service(),
|
||||
|error| async move {
|
||||
use crate::core::runstate::RUN_STATE;
|
||||
|
||||
RUN_STATE.allow_sidecar_after_service_refusal(format!("{error:#}"))?;
|
||||
let result = self.start_core_by_sidecar().await;
|
||||
if result.is_err() {
|
||||
SERVICE_MANAGER.withdraw_sidecar_allowance();
|
||||
} else {
|
||||
crate::core::service::notify_service_fallback();
|
||||
}
|
||||
result
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
self.start_core_by_service().await
|
||||
}
|
||||
StartupDecision::Sidecar => self.start_core_by_sidecar().await,
|
||||
StartupDecision::Wait => Ok(()),
|
||||
};
|
||||
|
||||
@@ -18,6 +18,44 @@ use tauri_plugin_shell::ShellExt as _;
|
||||
|
||||
const SIDECAR_READINESS_ATTEMPTS: usize = 30;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn retry_service_start<Start, StartFuture>(
|
||||
attempts: usize,
|
||||
retry_delay: std::time::Duration,
|
||||
mut start: Start,
|
||||
) -> Result<()>
|
||||
where
|
||||
Start: FnMut() -> StartFuture,
|
||||
StartFuture: std::future::Future<Output = Result<()>>,
|
||||
{
|
||||
let mut last_error = None;
|
||||
for attempt in 0..attempts {
|
||||
match start().await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(error) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"service start attempt {}/{} failed: {error:#}",
|
||||
attempt + 1,
|
||||
attempts
|
||||
);
|
||||
if error
|
||||
.downcast_ref::<service::ServiceStartRefusal>()
|
||||
.is_some_and(|refusal| service::StageRequest::is_about_the_bundle(refusal.code))
|
||||
{
|
||||
return Err(error);
|
||||
}
|
||||
last_error = Some(error);
|
||||
if attempt + 1 < attempts {
|
||||
tokio::time::sleep(retry_delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_error.unwrap_or_else(|| anyhow::anyhow!("service start failed")))
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
/// Restores profile selections before callers enable the system proxy.
|
||||
/// The bounded first pass continues in the background, and later calls supersede earlier ones.
|
||||
@@ -170,33 +208,10 @@ impl CoreManager {
|
||||
let pid = child.pid();
|
||||
tracing::Span::current().record("pid", pid);
|
||||
|
||||
let readiness = poll_sidecar_readiness(SIDECAR_READINESS_ATTEMPTS, SIDECAR_READINESS_INTERVAL, || async {
|
||||
tokio::time::timeout(SIDECAR_READINESS_PROBE_TIMEOUT, async {
|
||||
handle::Handle::mihomo().get_version().await
|
||||
})
|
||||
.await
|
||||
.context("Mihomo readiness probe timed out")??;
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
if let Err(readiness_error) = readiness {
|
||||
proxy_control::stop_guard().await;
|
||||
self.core_stopped();
|
||||
return match child.kill() {
|
||||
Ok(()) => Err(readiness_error),
|
||||
Err(kill_error) => Err(anyhow::anyhow!(
|
||||
"{readiness_error:#}; failed to terminate unready sidecar PID {pid}: {kill_error:#}"
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
self.set_job_handle(Some(job));
|
||||
self.set_running_child_sidecar(child);
|
||||
// The sidecar has to be drained from the moment it starts. tauri-plugin-shell buffers a
|
||||
// single event, so an unread channel stalls its reader threads and the core blocks on a
|
||||
// full stdout pipe before it ever creates the API socket.
|
||||
let core_readiness_generation = self.mark_core_ready();
|
||||
self.core_started(RunningMode::Sidecar);
|
||||
self.restore_selected_nodes().await;
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
@@ -226,6 +241,32 @@ impl CoreManager {
|
||||
}
|
||||
});
|
||||
|
||||
let readiness = poll_sidecar_readiness(SIDECAR_READINESS_ATTEMPTS, SIDECAR_READINESS_INTERVAL, || async {
|
||||
tokio::time::timeout(SIDECAR_READINESS_PROBE_TIMEOUT, async {
|
||||
handle::Handle::mihomo().get_version().await
|
||||
})
|
||||
.await
|
||||
.context("Mihomo readiness probe timed out")??;
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
if let Err(readiness_error) = readiness {
|
||||
proxy_control::stop_guard().await;
|
||||
self.core_stopped();
|
||||
return match child.kill() {
|
||||
Ok(()) => Err(readiness_error),
|
||||
Err(kill_error) => Err(anyhow::anyhow!(
|
||||
"{readiness_error:#}; failed to terminate unready sidecar PID {pid}: {kill_error:#}"
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
self.set_job_handle(Some(job));
|
||||
self.set_running_child_sidecar(child);
|
||||
self.core_started(RunningMode::Sidecar);
|
||||
self.restore_selected_nodes().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -269,30 +310,15 @@ impl CoreManager {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::constants::timing;
|
||||
let mut last_err = None;
|
||||
for attempt in 0..timing::SERVICE_START_RETRIES {
|
||||
match service::run_core_by_service(config_file).await {
|
||||
Ok(()) => {
|
||||
self.mark_core_ready();
|
||||
self.core_started(RunningMode::Service);
|
||||
self.restore_selected_nodes().await;
|
||||
service::request_runtime_provider_sync(crate::constants::timing::RUNTIME_PROVIDER_SYNC_DELAY);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"service start attempt {}/{} failed: {e:#}",
|
||||
attempt + 1,
|
||||
timing::SERVICE_START_RETRIES
|
||||
);
|
||||
last_err = Some(e);
|
||||
tokio::time::sleep(timing::SERVICE_START_RETRY_DELAY).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("service start failed")))
|
||||
retry_service_start(timing::SERVICE_START_RETRIES, timing::SERVICE_START_RETRY_DELAY, || {
|
||||
service::run_core_by_service(config_file)
|
||||
})
|
||||
.await?;
|
||||
self.mark_core_ready();
|
||||
self.core_started(RunningMode::Service);
|
||||
self.restore_selected_nodes().await;
|
||||
service::request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SYNC_DELAY);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
|
||||
@@ -323,6 +323,21 @@ impl<E: RunStateEnv> RunStateStore<E> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn allow_sidecar_after_service_refusal(&self, reason: String) -> Result<()> {
|
||||
let mut state = self.service.lock();
|
||||
if self.operation_running.load(Ordering::Acquire) || state.service.pending.is_some() {
|
||||
bail!("cannot fall back while a service operation is pending");
|
||||
}
|
||||
// Publish the refusal and allowance together so it never asks the UI for a repair.
|
||||
state.service.health = ServiceHealth::Unavailable(reason);
|
||||
state.service.allow_sidecar();
|
||||
state.bump();
|
||||
drop(state);
|
||||
self.announce();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Requests installation only for a confirmed absent Service.
|
||||
pub fn require_install_for_session(&self) -> Result<()> {
|
||||
let mut state = self.service.lock();
|
||||
|
||||
@@ -39,6 +39,17 @@ use std::{
|
||||
|
||||
static OWNER_MONITOR_GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
static ACTIVE_SERVICE_SESSION: Lazy<Mutex<Option<ActiveServiceSession>>> = Lazy::new(|| Mutex::new(None));
|
||||
static PENDING_SERVICE_FALLBACK_NOTICE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn notify_service_fallback() {
|
||||
PENDING_SERVICE_FALLBACK_NOTICE.store(true, Ordering::Relaxed);
|
||||
Handle::notice_message("service_core::sidecar_fallback", "");
|
||||
}
|
||||
|
||||
pub(crate) fn take_service_fallback_notice() -> bool {
|
||||
PENDING_SERVICE_FALLBACK_NOTICE.swap(false, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Capabilities of the service session that owns the running Core.
|
||||
/// They are discarded with that session rather than cached across service upgrades.
|
||||
@@ -1076,6 +1087,25 @@ pub(super) async fn stage_runtime_by_service(config_file: &Path) -> Result<Stage
|
||||
.context("Clash Verge Service 未返回运行时暂存结果")
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ServiceStartRefusal {
|
||||
pub code: u16,
|
||||
pub core_path: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServiceStartRefusal {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
formatter,
|
||||
"failed to start Service core at {} (code {}): {}",
|
||||
self.core_path, self.code, self.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ServiceStartRefusal {}
|
||||
|
||||
/// 尝试使用服务启动core
|
||||
#[tracing::instrument(skip_all, level = "info", fields(generation = tracing::field::Empty, staging = tracing::field::Empty, code = tracing::field::Empty, outcome = tracing::field::Empty))]
|
||||
pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()> {
|
||||
@@ -1111,10 +1141,12 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
err_msg
|
||||
);
|
||||
start_owner_monitor();
|
||||
bail!(
|
||||
"failed to start Service core at {}: {err_msg}",
|
||||
request.runtime.core_path
|
||||
);
|
||||
return Err(ServiceStartRefusal {
|
||||
code: response.code,
|
||||
core_path: request.runtime.core_path,
|
||||
message: err_msg,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
let result = response.data.context("Clash Verge Service 未返回会话信息")?;
|
||||
@@ -1133,6 +1165,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
// PAC follows the Running Mode; the caller opens it via `core_started(Service)`.
|
||||
start_owner_monitor();
|
||||
tracing::Span::current().record("outcome", "started");
|
||||
PENDING_SERVICE_FALLBACK_NOTICE.store(false, Ordering::Relaxed);
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
|
||||
@@ -24,7 +24,7 @@ use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tauri::{
|
||||
AppHandle, DynRuntime,
|
||||
AppHandle, Wry,
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ use menu_def::{MenuIds, MenuTexts};
|
||||
|
||||
// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑)
|
||||
|
||||
type ProxyMenuItem = (Option<Submenu<DynRuntime>>, Vec<Box<dyn IsMenuItem<DynRuntime>>>);
|
||||
type ProxyMenuItem = (Option<Submenu<Wry>>, Vec<Box<dyn IsMenuItem<Wry>>>);
|
||||
|
||||
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
||||
pub const TRAY_ID: &str = "clash-verge-rev-tray";
|
||||
@@ -440,7 +440,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<DynRuntime>>> {
|
||||
) -> Result<Vec<CheckMenuItem<Wry>>> {
|
||||
profiles_preview
|
||||
.into_iter()
|
||||
.map(|profile| {
|
||||
@@ -462,9 +462,9 @@ fn create_subcreate_proxy_menu_item(
|
||||
proxy_mode: &str,
|
||||
proxy_group_order_map: Option<HashMap<String, usize>>,
|
||||
proxy_nodes_data: Option<Proxies>,
|
||||
) -> Vec<Submenu<DynRuntime>> {
|
||||
let proxy_submenus: Vec<Submenu<DynRuntime>> = {
|
||||
let mut submenus: Vec<(String, usize, Submenu<DynRuntime>)> = Vec::new();
|
||||
) -> Vec<Submenu<Wry>> {
|
||||
let proxy_submenus: Vec<Submenu<Wry>> = {
|
||||
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
|
||||
|
||||
// TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息
|
||||
if let Some(proxy_nodes_data) = proxy_nodes_data {
|
||||
@@ -484,7 +484,7 @@ fn create_subcreate_proxy_menu_item(
|
||||
|
||||
let now_proxy = group_data.now.as_deref().unwrap_or_default();
|
||||
|
||||
let group_items: Vec<CheckMenuItem<DynRuntime>> = all_proxies
|
||||
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
|
||||
.iter()
|
||||
.filter_map(|proxy_str| {
|
||||
let is_selected = *proxy_str == now_proxy;
|
||||
@@ -515,10 +515,8 @@ fn create_subcreate_proxy_menu_item(
|
||||
|
||||
let group_display_name = group_name.to_string();
|
||||
|
||||
let group_items_refs: Vec<&dyn IsMenuItem<DynRuntime>> = group_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<DynRuntime>)
|
||||
.collect();
|
||||
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> =
|
||||
group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>).collect();
|
||||
|
||||
if let Ok(submenu) = Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
@@ -554,7 +552,7 @@ fn create_subcreate_proxy_menu_item(
|
||||
fn create_proxy_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
show_proxy_groups_inline: bool,
|
||||
proxy_submenus: Vec<Submenu<DynRuntime>>,
|
||||
proxy_submenus: Vec<Submenu<Wry>>,
|
||||
proxies_text: &str,
|
||||
) -> Result<ProxyMenuItem> {
|
||||
let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline {
|
||||
@@ -562,13 +560,13 @@ fn create_proxy_menu_item(
|
||||
None,
|
||||
proxy_submenus
|
||||
.into_iter()
|
||||
.map(|submenu| Box::new(submenu) as Box<dyn IsMenuItem<DynRuntime>>)
|
||||
.map(|submenu| Box::new(submenu) as Box<dyn IsMenuItem<Wry>>)
|
||||
.collect(),
|
||||
)
|
||||
} else if !proxy_submenus.is_empty() {
|
||||
let proxy_submenu_refs: Vec<&dyn IsMenuItem<DynRuntime>> = proxy_submenus
|
||||
let proxy_submenu_refs: Vec<&dyn IsMenuItem<Wry>> = proxy_submenus
|
||||
.iter()
|
||||
.map(|submenu| submenu as &dyn IsMenuItem<DynRuntime>)
|
||||
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
|
||||
(
|
||||
@@ -595,7 +593,7 @@ async fn create_tray_menu(
|
||||
tun_mode_available: bool,
|
||||
profiles_preview: Vec<IProfilePreview<'_>>,
|
||||
options: TrayMenuOptions,
|
||||
) -> Result<tauri::menu::Menu<DynRuntime>> {
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
|
||||
let mut verge_settings = Config::verge().await.latest_arc();
|
||||
@@ -649,12 +647,12 @@ async fn create_tray_menu(
|
||||
|
||||
let hotkeys = create_hotkeys(&verge_settings.hotkeys);
|
||||
|
||||
let profile_menu_items: Vec<CheckMenuItem<DynRuntime>> = create_profile_menu_item(app_handle, profiles_preview)?;
|
||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = create_profile_menu_item(app_handle, profiles_preview)?;
|
||||
|
||||
let texts = MenuTexts::new();
|
||||
let profile_menu_items_refs: Vec<&dyn IsMenuItem<DynRuntime>> = profile_menu_items
|
||||
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<DynRuntime>)
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
|
||||
let open_window = &MenuItem::with_id(
|
||||
@@ -707,9 +705,9 @@ async fn create_tray_menu(
|
||||
outbound_modes_label.as_str(),
|
||||
true,
|
||||
&[
|
||||
rule_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
global_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
direct_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
rule_mode as &dyn IsMenuItem<Wry>,
|
||||
global_mode as &dyn IsMenuItem<Wry>,
|
||||
direct_mode as &dyn IsMenuItem<Wry>,
|
||||
],
|
||||
)?)
|
||||
};
|
||||
@@ -814,7 +812,7 @@ async fn create_tray_menu(
|
||||
&texts.more,
|
||||
true,
|
||||
&[
|
||||
copy_env as &dyn IsMenuItem<DynRuntime>,
|
||||
copy_env as &dyn IsMenuItem<Wry>,
|
||||
close_all_connections,
|
||||
restart_clash,
|
||||
restart_app,
|
||||
@@ -831,13 +829,13 @@ async fn create_tray_menu(
|
||||
|
||||
let separator = &PredefinedMenuItem::separator(app_handle)?;
|
||||
|
||||
let mut menu_items: Vec<&dyn IsMenuItem<DynRuntime>> = vec![open_window, separator];
|
||||
let mut menu_items: Vec<&dyn IsMenuItem<Wry>> = vec![open_window, separator];
|
||||
|
||||
if show_outbound_modes_inline {
|
||||
menu_items.extend_from_slice(&[
|
||||
rule_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
global_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
direct_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
rule_mode as &dyn IsMenuItem<Wry>,
|
||||
global_mode as &dyn IsMenuItem<Wry>,
|
||||
direct_mode as &dyn IsMenuItem<Wry>,
|
||||
]);
|
||||
} else if let Some(ref outbound_modes) = outbound_modes {
|
||||
menu_items.push(outbound_modes);
|
||||
@@ -847,7 +845,7 @@ async fn create_tray_menu(
|
||||
|
||||
match tray_proxy_groups_display_mode {
|
||||
"default" => {
|
||||
menu_items.extend(proxies_menu.iter().map(|item| item as &dyn IsMenuItem<DynRuntime>));
|
||||
menu_items.extend(proxies_menu.iter().map(|item| item as &dyn IsMenuItem<_>));
|
||||
}
|
||||
"inline" if !inline_proxy_items.is_empty() => {
|
||||
menu_items.extend(inline_proxy_items.iter().map(|item| item.as_ref()));
|
||||
@@ -857,14 +855,14 @@ async fn create_tray_menu(
|
||||
|
||||
menu_items.extend_from_slice(&[
|
||||
separator,
|
||||
system_proxy as &dyn IsMenuItem<DynRuntime>,
|
||||
tun_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
system_proxy as &dyn IsMenuItem<Wry>,
|
||||
tun_mode as &dyn IsMenuItem<Wry>,
|
||||
separator,
|
||||
lightweight_mode as &dyn IsMenuItem<DynRuntime>,
|
||||
open_dir as &dyn IsMenuItem<DynRuntime>,
|
||||
more as &dyn IsMenuItem<DynRuntime>,
|
||||
lightweight_mode as &dyn IsMenuItem<Wry>,
|
||||
open_dir as &dyn IsMenuItem<Wry>,
|
||||
more as &dyn IsMenuItem<Wry>,
|
||||
separator,
|
||||
quit as &dyn IsMenuItem<DynRuntime>,
|
||||
quit as &dyn IsMenuItem<Wry>,
|
||||
]);
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
|
||||
|
||||
@@ -284,8 +284,7 @@ fn process_seq_items(
|
||||
config
|
||||
}
|
||||
|
||||
/// App 权威的顶层控制面键:核心连接、监听端口、UI/托盘开关。
|
||||
/// 平台键随 cfg 门控;`dns.ipv6` 单独处理。
|
||||
/// App 权威的顶层控制面键;`dns.ipv6` 单独处理。
|
||||
const CONTROL_PLANE_KEYS: &[&str] = &[
|
||||
"external-controller",
|
||||
#[cfg(unix)]
|
||||
@@ -308,25 +307,20 @@ const CONTROL_PLANE_KEYS: &[&str] = &[
|
||||
"unified-delay",
|
||||
];
|
||||
|
||||
/// The fields the app owns, held across the stages that apply the user's manual overrides.
|
||||
///
|
||||
/// Capturing and restoring are one value rather than two calls because the order is a
|
||||
/// correctness requirement, not a style: capture must happen after the app has finished
|
||||
/// deriving these fields and before any override runs, and restore must happen after every
|
||||
/// override. As four separate calls that contract lived only in comments.
|
||||
/// App-owned fields: captured after the app derives them, enforced after every override.
|
||||
struct AuthoritativeFields {
|
||||
control_plane: Mapping,
|
||||
tun: Mapping,
|
||||
/// Only tracked when the DNS page owns it; otherwise overrides may set `dns.ipv6` freely.
|
||||
/// Only when the DNS page switched it on.
|
||||
dns_ipv6: Option<Value>,
|
||||
}
|
||||
|
||||
impl AuthoritativeFields {
|
||||
fn capture(config: &Mapping, gui_tun_keys: &[Value], enable_dns_settings: bool) -> Self {
|
||||
fn capture(config: &Mapping, gui_tun_keys: &[Value], owns_dns_ipv6: bool) -> Self {
|
||||
Self {
|
||||
control_plane: snapshot_control_plane(config),
|
||||
tun: snapshot_tun(config, gui_tun_keys),
|
||||
dns_ipv6: enable_dns_settings.then(|| snapshot_dns_ipv6(config)).flatten(),
|
||||
dns_ipv6: owns_dns_ipv6.then(|| snapshot_dns_ipv6(config)).flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +395,7 @@ fn enforce_tun(mut config: Mapping, snapshot: Mapping) -> Mapping {
|
||||
config
|
||||
}
|
||||
|
||||
/// DNS 页权威的嵌套开关;只在 `enable_dns_settings` 时快照。
|
||||
/// 只在 DNS 页开启 IPv6 时快照。
|
||||
fn snapshot_dns_ipv6(config: &Mapping) -> Option<Value> {
|
||||
config.get("dns")?.get("ipv6").cloned()
|
||||
}
|
||||
@@ -481,11 +475,7 @@ async fn process_profile_items(
|
||||
(config, exists_keys, result_map)
|
||||
}
|
||||
|
||||
/// Merge the Application Merge Config over the profile's own configuration.
|
||||
///
|
||||
/// Every switch it consults is a parameter: it used to read `enable_external_controller`
|
||||
/// out of the global config from inside its loop, which made a stage that looks pure depend
|
||||
/// on process state and left it untestable.
|
||||
/// Merges the app's clash config over the profile; switches are parameters to keep it pure.
|
||||
fn merge_default_config(
|
||||
mut config: Mapping,
|
||||
clash_config: Mapping,
|
||||
@@ -683,9 +673,7 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
|
||||
config
|
||||
}
|
||||
|
||||
/// 当 DNS 处于 fake-ip 模式且启用 IPv6 时,补充缺失的 `fake-ip-range6`,
|
||||
/// 否则 AAAA 查询无法获得 fake-ip,导致 IPv6 解析失败(见 issue #7373)。
|
||||
/// 兼容旧版本生成的、缺少该字段的 dns_config.yaml。
|
||||
/// fake-ip + IPv6 缺少 `fake-ip-range6` 时补默认值,否则 AAAA 无法解析(#7373)。
|
||||
fn ensure_fake_ip_range6(dns: &mut Mapping) {
|
||||
use serde_yaml_ng::Value;
|
||||
|
||||
@@ -708,38 +696,77 @@ fn ensure_fake_ip_range6(dns: &mut Mapping) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping {
|
||||
/// On, non-blank text, or non-empty list/map; anything else keeps the profile's value.
|
||||
fn is_set(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::Null => false,
|
||||
Value::Bool(on) => *on,
|
||||
Value::String(text) => !text.trim().is_empty(),
|
||||
Value::Sequence(items) => !items.is_empty(),
|
||||
Value::Mapping(map) => !map.is_empty(),
|
||||
Value::Number(_) | Value::Tagged(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Overlay set fields onto base; maps merge per key, lists are leaves.
|
||||
fn merge_set_fields(mut base: Mapping, overlay: Mapping) -> Mapping {
|
||||
for (key, value) in overlay {
|
||||
let value = match (base.get_mut(&key), value) {
|
||||
(Some(Value::Mapping(existing)), Value::Mapping(patch)) => {
|
||||
*existing = merge_set_fields(std::mem::take(existing), patch);
|
||||
continue;
|
||||
}
|
||||
(_, Value::Mapping(patch)) => Value::Mapping(merge_set_fields(Mapping::new(), patch)),
|
||||
(_, value) => value,
|
||||
};
|
||||
if is_set(&value) {
|
||||
base.insert(key, value);
|
||||
}
|
||||
}
|
||||
base
|
||||
}
|
||||
|
||||
fn take_mapping(config: &mut Mapping, key: &str) -> Mapping {
|
||||
config
|
||||
.get_mut(key)
|
||||
.and_then(Value::as_mapping_mut)
|
||||
.map(std::mem::take)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the merged config and whether the page set `dns.ipv6`.
|
||||
async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> (Mapping, bool) {
|
||||
let mut owns_ipv6 = false;
|
||||
if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() {
|
||||
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
||||
|
||||
if dns_path.exists()
|
||||
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path).await
|
||||
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
|
||||
&& let Ok(mut dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
|
||||
{
|
||||
if let Some(hosts_value) = dns_config.get("hosts")
|
||||
&& hosts_value.is_mapping()
|
||||
{
|
||||
config.insert("hosts".into(), hosts_value.clone());
|
||||
if let Some(Value::Mapping(hosts)) = dns_config.remove("hosts") {
|
||||
let hosts = merge_set_fields(take_mapping(&mut config, "hosts"), hosts);
|
||||
config.insert("hosts".into(), hosts.into());
|
||||
logging!(debug, Type::Core, "apply hosts configuration");
|
||||
}
|
||||
|
||||
if let Some(dns_value) = dns_config.get("dns") {
|
||||
if let Some(dns_mapping) = dns_value.as_mapping() {
|
||||
let mut dns_mapping = dns_mapping.clone();
|
||||
ensure_fake_ip_range6(&mut dns_mapping);
|
||||
config.insert("dns".into(), dns_mapping.into());
|
||||
logging!(debug, Type::Core, "apply dns_config.yaml (dns section)");
|
||||
}
|
||||
} else {
|
||||
let mut dns_config = dns_config;
|
||||
ensure_fake_ip_range6(&mut dns_config);
|
||||
config.insert("dns".into(), dns_config.into());
|
||||
// Legacy layout: no `dns` root.
|
||||
let dns_override = match dns_config.remove("dns") {
|
||||
Some(Value::Mapping(dns)) => Some(dns),
|
||||
Some(_) => None,
|
||||
None => Some(dns_config),
|
||||
};
|
||||
if let Some(dns_override) = dns_override {
|
||||
owns_ipv6 = dns_override.get("ipv6").is_some_and(is_set);
|
||||
let mut dns = merge_set_fields(take_mapping(&mut config, "dns"), dns_override);
|
||||
ensure_fake_ip_range6(&mut dns);
|
||||
config.insert("dns".into(), dns.into());
|
||||
logging!(debug, Type::Core, "apply dns_config.yaml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
(config, owns_ipv6)
|
||||
}
|
||||
|
||||
/// Returns the enhanced profile, its original keys, script logs, and DNS override decision.
|
||||
@@ -800,9 +827,9 @@ pub async fn enhance(
|
||||
|
||||
let config = apply_builtin_scripts(config, clash_core, enable_builtin);
|
||||
let config = use_tun(config, enable_tun);
|
||||
let config = apply_dns_settings(config, enable_dns_settings).await;
|
||||
let (config, owns_dns_ipv6) = apply_dns_settings(config, enable_dns_settings).await;
|
||||
|
||||
let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, enable_dns_settings);
|
||||
let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, owns_dns_ipv6);
|
||||
|
||||
let (config, exists_keys, result_map) = process_global_items(
|
||||
config,
|
||||
@@ -864,8 +891,7 @@ mod fake_ip_tests {
|
||||
|
||||
#[test]
|
||||
fn a_hand_edited_empty_range_counts_as_missing() {
|
||||
// The reason this is not just a `contains_key` check: YAML edited by hand often
|
||||
// leaves the key present with nothing after the colon.
|
||||
// Hand-edited YAML may leave the key present but empty.
|
||||
for blank in ["", " "] {
|
||||
let mut config = dns(&[
|
||||
("ipv6", Value::from(true)),
|
||||
@@ -1004,8 +1030,7 @@ mod use_tun_tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn switching_tun_off_leaves_dns_untouched() {
|
||||
// Turning TUN off must not rewrite DNS the user or profile chose; only the system
|
||||
// resolver is restored, which is not part of the configuration.
|
||||
// TUN off must not rewrite the DNS the profile chose.
|
||||
let config = mapping(&[("dns", Value::Mapping(mapping(&[("enable", Value::from(false))])))]);
|
||||
|
||||
let result = use_tun(config, false);
|
||||
@@ -1038,8 +1063,7 @@ mod use_sort_tests {
|
||||
|
||||
#[test]
|
||||
fn the_bulky_list_fields_are_written_last() {
|
||||
// These are the fields that make a config file unreadable from the top, so they are
|
||||
// pushed below the settings a human actually scans for.
|
||||
// Bulky list fields go last so the top of the file stays readable.
|
||||
let mut config = Mapping::new();
|
||||
config.insert(Value::from("rules"), Value::from("rules"));
|
||||
config.insert(Value::from("mode"), Value::from("rule"));
|
||||
@@ -1129,8 +1153,7 @@ mod merge_default_config_tests {
|
||||
|
||||
#[test]
|
||||
fn a_disabled_listener_is_removed_rather_than_merged() {
|
||||
// Removed, not just skipped: the profile may have asked for the port itself, and a
|
||||
// listener the user switched off must not be opened by the profile's own value.
|
||||
// Removed, not skipped: the profile's own value must not reopen a disabled listener.
|
||||
let config = mapping(&[("socks-port", Value::from(7891)), ("port", Value::from(7890))]);
|
||||
let clash_config = mapping(&[("socks-port", Value::from(1080)), ("port", Value::from(8080))]);
|
||||
|
||||
@@ -1152,8 +1175,7 @@ mod merge_default_config_tests {
|
||||
|
||||
#[test]
|
||||
fn a_disabled_external_controller_is_blanked_rather_than_dropped() {
|
||||
// Blanked, not removed: an absent key lets mihomo fall back to its own default and
|
||||
// listen anyway, which is the opposite of what switching it off means.
|
||||
// Blanked, not removed: an absent key makes mihomo listen on its default.
|
||||
let clash_config = mapping(&[("external-controller", Value::from("127.0.0.1:9090"))]);
|
||||
|
||||
let merged = merge(Mapping::new(), clash_config, false);
|
||||
|
||||
@@ -39,7 +39,7 @@ mod app_init {
|
||||
}
|
||||
|
||||
/// Setup plugins for the Tauri builder
|
||||
pub fn setup_plugins(builder: tauri::Builder) -> tauri::Builder {
|
||||
pub fn setup_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = builder
|
||||
.plugin(tauri_plugin_clash_verge_sysinfo::init())
|
||||
@@ -116,7 +116,7 @@ mod app_init {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static {
|
||||
pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![
|
||||
tauri_plugin_clash_verge_sysinfo::commands::get_system_info,
|
||||
tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime,
|
||||
@@ -163,6 +163,7 @@ mod app_init {
|
||||
cmd::apply_dns_config,
|
||||
cmd::set_dns_override,
|
||||
cmd::take_dns_override_notice,
|
||||
cmd::take_service_fallback_notice,
|
||||
cmd::get_dns_config_content,
|
||||
cmd::validate_dns_config,
|
||||
cmd::get_clash_logs,
|
||||
@@ -255,7 +256,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().runtime(tauri_runtime_wry::Wry::default()))
|
||||
let builder = app_init::setup_plugins(tauri::Builder::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>) {
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
#![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, 8);
|
||||
|
||||
@@ -228,27 +228,21 @@ async fn migrate_legacy_macos_service_logs(log_dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Raise existing sub-floor update intervals, once per install.
|
||||
///
|
||||
/// One-off repair, not a standing rule — the UI warns about shorter intervals but still saves
|
||||
/// them, so the marker is written even when nothing needed raising.
|
||||
/// Once per install; the marker is written even when no interval needed raising.
|
||||
pub async fn migrate_short_update_intervals() -> Result<()> {
|
||||
let marker = dirs::update_interval_migrated_path()?;
|
||||
if fs::try_exists(&marker).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Same order as every other writer holding it (lock, then draft permit): save_file runs
|
||||
// inside the closure, ahead of the optimistic check, so a racing restore can't be undone.
|
||||
// Same lock order as other writers; saving inside the closure keeps a racing restore intact.
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let min = constants::profile::MIN_UPDATE_INTERVAL;
|
||||
let raised = Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles: IProfiles| async move {
|
||||
// `IProfiles::new` normalises items to Some when it read the file, and returns a
|
||||
// bare default() when it failed. A failed load also raises zero — without this the
|
||||
// marker would burn the one shot on profiles nobody managed to load.
|
||||
// A failed profiles load (bare default) must not burn the one-shot marker.
|
||||
if profiles.items.is_none() {
|
||||
anyhow::bail!("profiles.yaml was not loaded; refusing to record the migration as done");
|
||||
}
|
||||
@@ -370,14 +364,7 @@ pub(super) async fn init_dns_config() -> Result<()> {
|
||||
"nameserver-policy".into(),
|
||||
Value::Mapping(serde_yaml_ng::Mapping::new()),
|
||||
),
|
||||
(
|
||||
"proxy-server-nameserver".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("https://doh.pub/dns-query".into()),
|
||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||
Value::String("tls://223.5.5.5".into()),
|
||||
]),
|
||||
),
|
||||
("proxy-server-nameserver".into(), Value::Sequence(vec![])),
|
||||
("direct-nameserver".into(), Value::Sequence(vec![])),
|
||||
("direct-nameserver-follow-policy".into(), Value::Bool(false)),
|
||||
(
|
||||
|
||||
@@ -132,10 +132,7 @@ 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,
|
||||
_termination: &tauri::webview::WebContentProcessTermination,
|
||||
) {
|
||||
pub fn on_web_content_process_terminated(webview: &tauri::Webview) {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use clash_verge_logging::{Type, logging};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
use tauri::{Manager as _, WebviewWindow};
|
||||
use tauri::{Manager as _, WebviewWindow, Wry};
|
||||
|
||||
#[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>, WindowState) {
|
||||
fn get_main_window_with_state() -> (Option<WebviewWindow<Wry>>, 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> {
|
||||
pub fn get_main_window() -> Option<WebviewWindow<Wry>> {
|
||||
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>) -> WindowOperationResult {
|
||||
fn hide_main_window(window: Option<&WebviewWindow<Wry>>) -> 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>) -> WindowOperationResult {
|
||||
fn activate_existing_main_window(window: Option<&WebviewWindow<Wry>>) -> 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) -> WindowOperationResult {
|
||||
fn activate_window(window: &WebviewWindow<Wry>) -> 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>) -> bool {
|
||||
pub fn is_main_window_visible(window: Option<&WebviewWindow<Wry>>) -> bool {
|
||||
window.map(|w| w.is_visible().unwrap_or(false)).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_main_window_focused(window: Option<&WebviewWindow>) -> bool {
|
||||
pub fn is_main_window_focused(window: Option<&WebviewWindow<Wry>>) -> bool {
|
||||
window.map(|w| w.is_focused().unwrap_or(false)).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_main_window_minimized(window: Option<&WebviewWindow>) -> bool {
|
||||
fn is_main_window_minimized(window: Option<&WebviewWindow<Wry>>) -> bool {
|
||||
window.map(|w| w.is_minimized().unwrap_or(false)).unwrap_or(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
},
|
||||
"rpm": {
|
||||
"depends": ["openssl", "libayatana-appindicator-gtk3"],
|
||||
"recommends": ["policycoreutils", "policycoreutils-python-utils"],
|
||||
"desktopTemplate": "./packages/linux/clash-verge.desktop",
|
||||
"provides": ["clash-verge"],
|
||||
"conflicts": ["clash-verge"],
|
||||
"obsoletes": ["clash-verge"],
|
||||
"postInstallScript": "./packages/linux/post-install.sh",
|
||||
"postInstallScript": "./packages/linux/post-install-rpm.sh",
|
||||
"preRemoveScript": "./packages/linux/pre-remove.sh"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,12 +13,24 @@ interface SortableItemProps {
|
||||
index: number
|
||||
group?: string
|
||||
disabled?: boolean
|
||||
// 用于给 tanstack virtual 列表测量当前拖动元素尺寸
|
||||
measureElement?: (element: HTMLDivElement | null) => void
|
||||
dataIndex?: number
|
||||
style?: CSSProperties
|
||||
children: ReactNode | ((props: SortableItemRenderProps) => ReactNode)
|
||||
}
|
||||
|
||||
export const SortableItem = (props: SortableItemProps) => {
|
||||
const { id, index, group, disabled, children, style } = props
|
||||
const {
|
||||
id,
|
||||
index,
|
||||
group,
|
||||
disabled,
|
||||
measureElement,
|
||||
dataIndex,
|
||||
children,
|
||||
style,
|
||||
} = props
|
||||
const { ref, handleRef, sortable, isDragging } = useSortable({
|
||||
id,
|
||||
index,
|
||||
@@ -30,8 +42,9 @@ export const SortableItem = (props: SortableItemProps) => {
|
||||
(element: HTMLDivElement | null) => {
|
||||
handleRef(element?.querySelector('[data-sortable-handle]') ?? null)
|
||||
ref(element)
|
||||
measureElement?.(element)
|
||||
},
|
||||
[handleRef, ref],
|
||||
[handleRef, ref, measureElement],
|
||||
)
|
||||
|
||||
const mergedStyle: CSSProperties = {
|
||||
@@ -53,7 +66,7 @@ export const SortableItem = (props: SortableItemProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setRef} style={mergedStyle}>
|
||||
<div ref={setRef} data-index={dataIndex} style={mergedStyle}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @eslint-react/set-state-in-effect */
|
||||
import {
|
||||
AccessTimeRounded,
|
||||
ArrowDropDown,
|
||||
ChevronRight,
|
||||
NetworkCheckRounded,
|
||||
WifiOff as SignalError,
|
||||
@@ -15,25 +16,22 @@ import {
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Chip,
|
||||
ClickAwayListener,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
type MenuProps,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Paper,
|
||||
Popper,
|
||||
Select,
|
||||
type SelectChangeEvent,
|
||||
type PopperProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
@@ -68,6 +66,40 @@ const STORAGE_KEY_SORT_TYPE = 'clash-verge-proxy-sort-type'
|
||||
const AUTO_CHECK_DEFAULT_INTERVAL_MINUTES = 5
|
||||
const AUTO_CHECK_INITIAL_DELAY_MS = 100
|
||||
const PROXY_MENU_MAX_HEIGHT = 500
|
||||
const PROXY_MENU_VIEWPORT_MARGIN = 8
|
||||
|
||||
const proxyMenuModifiers: PopperProps['modifiers'] = [
|
||||
{
|
||||
name: 'menuSize',
|
||||
enabled: true,
|
||||
phase: 'beforeRead',
|
||||
fn: ({ state }) => {
|
||||
const popper = state.elements.popper
|
||||
const anchor = state.elements.reference.getBoundingClientRect()
|
||||
const viewportHeight = popper.ownerDocument.documentElement.clientHeight
|
||||
// Use the larger side before flip runs so sizing cannot alternate with placement.
|
||||
const availableHeight =
|
||||
Math.max(anchor.top, viewportHeight - anchor.bottom) -
|
||||
PROXY_MENU_VIEWPORT_MARGIN
|
||||
popper.style.width = `${anchor.width}px`
|
||||
popper.style.setProperty(
|
||||
'--proxy-menu-max-height',
|
||||
`${Math.max(0, Math.min(PROXY_MENU_MAX_HEIGHT, availableHeight))}px`,
|
||||
)
|
||||
state.rects.popper.width = popper.offsetWidth
|
||||
state.rects.popper.height = popper.offsetHeight
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'showMenu',
|
||||
enabled: true,
|
||||
phase: 'beforeWrite',
|
||||
requires: ['computeStyles'],
|
||||
fn: ({ state }) => {
|
||||
state.styles.popper.visibility = 'visible'
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
interface ProxyOption {
|
||||
memberIndex: number
|
||||
@@ -163,34 +195,6 @@ const optionValue = (memberIndex: number, member: ResolvedProxyMember) =>
|
||||
member.kind === 'node' ? member.node.recordId : member.ref.name
|
||||
}`
|
||||
|
||||
const hasPointerCoordinates = (
|
||||
value: unknown,
|
||||
): value is { clientX: number; clientY: number } =>
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'clientX' in value &&
|
||||
typeof value.clientX === 'number' &&
|
||||
'clientY' in value &&
|
||||
typeof value.clientY === 'number'
|
||||
|
||||
const eventHitsElement = (event: unknown, element: HTMLElement | null) => {
|
||||
if (!element) return false
|
||||
|
||||
const nativeEvent =
|
||||
typeof event === 'object' && event !== null && 'nativeEvent' in event
|
||||
? event.nativeEvent
|
||||
: event
|
||||
if (!hasPointerCoordinates(nativeEvent)) return false
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
return (
|
||||
nativeEvent.clientX >= rect.left &&
|
||||
nativeEvent.clientX <= rect.right &&
|
||||
nativeEvent.clientY >= rect.top &&
|
||||
nativeEvent.clientY <= rect.bottom
|
||||
)
|
||||
}
|
||||
|
||||
const sortProxyOptions = (
|
||||
options: ProxyOption[],
|
||||
sortType: ProxySortType,
|
||||
@@ -216,58 +220,71 @@ const sortProxyOptions = (
|
||||
.map(({ option }) => option)
|
||||
}
|
||||
|
||||
interface PersistentProxySelectProps {
|
||||
interface PersistentSelectProps {
|
||||
label: string
|
||||
groupName: string
|
||||
fixed?: string
|
||||
value: string
|
||||
selectedName: string
|
||||
selectedDelay: number
|
||||
options: ProxyOption[]
|
||||
displayValue: React.ReactNode
|
||||
open: boolean
|
||||
disabled: boolean
|
||||
keepOpenRef: React.RefObject<HTMLElement | null>
|
||||
onOpen: () => void
|
||||
onClose: () => void
|
||||
onChange: (value: string) => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
renderOptions: () => React.ReactNode
|
||||
}
|
||||
|
||||
const PersistentProxySelect = ({
|
||||
const PersistentSelect = ({
|
||||
label,
|
||||
groupName,
|
||||
fixed,
|
||||
value,
|
||||
selectedName,
|
||||
selectedDelay,
|
||||
options,
|
||||
displayValue,
|
||||
open,
|
||||
disabled,
|
||||
keepOpenRef,
|
||||
onOpen,
|
||||
onClose,
|
||||
onChange,
|
||||
}: PersistentProxySelectProps) => {
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const listboxId = 'current-proxy-node-listbox'
|
||||
const labelId = 'proxy-select-label'
|
||||
const fixedProxyInUsed = selectedName === fixed
|
||||
onOpenChange,
|
||||
renderOptions,
|
||||
}: PersistentSelectProps) => {
|
||||
const id = useId()
|
||||
const labelId = `${id}-label`
|
||||
const valueId = `${id}-value`
|
||||
const listboxId = `${id}-listbox`
|
||||
const anchorRef = useRef<HTMLButtonElement>(null)
|
||||
const listRef = useRef<HTMLUListElement>(null)
|
||||
|
||||
const popperOptions = useMemo<PopperProps['popperOptions']>(
|
||||
() => ({
|
||||
onFirstUpdate: () => {
|
||||
const list = listRef.current
|
||||
const paper = list?.parentElement
|
||||
const item =
|
||||
list?.querySelector<HTMLElement>(
|
||||
'[aria-selected="true"]:not([aria-disabled="true"])',
|
||||
) ??
|
||||
list?.querySelector<HTMLElement>(
|
||||
'[role="option"]:not([aria-disabled="true"])',
|
||||
)
|
||||
if (!item || !paper) return
|
||||
|
||||
// Focusing before positioning can scroll the page instead of just the menu.
|
||||
item.focus({ preventScroll: true })
|
||||
paper.scrollTop +=
|
||||
item.getBoundingClientRect().top -
|
||||
paper.getBoundingClientRect().top -
|
||||
(paper.clientHeight - item.offsetHeight) / 2
|
||||
},
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
const closeFromKeyboard = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Tab') {
|
||||
onClose()
|
||||
onOpenChange(false)
|
||||
} else if (event.key === 'Escape') {
|
||||
onClose()
|
||||
anchorRef.current
|
||||
?.querySelector<HTMLElement>('[role="combobox"]')
|
||||
?.focus()
|
||||
onOpenChange(false)
|
||||
anchorRef.current?.focus({ preventScroll: true })
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', closeFromKeyboard)
|
||||
return () => document.removeEventListener('keydown', closeFromKeyboard)
|
||||
}, [onClose, open])
|
||||
}, [onOpenChange, open])
|
||||
|
||||
return (
|
||||
<ClickAwayListener
|
||||
@@ -277,134 +294,119 @@ const PersistentProxySelect = ({
|
||||
if (target instanceof Node && keepOpenRef.current?.contains(target)) {
|
||||
return
|
||||
}
|
||||
onClose()
|
||||
onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<FormControl ref={anchorRef} fullWidth variant="outlined" size="small">
|
||||
<InputLabel id={labelId}>{label}</InputLabel>
|
||||
<Select
|
||||
labelId={labelId}
|
||||
value={value}
|
||||
open={false}
|
||||
onOpen={onOpen}
|
||||
label={label}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
borderRadius: 1,
|
||||
'& fieldset': {
|
||||
borderColor: (theme) =>
|
||||
alpha(theme.palette.text.primary, disabled ? 0.12 : 0.23),
|
||||
},
|
||||
...(!disabled && {
|
||||
'&:hover fieldset': { borderColor: 'text.primary' },
|
||||
'&:focus-within fieldset': {
|
||||
borderColor: 'primary.main',
|
||||
borderWidth: 2,
|
||||
},
|
||||
'&:focus-within legend': { color: 'primary.main' },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
ref={anchorRef}
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-labelledby={`${labelId} ${valueId}`}
|
||||
aria-haspopup="listbox"
|
||||
aria-controls={open ? listboxId : undefined}
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
renderValue={() => (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography noWrap>{selectedName}</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(selectedDelay)}
|
||||
color={convertDelayColor(selectedDelay)}
|
||||
/>
|
||||
{fixedProxyInUsed && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
fontSize: '12px',
|
||||
top: '-3px',
|
||||
right: '25px',
|
||||
}}
|
||||
>
|
||||
📌
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
SelectDisplayProps={{
|
||||
'aria-controls': open ? listboxId : undefined,
|
||||
'aria-expanded': open ? 'true' : 'false',
|
||||
onClick={() => onOpenChange(!open)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
onOpenChange(true)
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: 40,
|
||||
px: 1.75,
|
||||
borderRadius: 'inherit',
|
||||
justifyContent: 'space-between',
|
||||
textAlign: 'left',
|
||||
color: disabled ? 'text.disabled' : 'text.primary',
|
||||
}}
|
||||
>
|
||||
<MenuItem value={value}>{selectedName}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Box id={valueId} sx={{ minWidth: 0, flex: 1 }}>
|
||||
{displayValue}
|
||||
</Box>
|
||||
<ArrowDropDown
|
||||
sx={{
|
||||
ml: 1,
|
||||
color: disabled ? 'action.disabled' : 'action.active',
|
||||
transform: open ? 'rotate(180deg)' : undefined,
|
||||
}}
|
||||
/>
|
||||
</ButtonBase>
|
||||
<Box
|
||||
component="fieldset"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: '-5px 0 0',
|
||||
m: 0,
|
||||
px: 1,
|
||||
minWidth: 0,
|
||||
border: '1px solid',
|
||||
borderRadius: 'inherit',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="legend"
|
||||
id={labelId}
|
||||
sx={{
|
||||
px: 0.5,
|
||||
typography: 'caption',
|
||||
lineHeight: '11px',
|
||||
color: disabled ? 'text.disabled' : 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
placement="bottom-start"
|
||||
modifiers={proxyMenuModifiers}
|
||||
popperOptions={popperOptions}
|
||||
sx={{
|
||||
width: anchorRef.current?.clientWidth,
|
||||
visibility: 'hidden',
|
||||
zIndex: (theme) => theme.zIndex.modal,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={8}
|
||||
sx={{ maxHeight: PROXY_MENU_MAX_HEIGHT, overflow: 'auto' }}
|
||||
sx={{
|
||||
maxHeight: `var(--proxy-menu-max-height, ${PROXY_MENU_MAX_HEIGHT}px)`,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<MenuList
|
||||
ref={listRef}
|
||||
id={listboxId}
|
||||
role="listbox"
|
||||
aria-labelledby={labelId}
|
||||
autoFocusItem
|
||||
variant="selectedMenu"
|
||||
>
|
||||
{options.map((option) => {
|
||||
const selected = option.value === value
|
||||
const isFixed = option.name === fixed
|
||||
const delay = option.disabled
|
||||
? -1
|
||||
: delayManager.getDelayFix(option.member, groupName)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
selected={selected}
|
||||
disabled={option.disabled}
|
||||
onClick={() => {
|
||||
if (!option.disabled) onChange(option.value)
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
{isFixed && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
fontSize: '12px',
|
||||
top: '-5px',
|
||||
right: '5px',
|
||||
...(!fixedProxyInUsed && {
|
||||
filter: 'grayscale(1)',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
📌
|
||||
</span>
|
||||
)}
|
||||
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
|
||||
{option.name}
|
||||
</Typography>
|
||||
{!option.disabled && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delay)}
|
||||
color={convertDelayColor(delay)}
|
||||
sx={{
|
||||
minWidth: '60px',
|
||||
height: '22px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
{open && renderOptions()}
|
||||
</MenuList>
|
||||
</Paper>
|
||||
</Popper>
|
||||
@@ -413,6 +415,124 @@ const PersistentProxySelect = ({
|
||||
)
|
||||
}
|
||||
|
||||
interface PersistentProxySelectProps
|
||||
extends Omit<PersistentSelectProps, 'displayValue' | 'renderOptions'> {
|
||||
value: string
|
||||
selectedName: string
|
||||
groupName: string
|
||||
fixed?: string
|
||||
selectedDelay: number
|
||||
options: ProxyOption[]
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const PersistentProxySelect = ({
|
||||
groupName,
|
||||
fixed,
|
||||
selectedName,
|
||||
selectedDelay,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
...props
|
||||
}: PersistentProxySelectProps) => {
|
||||
const fixedProxyInUsed = selectedName === fixed
|
||||
|
||||
return (
|
||||
<PersistentSelect
|
||||
{...props}
|
||||
displayValue={
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography noWrap>{selectedName}</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(selectedDelay)}
|
||||
color={convertDelayColor(selectedDelay)}
|
||||
/>
|
||||
{fixedProxyInUsed && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
fontSize: '12px',
|
||||
top: '-3px',
|
||||
right: '25px',
|
||||
}}
|
||||
>
|
||||
📌
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
renderOptions={() =>
|
||||
options.map((option) => {
|
||||
const selected = option.value === value
|
||||
const isFixed = option.name === fixed
|
||||
const delay = option.disabled
|
||||
? -1
|
||||
: delayManager.getDelayFix(option.member, groupName)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
selected={selected}
|
||||
disabled={option.disabled}
|
||||
onClick={() => {
|
||||
if (!option.disabled) onChange(option.value)
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
{isFixed && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
fontSize: '12px',
|
||||
top: '-5px',
|
||||
right: '5px',
|
||||
...(!fixedProxyInUsed && {
|
||||
filter: 'grayscale(1)',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
📌
|
||||
</span>
|
||||
)}
|
||||
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
|
||||
{option.name}
|
||||
</Typography>
|
||||
{!option.disabled && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delay)}
|
||||
color={convertDelayColor(delay)}
|
||||
sx={{
|
||||
minWidth: '60px',
|
||||
height: '22px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const CurrentProxyCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
@@ -497,6 +617,11 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
const [selectedGroupName, setSelectedGroupName] = useState('')
|
||||
const [openSelect, setOpenSelect] = useState<OpenSelect>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setOpenSelect(null)
|
||||
}, [mode])
|
||||
|
||||
const delayButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const delays = useGroupDelays(selectedGroupName || null)
|
||||
|
||||
@@ -621,9 +746,8 @@ export const CurrentProxyCard = () => {
|
||||
latestProxyMemberRef.current = currentOption?.member ?? null
|
||||
|
||||
const handleGroupChange = useCallback(
|
||||
(event: SelectChangeEvent<string>) => {
|
||||
(newGroupName: string) => {
|
||||
if (isGlobalMode || isDirectMode) return
|
||||
const newGroupName = event.target.value
|
||||
setSelectedGroupName(newGroupName)
|
||||
writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName)
|
||||
},
|
||||
@@ -785,24 +909,9 @@ export const CurrentProxyCard = () => {
|
||||
refreshProxy()
|
||||
})
|
||||
|
||||
const handleGroupMenuClose = useCallback<NonNullable<MenuProps['onClose']>>(
|
||||
(event, reason) => {
|
||||
if (
|
||||
reason === 'backdropClick' &&
|
||||
eventHitsElement(event, delayButtonRef.current)
|
||||
) {
|
||||
void handleCheckDelay()
|
||||
return
|
||||
}
|
||||
|
||||
setOpenSelect(null)
|
||||
},
|
||||
[handleCheckDelay],
|
||||
)
|
||||
|
||||
const proxyOptions = useMemo(
|
||||
() =>
|
||||
isDirectMode
|
||||
isDirectMode || openSelect !== 'proxy'
|
||||
? []
|
||||
: sortProxyOptions(
|
||||
unsortedProxyOptions,
|
||||
@@ -813,6 +922,7 @@ export const CurrentProxyCard = () => {
|
||||
[
|
||||
delays,
|
||||
isDirectMode,
|
||||
openSelect,
|
||||
unsortedProxyOptions,
|
||||
sortType,
|
||||
defaultLatencyTimeout,
|
||||
@@ -975,32 +1085,29 @@ export const CurrentProxyCard = () => {
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mb: 1.5 }}
|
||||
>
|
||||
<InputLabel id="proxy-group-select-label">
|
||||
{t('home.components.currentProxy.labels.group')}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="proxy-group-select-label"
|
||||
value={selectedGroupName}
|
||||
onChange={handleGroupChange}
|
||||
open={openSelect === 'group'}
|
||||
onOpen={() => setOpenSelect('group')}
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
<PersistentSelect
|
||||
label={t('home.components.currentProxy.labels.group')}
|
||||
displayValue={<Typography noWrap>{selectedGroupName}</Typography>}
|
||||
open={openSelect === 'group'}
|
||||
disabled={isGlobalMode || isDirectMode}
|
||||
MenuProps={{ onClose: handleGroupMenuClose }}
|
||||
>
|
||||
{selectableGroups.map((group) => (
|
||||
<MenuItem key={group.name} value={group.name}>
|
||||
{group.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
keepOpenRef={delayButtonRef}
|
||||
onOpenChange={(open) => setOpenSelect(open ? 'group' : null)}
|
||||
renderOptions={() =>
|
||||
selectableGroups.map((group) => (
|
||||
<MenuItem
|
||||
key={group.name}
|
||||
role="option"
|
||||
aria-selected={group.name === selectedGroupName}
|
||||
selected={group.name === selectedGroupName}
|
||||
onClick={() => handleGroupChange(group.name)}
|
||||
>
|
||||
<Typography noWrap>{group.name}</Typography>
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<PersistentProxySelect
|
||||
label={t('home.components.currentProxy.labels.proxy')}
|
||||
@@ -1013,12 +1120,11 @@ export const CurrentProxyCard = () => {
|
||||
}
|
||||
selectedName={selectedProxyName}
|
||||
selectedDelay={currentDelay}
|
||||
options={isDirectMode ? [] : proxyOptions}
|
||||
options={proxyOptions}
|
||||
open={openSelect === 'proxy'}
|
||||
disabled={isDirectMode}
|
||||
keepOpenRef={delayButtonRef}
|
||||
onOpen={() => setOpenSelect('proxy')}
|
||||
onClose={() => setOpenSelect(null)}
|
||||
onOpenChange={(open) => setOpenSelect(open ? 'proxy' : null)}
|
||||
onChange={handleProxyChange}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useNavigate } from 'react-router'
|
||||
import { useAppRefreshers } from '@/providers/app-data-context'
|
||||
import { updateProfile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
import { openExternalUrl } from '@/utils/open-external-url'
|
||||
import parseTraffic from '@/utils/parse-traffic'
|
||||
|
||||
@@ -41,7 +42,7 @@ const round = keyframes`
|
||||
// 辅助函数解析URL和过期时间
|
||||
const parseUrl = (url?: string) => {
|
||||
if (!url) return '-'
|
||||
if (url.startsWith('http')) return new URL(url).host
|
||||
if (isValidUrl(url)) return new URL(url).host
|
||||
return 'local'
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type ServiceRequest,
|
||||
type ServiceRequestReason,
|
||||
} from '@/services/service-request'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
type Remedy = 'installAndRestart' | 'restartOnly'
|
||||
|
||||
@@ -98,19 +99,25 @@ export const SysproxyPrivilegeDialog = () => {
|
||||
setStep('restarting')
|
||||
await restartCore()
|
||||
|
||||
// Verify that restart actually moved the core into the service.
|
||||
const runState = await getRuntimeState()
|
||||
if (runState.mode === 'Service') {
|
||||
// Retry the original request now that the service can apply it.
|
||||
const usingAdminFallback =
|
||||
getSystem() === 'windows' &&
|
||||
runState.mode === 'Sidecar' &&
|
||||
runState.isAdmin &&
|
||||
runState.sidecarAllowed &&
|
||||
runState.service === 'unavailable'
|
||||
if (runState.mode === 'Service' || usingAdminFallback) {
|
||||
if (restoring !== undefined) {
|
||||
setStep('applying')
|
||||
await patchVergeConfig(restoring)
|
||||
}
|
||||
showNotice.success(
|
||||
restoring === undefined
|
||||
? 'settings.sections.proxyControl.messages.installedCheckProxy'
|
||||
: 'settings.sections.proxyControl.messages.installedProxyRestored',
|
||||
)
|
||||
if (!usingAdminFallback) {
|
||||
showNotice.success(
|
||||
restoring === undefined
|
||||
? 'settings.sections.proxyControl.messages.installedCheckProxy'
|
||||
: 'settings.sections.proxyControl.messages.installedProxyRestored',
|
||||
)
|
||||
}
|
||||
close()
|
||||
} else {
|
||||
showNotice.error(
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { move } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DragDropProvider,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { type CSSProperties, type ReactNode, useRef, useState } from 'react'
|
||||
|
||||
import { SortableItem } from '../base'
|
||||
|
||||
type GroupedVirtualCategory = 'prepend' | 'original' | 'append'
|
||||
|
||||
export interface GroupedVirtualItem<T> {
|
||||
id: string
|
||||
category: GroupedVirtualCategory
|
||||
item: T
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function buildGroupedItems<T>(
|
||||
prepend: T[],
|
||||
original: T[],
|
||||
append: T[],
|
||||
getKey: (item: T) => string,
|
||||
): GroupedVirtualItem<T>[] {
|
||||
const build = (
|
||||
category: GroupedVirtualCategory,
|
||||
list: T[],
|
||||
): GroupedVirtualItem<T>[] =>
|
||||
list.map((item) => ({
|
||||
id: `${category}:${getKey(item)}`,
|
||||
category,
|
||||
item,
|
||||
}))
|
||||
|
||||
return [
|
||||
...build('prepend', prepend),
|
||||
...build('original', original),
|
||||
...build('append', append),
|
||||
]
|
||||
}
|
||||
|
||||
interface GroupedVirtualListProps<T> {
|
||||
items: GroupedVirtualItem<T>[]
|
||||
estimateSize?: number
|
||||
renderItem: (entry: GroupedVirtualItem<T>) => ReactNode
|
||||
onReorder: (
|
||||
category: 'prepend' | 'append',
|
||||
activeIndex: number,
|
||||
overIndex: number,
|
||||
) => void
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于编辑规则/节点/代理组功能的 prepend/original/append 分组虚拟化列表
|
||||
*/
|
||||
export function GroupedVirtualList<T>(props: GroupedVirtualListProps<T>) {
|
||||
const {
|
||||
items: itemsProp,
|
||||
estimateSize = 56,
|
||||
renderItem,
|
||||
onReorder,
|
||||
style,
|
||||
} = props
|
||||
const [items, setItems] = useState(itemsProp)
|
||||
const [prevItemsProp, setPrevItemsProp] = useState(itemsProp)
|
||||
const snapshotRef = useRef(itemsProp)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
if (itemsProp !== prevItemsProp) {
|
||||
setPrevItemsProp(itemsProp)
|
||||
setItems(itemsProp)
|
||||
}
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: items.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => estimateSize,
|
||||
overscan: 15,
|
||||
gap: 8,
|
||||
getItemKey: (index) => items[index]?.id ?? index,
|
||||
})
|
||||
|
||||
const onDragStart = () => {
|
||||
snapshotRef.current = items
|
||||
}
|
||||
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (!source || !target) return
|
||||
|
||||
const sourceEntry = items.find((entry) => entry.id === source.id)
|
||||
const targetEntry = items.find((entry) => entry.id === target.id)
|
||||
if (
|
||||
!sourceEntry ||
|
||||
!targetEntry ||
|
||||
sourceEntry.category !== targetEntry.category
|
||||
) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
setItems((current) => move(current, event))
|
||||
}
|
||||
|
||||
const onDragEnd = (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
|
||||
if (event.canceled) {
|
||||
setItems(snapshotRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const snapshot = snapshotRef.current
|
||||
const sourceEntry = snapshot.find((entry) => entry.id === source.id)
|
||||
if (!sourceEntry) return
|
||||
|
||||
const { category } = sourceEntry
|
||||
if (category !== 'prepend' && category !== 'append') return
|
||||
|
||||
const activeIndex = snapshot
|
||||
.filter((entry) => entry.category === category)
|
||||
.findIndex((entry) => entry.id === source.id)
|
||||
const overIndex = items
|
||||
.filter((entry) => entry.category === category)
|
||||
.findIndex((entry) => entry.id === source.id)
|
||||
if (activeIndex < 0 || overIndex < 0 || activeIndex === overIndex) return
|
||||
|
||||
onReorder(category, activeIndex, overIndex)
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<div ref={scrollRef} style={{ ...style, overflow: 'auto' }}>
|
||||
<div
|
||||
style={{
|
||||
height: virtualizer.getTotalSize(),
|
||||
position: 'relative',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((vi) => {
|
||||
const entry = items[vi.index]
|
||||
if (!entry) return null
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
key={vi.key}
|
||||
id={entry.id}
|
||||
index={vi.index}
|
||||
disabled={entry.category === 'original'}
|
||||
measureElement={virtualizer.measureElement}
|
||||
dataIndex={vi.index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${vi.start}px`,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{renderItem(entry)}
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</DragDropProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -43,13 +35,7 @@ import {
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { BaseSearchBox, MonacoEditor, Switch } from '@/components/base'
|
||||
import { GroupItem } from '@/components/profile/group-item'
|
||||
import {
|
||||
getNetworkInterfaces,
|
||||
@@ -63,6 +49,12 @@ import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import { MONACO_FONT_FAMILY } from '@/utils/font-family'
|
||||
import { parseYamlSafe } from '@/utils/yaml'
|
||||
|
||||
import {
|
||||
buildGroupedItems,
|
||||
type GroupedVirtualItem,
|
||||
GroupedVirtualList,
|
||||
} from './grouped-virtual-list'
|
||||
|
||||
interface Props {
|
||||
proxiesUid: string
|
||||
mergeUid: string
|
||||
@@ -203,106 +195,71 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
[appendSeq, match],
|
||||
)
|
||||
|
||||
const renderItem = (index: number): React.ReactNode => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
buildGroupedItems(
|
||||
filteredPrependSeq,
|
||||
filteredGroupList,
|
||||
filteredAppendSeq,
|
||||
(group) => group.name,
|
||||
),
|
||||
[filteredPrependSeq, filteredGroupList, filteredAppendSeq],
|
||||
)
|
||||
|
||||
const renderItem = (entry: GroupedVirtualItem<IProxyGroupConfig>) => {
|
||||
const { category, item } = entry
|
||||
|
||||
if (category === 'original') {
|
||||
const isDeleted = deleteSeq.includes(item.name)
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`prepend:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<GroupItem
|
||||
type="prepend"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(
|
||||
prependSeq.filter((v) => v.name !== item.name),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredGroupList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<GroupItem
|
||||
key={filteredGroupList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
<GroupItem
|
||||
type={isDeleted ? 'delete' : 'original'}
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
if (isDeleted) {
|
||||
setDeleteSeq(deleteSeq.filter((v) => v !== item.name))
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, item.name])
|
||||
}
|
||||
group={filteredGroupList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredGroupList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredGroupList[newIndex].name,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredGroupList[newIndex].name,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
}
|
||||
|
||||
if (category === 'prepend') {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`append:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<GroupItem
|
||||
type="append"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
<GroupItem
|
||||
type="prepend"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(prependSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupItem
|
||||
type="append"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
const onReorder = (
|
||||
category: 'prepend' | 'append',
|
||||
activeIndex: number,
|
||||
overIndex: number,
|
||||
) => {
|
||||
const list = category === 'prepend' ? prependSeq : appendSeq
|
||||
const filtered =
|
||||
category === 'prepend' ? filteredPrependSeq : filteredAppendSeq
|
||||
const setList = category === 'prepend' ? setPrependSeq : setAppendSeq
|
||||
const activeRealIndex = findRealIndex(list, filtered, activeIndex)
|
||||
const overRealIndex = findRealIndex(list, filtered, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
@@ -311,55 +268,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(arrayMove(appendSeq, activeRealIndex, overRealIndex))
|
||||
setList(arrayMove(list, activeRealIndex, overRealIndex))
|
||||
}
|
||||
|
||||
// prevent drag over if source and target are in different groups
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
const fetchContent = useCallback(async () => {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
const data = await readProfileFile(property)
|
||||
@@ -1152,22 +1063,12 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredGroupList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
<GroupedVirtualList
|
||||
items={items}
|
||||
renderItem={renderItem}
|
||||
onReorder={onReorder}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -43,6 +43,7 @@ import { showNotice } from '@/services/notice-service'
|
||||
import { useLoadingCache, useSetLoadingCache } from '@/services/states'
|
||||
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
import { openExternalUrl } from '@/utils/open-external-url'
|
||||
import parseTraffic from '@/utils/parse-traffic'
|
||||
|
||||
@@ -965,9 +966,7 @@ export const ProfileItem = memo(ProfileItemBase)
|
||||
|
||||
function parseUrl(url?: string) {
|
||||
if (!url) return ''
|
||||
const regex = /https?:\/\/(.+?)\//
|
||||
const result = url.match(regex)
|
||||
return result ? result[1] : 'local file'
|
||||
return isValidUrl(url) ? new URL(url).host : 'local file'
|
||||
}
|
||||
|
||||
function parseExpire(expire?: number) {
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -35,12 +27,7 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { BaseSearchBox, MonacoEditor } from '@/components/base'
|
||||
import { ProxyItem } from '@/components/profile/proxy-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
@@ -50,6 +37,12 @@ import { MONACO_FONT_FAMILY } from '@/utils/font-family'
|
||||
import parseUri from '@/utils/uri-parser'
|
||||
import { parseYamlSafe } from '@/utils/yaml'
|
||||
|
||||
import {
|
||||
buildGroupedItems,
|
||||
type GroupedVirtualItem,
|
||||
GroupedVirtualList,
|
||||
} from './grouped-virtual-list'
|
||||
|
||||
interface Props {
|
||||
profileUid: string
|
||||
property: string
|
||||
@@ -107,106 +100,71 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
[appendSeq, match],
|
||||
)
|
||||
|
||||
const renderItem = (index: number): React.ReactNode => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
buildGroupedItems(
|
||||
filteredPrependSeq,
|
||||
filteredProxyList,
|
||||
filteredAppendSeq,
|
||||
(proxy) => proxy.name,
|
||||
),
|
||||
[filteredPrependSeq, filteredProxyList, filteredAppendSeq],
|
||||
)
|
||||
|
||||
const renderItem = (entry: GroupedVirtualItem<IProxyConfig>) => {
|
||||
const { category, item } = entry
|
||||
|
||||
if (category === 'original') {
|
||||
const isDeleted = deleteSeq.includes(item.name)
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`prepend:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<ProxyItem
|
||||
type="prepend"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(
|
||||
prependSeq.filter((v) => v.name !== item.name),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredProxyList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<ProxyItem
|
||||
key={filteredProxyList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
<ProxyItem
|
||||
type={isDeleted ? 'delete' : 'original'}
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
if (isDeleted) {
|
||||
setDeleteSeq(deleteSeq.filter((v) => v !== item.name))
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, item.name])
|
||||
}
|
||||
proxy={filteredProxyList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredProxyList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredProxyList[newIndex].name,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredProxyList[newIndex].name,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
}
|
||||
|
||||
if (category === 'prepend') {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`append:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<ProxyItem
|
||||
type="append"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
<ProxyItem
|
||||
type="prepend"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(prependSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ProxyItem
|
||||
type="append"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
const onReorder = (
|
||||
category: 'prepend' | 'append',
|
||||
activeIndex: number,
|
||||
overIndex: number,
|
||||
) => {
|
||||
const list = category === 'prepend' ? prependSeq : appendSeq
|
||||
const filtered =
|
||||
category === 'prepend' ? filteredPrependSeq : filteredAppendSeq
|
||||
const setList = category === 'prepend' ? setPrependSeq : setAppendSeq
|
||||
const activeRealIndex = findRealIndex(list, filtered, activeIndex)
|
||||
const overRealIndex = findRealIndex(list, filtered, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
@@ -215,52 +173,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeRealIndex, overRealIndex))
|
||||
setList(arrayMove(list, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(arrayMove(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
// 优化:异步分片解析,避免主线程阻塞,解析完成后批量setState
|
||||
const handleParseAsync = (cb: (proxies: IProxyConfig[]) => void) => {
|
||||
const proxies: IProxyConfig[] = []
|
||||
@@ -522,22 +437,12 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredProxyList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
<GroupedVirtualList
|
||||
items={items}
|
||||
renderItem={renderItem}
|
||||
onReorder={onReorder}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -36,13 +29,7 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { BaseSearchBox, MonacoEditor, Switch } from '@/components/base'
|
||||
import { RuleItem } from '@/components/profile/rule-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
@@ -54,6 +41,12 @@ import getSystem from '@/utils/get-system'
|
||||
import { isValidIpCidr } from '@/utils/network'
|
||||
import { parseYamlSafe } from '@/utils/yaml'
|
||||
|
||||
import {
|
||||
buildGroupedItems,
|
||||
type GroupedVirtualItem,
|
||||
GroupedVirtualList,
|
||||
} from './grouped-virtual-list'
|
||||
|
||||
interface Props {
|
||||
groupsUid: string
|
||||
mergeUid: string
|
||||
@@ -302,117 +295,83 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
[appendSeq, match],
|
||||
)
|
||||
|
||||
const renderItem = (index: number): React.ReactNode => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
buildGroupedItems(
|
||||
filteredPrependSeq,
|
||||
filteredRuleList,
|
||||
filteredAppendSeq,
|
||||
(rule) => rule,
|
||||
),
|
||||
[filteredPrependSeq, filteredRuleList, filteredAppendSeq],
|
||||
)
|
||||
|
||||
const renderItem = (entry: GroupedVirtualItem<string>) => {
|
||||
const { category, item } = entry
|
||||
|
||||
if (category === 'original') {
|
||||
const isDeleted = deleteSeq.includes(item)
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item}
|
||||
id={`prepend:${item}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<RuleItem
|
||||
type="prepend"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
onAppend={() => {
|
||||
setAppendSeq((prev) =>
|
||||
prev.includes(item) ? prev : [...prev, item],
|
||||
)
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredRuleList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<RuleItem
|
||||
key={filteredRuleList[newIndex]}
|
||||
type={
|
||||
deleteSeq.includes(filteredRuleList[newIndex])
|
||||
? 'delete'
|
||||
: 'original'
|
||||
<RuleItem
|
||||
type={isDeleted ? 'delete' : 'original'}
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
if (isDeleted) {
|
||||
setDeleteSeq(deleteSeq.filter((v) => v !== item))
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, item])
|
||||
}
|
||||
ruleRaw={filteredRuleList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredRuleList[newIndex]),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, filteredRuleList[newIndex]])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
}
|
||||
|
||||
if (category === 'prepend') {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item}
|
||||
id={`append:${item}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<RuleItem
|
||||
type="append"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
onPrepend={() => {
|
||||
setPrependSeq((prev) =>
|
||||
prev.includes(item) ? prev : [...prev, item],
|
||||
)
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
<RuleItem
|
||||
type="prepend"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
onAppend={() => {
|
||||
setAppendSeq((prev) =>
|
||||
prev.includes(item) ? prev : [...prev, item],
|
||||
)
|
||||
})}
|
||||
</>
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RuleItem
|
||||
type="append"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
onPrepend={() => {
|
||||
setPrependSeq((prev) =>
|
||||
prev.includes(item) ? prev : [...prev, item],
|
||||
)
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const reorder = (list: string[], startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(list)
|
||||
const [removed] = result.splice(startIndex, 1)
|
||||
result.splice(endIndex, 0, removed)
|
||||
return result
|
||||
}
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
const onReorder = (
|
||||
category: 'prepend' | 'append',
|
||||
activeIndex: number,
|
||||
overIndex: number,
|
||||
) => {
|
||||
const list = category === 'prepend' ? prependSeq : appendSeq
|
||||
const filtered =
|
||||
category === 'prepend' ? filteredPrependSeq : filteredAppendSeq
|
||||
const setList = category === 'prepend' ? setPrependSeq : setAppendSeq
|
||||
const activeRealIndex = findRealIndex(list, filtered, activeIndex)
|
||||
const overRealIndex = findRealIndex(list, filtered, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
@@ -421,52 +380,9 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(reorder(prependSeq, activeRealIndex, overRealIndex))
|
||||
setList(arrayMove(list, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(reorder(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
const fetchContent = useCallback(async () => {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
const data = await readProfileFile(property)
|
||||
@@ -854,22 +770,12 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredRuleList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
<GroupedVirtualList
|
||||
items={items}
|
||||
renderItem={renderItem}
|
||||
onReorder={onReorder}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -17,6 +17,8 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager from '@/services/delay'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
|
||||
import { BaseSearchBox, type SearchState } from '../base'
|
||||
|
||||
@@ -174,6 +176,10 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
if (testUrl?.trim() && textState !== 'filter') {
|
||||
onHeadState({ textState: 'url' })
|
||||
}
|
||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||
return
|
||||
}
|
||||
onCheckDelay()
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -18,7 +18,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import { BaseSearchBox } from '@/components/base'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager from '@/services/delay'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
|
||||
import type { ProxySortType } from './use-filter-sort'
|
||||
import type { HeadState } from './use-head-state'
|
||||
@@ -95,6 +97,10 @@ export const ProxyHead = ({
|
||||
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`)
|
||||
onHeadState({ textState: 'url' })
|
||||
}
|
||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||
return
|
||||
}
|
||||
onCheckDelay()
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -171,11 +171,7 @@ const DEFAULT_DNS_CONFIG = {
|
||||
],
|
||||
fallback: [],
|
||||
'nameserver-policy': {},
|
||||
'proxy-server-nameserver': [
|
||||
'https://doh.pub/dns-query',
|
||||
'https://dns.alidns.com/dns-query',
|
||||
'tls://223.5.5.5',
|
||||
],
|
||||
'proxy-server-nameserver': [],
|
||||
'direct-nameserver': [],
|
||||
'direct-nameserver-follow-policy': false,
|
||||
'fallback-filter': {
|
||||
@@ -340,8 +336,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
listen: values.listen,
|
||||
'enhanced-mode': values.enhancedMode,
|
||||
'fake-ip-range': values.fakeIpRange,
|
||||
'fake-ip-range6':
|
||||
values.fakeIpRange6 || DEFAULT_DNS_CONFIG['fake-ip-range6'],
|
||||
'fake-ip-range6': values.fakeIpRange6,
|
||||
'fake-ip-filter-mode': values.fakeIpFilterMode,
|
||||
'prefer-h3': values.preferH3,
|
||||
'respect-rules': values.respectRules,
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "تم تثبيت الخدمة بنجاح",
|
||||
"sidecarFallback": "تعذّر على الخدمة تشغيل النواة بسبب أذونات المسار. تم التبديل إلى وضع المسؤول لمواصلة التشغيل.",
|
||||
"uninstallSuccess": "تم إلغاء تثبيت الخدمة بنجاح"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Service erfolgreich installiert",
|
||||
"sidecarFallback": "Der Dienst konnte den Kern aufgrund der Pfadberechtigungen nicht starten. Die Anwendung läuft im Administratormodus weiter.",
|
||||
"uninstallSuccess": "Service erfolgreich deinstalliert"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Service Installed Successfully",
|
||||
"sidecarFallback": "The service could not start the core due to path permissions. Switched to administrator mode to continue running.",
|
||||
"uninstallSuccess": "Service Uninstalled Successfully"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Servicio instalado con éxito",
|
||||
"sidecarFallback": "El servicio no pudo iniciar el núcleo debido a los permisos de la ruta. Se ha cambiado al modo administrador para continuar.",
|
||||
"uninstallSuccess": "Servicio desinstalado con éxito"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "سرویس با موفقیت نصب شد",
|
||||
"sidecarFallback": "سرویس بهدلیل مجوزهای مسیر نتوانست هسته را اجرا کند. برای ادامهٔ اجرا به حالت مدیر تغییر یافت.",
|
||||
"uninstallSuccess": "سرویس با موفقیت حذف نصب شد"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Layanan Berhasil Diinstal",
|
||||
"sidecarFallback": "Layanan tidak dapat memulai inti karena izin jalur. Beralih ke mode administrator untuk melanjutkan.",
|
||||
"uninstallSuccess": "Layanan Berhasil Dicopot"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "サービスのインストールに成功しました。",
|
||||
"sidecarFallback": "パスの権限によりサービスがコアを起動できなかったため、管理者モードに切り替えて動作を継続しています。",
|
||||
"uninstallSuccess": "サービスのアンインストールに成功しました。"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} 업데이트 성공",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "서비스가 설치되었습니다",
|
||||
"sidecarFallback": "경로 권한 문제로 서비스가 코어를 시작하지 못해 관리자 모드로 전환하여 계속 실행합니다.",
|
||||
"uninstallSuccess": "서비스가 제거되었습니다"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} успешно обновлён",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Служба успешно установлена",
|
||||
"sidecarFallback": "Служба не смогла запустить ядро из-за прав доступа к пути. Работа продолжена в режиме администратора.",
|
||||
"uninstallSuccess": "Служба успешно удалена"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -17,18 +17,18 @@
|
||||
"type": "Tip"
|
||||
},
|
||||
"order": {
|
||||
"default": "Default",
|
||||
"default": "Varsayılan",
|
||||
"uploadSpeed": "Yükleme Hızı",
|
||||
"downloadSpeed": "İndirme Hızı"
|
||||
},
|
||||
"actions": {
|
||||
"active": "Active",
|
||||
"closed": "Closed",
|
||||
"active": "Etkin",
|
||||
"closed": "Kapatılan",
|
||||
"closeConnection": "Bağlantıyı Kapat"
|
||||
},
|
||||
"columnManager": {
|
||||
"title": "Sütunlar",
|
||||
"dragHandle": "Drag handle"
|
||||
"dragHandle": "Sürükleme tutamacı"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"fields": {
|
||||
"coreVersion": "Çekirdek Sürümü",
|
||||
"systemProxyAddress": "Sistem Vekil Adresi",
|
||||
"mixedPort": "Mixed Port",
|
||||
"mixedPort": "Karma Port",
|
||||
"uptime": "Çalışma Süresi",
|
||||
"rulesCount": "Kural Sayısı"
|
||||
}
|
||||
@@ -127,7 +127,7 @@
|
||||
"download": "İndirme"
|
||||
},
|
||||
"patterns": {
|
||||
"minutes": "{{time}} Minutes"
|
||||
"minutes": "{{time}} Dakika"
|
||||
},
|
||||
"unknownTime": "Bilinmeyen zaman",
|
||||
"chartStyles": {
|
||||
@@ -146,9 +146,9 @@
|
||||
"direct": "Doğrudan Mod"
|
||||
},
|
||||
"descriptions": {
|
||||
"rule": "Automatically choose proxies according to the rule set.",
|
||||
"global": "Forward all network requests through the selected proxy.",
|
||||
"direct": "Bypass the proxy and connect to the internet directly."
|
||||
"rule": "Vekilleri kural kümesine göre otomatik olarak seçer.",
|
||||
"global": "Tüm ağ isteklerini seçili vekil üzerinden iletir.",
|
||||
"direct": "Vekili atlar ve internete doğrudan bağlanır."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -12,23 +12,23 @@
|
||||
"settings": "Ayarlar"
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Menu reorder mode",
|
||||
"restoreDefaultOrder": "Restore default order",
|
||||
"unlock": "Unlock menu order",
|
||||
"lock": "Lock menu order",
|
||||
"collapseNavBar": "Collapse navigation bar",
|
||||
"expandNavBar": "Expand navigation bar"
|
||||
"reorderMode": "Menü sıralama modu",
|
||||
"restoreDefaultOrder": "Varsayılan sırayı geri yükle",
|
||||
"unlock": "Menü sırasının kilidini aç",
|
||||
"lock": "Menü sırasını kilitle",
|
||||
"collapseNavBar": "Gezinme çubuğunu daralt",
|
||||
"expandNavBar": "Gezinme çubuğunu genişlet"
|
||||
}
|
||||
},
|
||||
"serviceMigration": {
|
||||
"title": "Service update required",
|
||||
"message": "An older Clash Verge service installation was detected. Reinstall it to use the current secure IPC protocol, or continue with Sidecar for this session.",
|
||||
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
|
||||
"unavailableMessage": "The required Clash Verge service is not available. Install or repair it, or continue with Sidecar for this session.",
|
||||
"reinstall": "Reinstall service",
|
||||
"repair": "Repair service",
|
||||
"continueSidecar": "Continue with Sidecar",
|
||||
"success": "The Clash Verge service was updated successfully",
|
||||
"title": "Hizmet güncellemesi gerekli",
|
||||
"message": "Eski bir Clash Verge hizmet kurulumu algılandı. Güncel güvenli IPC protokolünü kullanmak için hizmeti yeniden kurun ya da bu oturumda Sidecar ile devam edin.",
|
||||
"checkingMessage": "Sistem hizmetinin durumu ve geçerli çalışma modu denetleniyor. Lütfen bekleyin…",
|
||||
"unavailableMessage": "Gerekli Clash Verge hizmeti kullanılamıyor. Hizmeti kurun veya onarın ya da bu oturumda Sidecar ile devam edin.",
|
||||
"reinstall": "Hizmeti yeniden kur",
|
||||
"repair": "Hizmeti onar",
|
||||
"continueSidecar": "Sidecar ile devam et",
|
||||
"success": "Clash Verge hizmeti başarıyla güncellendi",
|
||||
"errors": {
|
||||
"actionFailed": "Hizmet işlemi başarısız oldu",
|
||||
"stateRefreshFailed": "Hizmet durumu yenilenemedi",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"title": "Günlükler"
|
||||
},
|
||||
"actions": {
|
||||
"showDescending": "Newest first",
|
||||
"showAscending": "Oldest first"
|
||||
"showDescending": "Önce en yeni",
|
||||
"showAscending": "Önce en eski"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"feedback": {
|
||||
"errors": {
|
||||
"invalidUrl": "Invalid profile URL. Please enter a URL starting with http:// or https://",
|
||||
"invalidUrl": "Geçersiz profil URL'si. Lütfen http:// veya https:// ile başlayan bir URL girin",
|
||||
"onlyYaml": "Yalnızca YAML Dosyaları Desteklenir",
|
||||
"createFailed": "Profil oluşturulamadı",
|
||||
"deleteFailed": "Profil silinemedi",
|
||||
@@ -42,16 +42,16 @@
|
||||
"notifications": {
|
||||
"importRetry": "İçe aktarma başarısız oldu, Clash vekil ile yeniden deneniyor...",
|
||||
"importFail": "Clash vekil ile bile içe aktarma başarısız oldu",
|
||||
"importNeedsRefresh": "Profile imported but may need manual refresh",
|
||||
"importSuccess": "Profile imported successfully, please restart if not visible",
|
||||
"importNeedsRefresh": "Profil içe aktarıldı ancak elle yenilemek gerekebilir",
|
||||
"importSuccess": "Profil başarıyla içe aktarıldı, görünmüyorsa lütfen yeniden başlatın",
|
||||
"profileSwitched": "Profil Değiştirildi",
|
||||
"profileReactivated": "Profil Yeniden Etkinleştirildi",
|
||||
"switchBusy": "Profil değiştirme işlemi zaten devam ediyor",
|
||||
"batchDeleted": "Seçili profiller başarıyla silindi"
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
"forceRefreshCompleted": "Zorunlu yenileme tamamlandı",
|
||||
"emergencyRefreshFailed": "Acil yenileme başarısız oldu: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
"forceRefreshStaleData": "Profil verileri kullanılamıyor; zorla yenilemek için tıklayın"
|
||||
@@ -71,7 +71,7 @@
|
||||
"menu": {
|
||||
"home": "Ana Sayfa",
|
||||
"select": "Seç",
|
||||
"shareQrCode": "Share QR Code",
|
||||
"shareQrCode": "QR Kodu Paylaş",
|
||||
"editInfo": "Bilgileri Düzenle",
|
||||
"editFile": "Dosyayı Düzenle",
|
||||
"editRules": "Kuralları Düzenle",
|
||||
@@ -81,22 +81,22 @@
|
||||
"extendScript": "Betik Genişletme",
|
||||
"openFile": "Dosyayı Aç",
|
||||
"update": "Güncelle",
|
||||
"updateViaProxy": "Update via proxy"
|
||||
"updateViaProxy": "Vekil üzerinden güncelle"
|
||||
},
|
||||
"more": {
|
||||
"global": {
|
||||
"merge": "Global Extend Config",
|
||||
"script": "Global Extend Script"
|
||||
"merge": "Küresel Yapılandırma Genişletme",
|
||||
"script": "Küresel Betik Genişletme"
|
||||
},
|
||||
"chips": {
|
||||
"merge": "Merge",
|
||||
"script": "Script"
|
||||
"merge": "Birleştirme",
|
||||
"script": "Betik"
|
||||
}
|
||||
},
|
||||
"profileItem": {
|
||||
"tooltips": {
|
||||
"showLast": "Click to show last update time",
|
||||
"showNext": "Click to show next update"
|
||||
"showLast": "Son güncelleme zamanını göstermek için tıklayın",
|
||||
"showNext": "Sonraki güncellemeyi göstermek için tıklayın"
|
||||
},
|
||||
"status": {
|
||||
"lastUpdateFailed": "Son güncelleme başarısız oldu",
|
||||
@@ -118,12 +118,12 @@
|
||||
"description": "Açıklamalar",
|
||||
"subscriptionUrl": "Abonelik URL'si",
|
||||
"userAgent": "Kullanıcı Aracısı",
|
||||
"httpTimeout": "HTTP Request Timeout",
|
||||
"httpTimeout": "HTTP İsteği Zaman Aşımı",
|
||||
"updateInterval": "Güncelleme Aralığı",
|
||||
"useSystemProxy": "Sistem Vekil'ini Kullan",
|
||||
"useClashProxy": "Clash Vekil'ini Kullan",
|
||||
"acceptInvalidCerts": "Geçersiz Sertifikalara İzin Ver (Tehlikeli)",
|
||||
"allowAutoUpdate": "Allow Auto Update"
|
||||
"allowAutoUpdate": "Otomatik Güncellemeye İzin Ver"
|
||||
},
|
||||
"types": {
|
||||
"remote": "Uzak",
|
||||
@@ -206,7 +206,7 @@
|
||||
"title": "Betik Konsolu"
|
||||
},
|
||||
"qrViewer": {
|
||||
"title": "Subscription QR Code"
|
||||
"title": "Abonelik QR Kodu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-24
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"page": {
|
||||
"modes": {
|
||||
"rule": "Rule",
|
||||
"global": "Global",
|
||||
"direct": "Direct"
|
||||
"rule": "Kural",
|
||||
"global": "Küresel",
|
||||
"direct": "Doğrudan"
|
||||
},
|
||||
"actions": {
|
||||
"toggleChain": "Zincir Proxy",
|
||||
"connect": "Connect",
|
||||
"disconnect": "Disconnect",
|
||||
"connecting": "Connecting...",
|
||||
"clearChainConfig": "Delete Chain Config"
|
||||
"connect": "Bağlan",
|
||||
"disconnect": "Bağlantıyı Kes",
|
||||
"connecting": "Bağlanıyor...",
|
||||
"clearChainConfig": "Zincir Yapılandırmasını Sil"
|
||||
},
|
||||
"provider": {
|
||||
"title": "Vekil Sağlayıcısı",
|
||||
@@ -20,8 +20,8 @@
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"title": "Proxy Rules",
|
||||
"select": "Select Rules"
|
||||
"title": "Vekil Kuralları",
|
||||
"select": "Kural Seç"
|
||||
},
|
||||
"labels": {
|
||||
"proxyCount": "Vekil Sayısı",
|
||||
@@ -68,15 +68,15 @@
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"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",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
"connectFailed": "Failed to connect to proxy chain",
|
||||
"disconnectFailed": "Failed to disconnect from proxy chain",
|
||||
"duplicateNode": "Proxy node already exists in 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.",
|
||||
"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",
|
||||
"minimumNodesHint": "Zincir vekil en az 2 düğüm gerektirir. Lütfen bir düğüm daha ekleyin.",
|
||||
"connectFailed": "Vekil zincirine bağlanılamadı",
|
||||
"disconnectFailed": "Vekil zinciriyle bağlantı kesilemedi",
|
||||
"duplicateNode": "Vekil düğümü zincirde zaten var",
|
||||
"entryNode": "Giriş",
|
||||
"exitNode": "Çıkış"
|
||||
},
|
||||
@@ -85,17 +85,20 @@
|
||||
},
|
||||
"title": {
|
||||
"default": "Vekil Grupları",
|
||||
"chainMode": "Proxy Chain Mode"
|
||||
"chainMode": "Vekil Zinciri Modu"
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL'si http:// veya https:// ile başlamalıdır"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
"updateFailed": "Failed to update {{name}}: {{message}}",
|
||||
"genericError": "Update failed: {{message}}",
|
||||
"none": "No providers available to update",
|
||||
"allUpdated": "All providers updated successfully"
|
||||
"updateSuccess": "{{name}} başarıyla güncellendi",
|
||||
"updateFailed": "{{name}} güncellenemedi: {{message}}",
|
||||
"genericError": "Güncelleme başarısız oldu: {{message}}",
|
||||
"none": "Güncellenecek sağlayıcı yok",
|
||||
"allUpdated": "Tüm sağlayıcılar başarıyla güncellendi"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
"updateFailed": "Failed to update {{name}}: {{message}}",
|
||||
"genericError": "Update failed: {{message}}",
|
||||
"none": "No providers available to update",
|
||||
"allUpdated": "All providers updated successfully"
|
||||
"updateSuccess": "{{name}} başarıyla güncellendi",
|
||||
"updateFailed": "{{name}} güncellenemedi: {{message}}",
|
||||
"genericError": "Güncelleme başarısız oldu: {{message}}",
|
||||
"none": "Güncellenecek sağlayıcı yok",
|
||||
"allUpdated": "Tüm sağlayıcılar başarıyla güncellendi"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"actions": {
|
||||
"manual": "Kılavuz",
|
||||
"telegram": "Telegram Kanalı",
|
||||
"github": "Github Repo"
|
||||
"github": "GitHub Deposu"
|
||||
},
|
||||
"actionsGroupLabel": "Ayar bağlantıları",
|
||||
"title": "Ayarlar"
|
||||
@@ -24,8 +24,8 @@
|
||||
},
|
||||
"notifications": {
|
||||
"tunMode": {
|
||||
"autoDisabled": "TUN Mode automatically disabled due to service unavailable",
|
||||
"autoDisableFailed": "Failed to disable TUN Mode automatically"
|
||||
"autoDisabled": "Hizmet kullanılamadığı için TUN Modu otomatik olarak devre dışı bırakıldı",
|
||||
"autoDisableFailed": "TUN Modu otomatik olarak devre dışı bırakılamadı"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -33,11 +33,11 @@
|
||||
"tooltips": {
|
||||
"systemProxy": "İşletim sisteminin vekil ayarlarını değiştirmek için etkinleştirin. Etkinleştirme başarısız olursa, işletim sisteminin proxy ayarlarını manuel olarak değiştirin",
|
||||
"tunMode": "Tun (Sanal Ağ Kartı) modu: Tüm sistem trafiğini yakalar, etkinleştirildiğinde sistem vekil'ini etkinleştirmeye gerek yoktur.",
|
||||
"tunUnavailable": "TUN requires Service Mode or Admin Mode"
|
||||
"tunUnavailable": "TUN, Hizmet Modu veya Yönetici Modu gerektirir"
|
||||
},
|
||||
"actions": {
|
||||
"installService": "Hizmeti Kur",
|
||||
"uninstallService": "Uninstall Service",
|
||||
"uninstallService": "Hizmeti Kaldır",
|
||||
"switchToServiceMode": "Hizmet moduna geç"
|
||||
},
|
||||
"fields": {
|
||||
@@ -92,9 +92,9 @@
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
"light": "Açık",
|
||||
"dark": "Koyu",
|
||||
"system": "Sistem"
|
||||
},
|
||||
"clash": {
|
||||
"title": "Clash Ayarı",
|
||||
@@ -144,11 +144,11 @@
|
||||
},
|
||||
"options": {
|
||||
"logLevel": {
|
||||
"debug": "Debug",
|
||||
"info": "Info",
|
||||
"warning": "Warn",
|
||||
"error": "Error",
|
||||
"silent": "Silent"
|
||||
"debug": "Hata Ayıklama",
|
||||
"info": "Bilgi",
|
||||
"warning": "Uyarı",
|
||||
"error": "Hata",
|
||||
"silent": "Sessiz"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,11 +187,11 @@
|
||||
"liteMode": "GUI'yi kapatın ve yalnızca çekirdeği çalışır durumda tutun"
|
||||
},
|
||||
"actions": {
|
||||
"copyVersion": "Copy Version"
|
||||
"copyVersion": "Sürümü Kopyala"
|
||||
},
|
||||
"notifications": {
|
||||
"latestVersion": "Şu Anda En Son Sürümdesiniz",
|
||||
"versionCopied": "Version copied to clipboard"
|
||||
"versionCopied": "Sürüm panoya kopyalandı"
|
||||
},
|
||||
"fields": {
|
||||
"backupSetting": "Yedekleme Ayarı",
|
||||
@@ -222,27 +222,27 @@
|
||||
"cssInjection": "CSS Enjeksiyonu"
|
||||
},
|
||||
"actions": {
|
||||
"editCss": "Edit CSS"
|
||||
"editCss": "CSS'i Düzenle"
|
||||
},
|
||||
"dialogs": {
|
||||
"editCssTitle": "Edit CSS"
|
||||
"editCssTitle": "CSS'i Düzenle"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"title": "Düzen Ayarı",
|
||||
"fields": {
|
||||
"preferSystemTitlebar": "Prefer System Titlebar",
|
||||
"preferSystemTitlebar": "Sistem Başlık Çubuğunu Tercih Et",
|
||||
"trafficGraph": "Trafik Grafiği",
|
||||
"memoryUsage": "Çekirdek Kullanımı",
|
||||
"proxyGroupIcon": "Vekil Grup Simgesi",
|
||||
"toastPosition": "Toast konumu",
|
||||
"hoverNavigator": "Hover Jump Navigator",
|
||||
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
|
||||
"hoverNavigator": "Üzerine Gelince Atlama Gezgini",
|
||||
"hoverNavigatorDelay": "Üzerine Gelince Atlama Gecikmesi",
|
||||
"navIcon": "Gezinme Simgesi",
|
||||
"collapseNavBar": "Gezinme çubuğunu daralt",
|
||||
"trayIcon": "Tepsi Simgesi",
|
||||
"proxyGroupsDisplayMode": "Proxy Groups Display Mode",
|
||||
"showOutboundModesInline": "Show Outbound Modes Inline",
|
||||
"proxyGroupsDisplayMode": "Vekil Grupları Görüntüleme Modu",
|
||||
"showOutboundModesInline": "Çıkış Modlarını Satır İçinde Göster",
|
||||
"commonTrayIcon": "Genel Tepsi Simgesi",
|
||||
"systemProxyTrayIcon": "Sistem Vekil Tepsi Simgesi",
|
||||
"tunTrayIcon": "Tun Tepsi Simgesi",
|
||||
@@ -251,8 +251,8 @@
|
||||
"pauseRenderTrafficStatsOnBlur": "Odak kaybolduğunda trafik istatistiklerinin işlenmesini duraklat"
|
||||
},
|
||||
"tooltips": {
|
||||
"hoverNavigator": "Automatically scroll to the corresponding proxy group when hovering over alphabet letters",
|
||||
"hoverNavigatorDelay": "Delay before auto scrolling when hovering, in milliseconds"
|
||||
"hoverNavigator": "Alfabe harflerinin üzerine gelindiğinde ilgili vekil grubuna otomatik olarak kaydırır",
|
||||
"hoverNavigatorDelay": "Üzerine gelindiğinde otomatik kaydırmadan önceki gecikme, milisaniye cinsinden"
|
||||
},
|
||||
"options": {
|
||||
"icon": {
|
||||
@@ -267,9 +267,9 @@
|
||||
"bottomRight": "Sağ alt"
|
||||
},
|
||||
"proxyGroupsDisplayMode": {
|
||||
"default": "Default",
|
||||
"inline": "Inline",
|
||||
"disable": "Disable"
|
||||
"default": "Varsayılan",
|
||||
"inline": "Satır İçi",
|
||||
"disable": "Devre Dışı"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,9 +289,9 @@
|
||||
"random": "Rastgele Port"
|
||||
},
|
||||
"messages": {
|
||||
"portInUse": "Port {{port}} is already in use",
|
||||
"saved": "Port settings saved",
|
||||
"saveFailed": "Failed to save port settings",
|
||||
"portInUse": "{{port}} portu zaten kullanımda",
|
||||
"saved": "Port ayarları kaydedildi",
|
||||
"saveFailed": "Port ayarları kaydedilemedi",
|
||||
"automaticFallback": "{{oldPort}} portu kullanımda olduğu için bu oturum {{newPort}} portunu kullanıyor. Ayarınız değişmedi ve sonraki başlatmada {{oldPort}} yeniden denenecek.",
|
||||
"automaticFallbackFailed": "Karma proxy portu otomatik olarak değiştirilemedi: {{error}}",
|
||||
"portTooLow": "Bağlantı noktası 1000 veya daha yüksek olmalıdır",
|
||||
@@ -325,24 +325,24 @@
|
||||
"backup": {
|
||||
"title": "Yedekleme Ayarı",
|
||||
"tabs": {
|
||||
"local": "Local backup",
|
||||
"webdav": "WebDAV backup"
|
||||
"local": "Yerel yedek",
|
||||
"webdav": "WebDAV yedeği"
|
||||
},
|
||||
"actions": {
|
||||
"selectTarget": "Select backup target",
|
||||
"selectTarget": "Yedekleme hedefini seçin",
|
||||
"backup": "Yedekle",
|
||||
"export": "Export",
|
||||
"exportBackup": "Export Backup",
|
||||
"importBackup": "Import Backup",
|
||||
"export": "Dışa Aktar",
|
||||
"exportBackup": "Yedeği Dışa Aktar",
|
||||
"importBackup": "Yedeği İçe Aktar",
|
||||
"deleteBackup": "Yedeği Sil",
|
||||
"restore": "Geri Yükle",
|
||||
"restoreBackup": "Yedeği Geri Yükle",
|
||||
"viewHistory": "View history"
|
||||
"viewHistory": "Geçmişi görüntüle"
|
||||
},
|
||||
"fields": {
|
||||
"webdavUrl": "WebDAV Sunucu URL'si",
|
||||
"username": "Kullanıcı Adı",
|
||||
"info": "Backups are stored locally in the application data directory. Use the list below to restore or delete backups."
|
||||
"info": "Yedekler yerel olarak uygulama veri dizininde saklanır. Yedekleri geri yüklemek veya silmek için aşağıdaki listeyi kullanın."
|
||||
},
|
||||
"messages": {
|
||||
"webdavUrlRequired": "WebDAV URL'si boş olamaz",
|
||||
@@ -353,60 +353,60 @@
|
||||
"webdavConfigSaveFailed": "WebDAV yapılandırması kaydedilemedi: {{error}}",
|
||||
"backupCreated": "Yedek başarıyla oluşturuldu",
|
||||
"backupFailed": "Yedekleme başarısız oldu: {{error}}",
|
||||
"localBackupCreated": "Local backup created successfully",
|
||||
"localBackupFailed": "Local backup failed",
|
||||
"localBackupCreated": "Yerel yedek başarıyla oluşturuldu",
|
||||
"localBackupFailed": "Yerel yedekleme başarısız oldu",
|
||||
"restoreSuccess": "Geri Yükleme Başarılı, Uygulama 1 saniye içinde yeniden başlatılacak",
|
||||
"localBackupExported": "Local backup exported successfully",
|
||||
"localBackupExportFailed": "Failed to export local backup",
|
||||
"localBackupImported": "Local backup imported successfully",
|
||||
"localBackupImportFailed": "Failed to import local backup: {{error}}",
|
||||
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||
"localBackupExported": "Yerel yedek başarıyla dışa aktarıldı",
|
||||
"localBackupExportFailed": "Yerel yedek dışa aktarılamadı",
|
||||
"localBackupImported": "Yerel yedek başarıyla içe aktarıldı",
|
||||
"localBackupImportFailed": "Yerel yedek içe aktarılamadı: {{error}}",
|
||||
"webdavRefreshSuccess": "WebDAV yenilemesi başarılı",
|
||||
"webdavRefreshFailed": "WebDAV yenilemesi başarısız oldu: {{error}}",
|
||||
"confirmDelete": "Bu yedek dosyasını silmeyi onaylıyor musunuz?",
|
||||
"confirmRestore": "Bu yedek dosyasını geri yüklemeyi onaylıyor musunuz?"
|
||||
},
|
||||
"auto": {
|
||||
"title": "Automatic backup",
|
||||
"scheduleLabel": "Enable scheduled backup",
|
||||
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||
"intervalLabel": "Backup frequency",
|
||||
"changeLabel": "Backup on critical changes",
|
||||
"changeHelper": "Automatically backup when Global Extend Config/Script changes.",
|
||||
"title": "Otomatik yedekleme",
|
||||
"scheduleLabel": "Zamanlanmış yedeklemeyi etkinleştir",
|
||||
"scheduleHelper": "Yapılandırılan aralıkta arka planda yerel yedekler oluşturur.",
|
||||
"intervalLabel": "Yedekleme sıklığı",
|
||||
"changeLabel": "Kritik değişikliklerde yedekle",
|
||||
"changeHelper": "Küresel Yapılandırma/Betik Genişletme değiştiğinde otomatik olarak yedekler.",
|
||||
"options": {
|
||||
"hours": "Every {{n}} hours",
|
||||
"days": "Every {{n}} days"
|
||||
"hours": "{{n}} saatte bir",
|
||||
"days": "{{n}} günde bir"
|
||||
}
|
||||
},
|
||||
"manual": {
|
||||
"title": "Manual backup",
|
||||
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||
"configureWebdav": "Configure WebDAV"
|
||||
"title": "Elle yedekleme",
|
||||
"local": "Bu cihazda, uygulama veri dizini altında saklanan bir anlık görüntü oluşturur.",
|
||||
"webdav": "Kimlik bilgileri ayarlandıktan sonra WebDAV sunucunuza bir anlık görüntü yükler.",
|
||||
"configureWebdav": "WebDAV'ı Yapılandır"
|
||||
},
|
||||
"history": {
|
||||
"title": "Backup history",
|
||||
"summary": "{{count}} backups • latest {{recent}}",
|
||||
"empty": "No backups available",
|
||||
"unknownPlatform": "unknown",
|
||||
"unknownTime": "Unknown time"
|
||||
"title": "Yedekleme geçmişi",
|
||||
"summary": "{{count}} yedek • en son {{recent}}",
|
||||
"empty": "Kullanılabilir yedek yok",
|
||||
"unknownPlatform": "bilinmiyor",
|
||||
"unknownTime": "Bilinmeyen zaman"
|
||||
},
|
||||
"webdav": {
|
||||
"title": "WebDAV settings"
|
||||
"title": "WebDAV ayarları"
|
||||
},
|
||||
"table": {
|
||||
"filename": "Dosya Adı",
|
||||
"backupTime": "Yedekleme Zamanı",
|
||||
"actions": "İşlemler",
|
||||
"noBackups": "Kullanılabilir yedek yok",
|
||||
"rowsPerPage": "Rows per page"
|
||||
"rowsPerPage": "Sayfa başına satır"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"title": "Çeşitli",
|
||||
"fields": {
|
||||
"appLogLevel": "Uygulama Günlük Seviyesi",
|
||||
"appLogMaxSize": "App Log Max Size",
|
||||
"appLogMaxCount": "App Log Max Count",
|
||||
"appLogMaxSize": "Uygulama Günlüğü En Büyük Boyutu",
|
||||
"appLogMaxCount": "Uygulama Günlüğü En Fazla Dosya Sayısı",
|
||||
"autoCloseConnections": "Bağlantıları Otomatik Kapat",
|
||||
"autoCheckUpdate": "Otomatik Güncelleme Kontrolü",
|
||||
"enableBuiltinEnhanced": "Yerleşik Geliştirilmiş Modu Etkinleştir",
|
||||
@@ -434,7 +434,7 @@
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"title": "New Version v{{version}}",
|
||||
"title": "Yeni Sürüm v{{version}}",
|
||||
"actions": {
|
||||
"goToRelease": "Sürüm Sayfasına Git",
|
||||
"update": "Güncelle"
|
||||
@@ -486,18 +486,18 @@
|
||||
"title": "Tun Modu",
|
||||
"fields": {
|
||||
"stack": "Tun Yığını",
|
||||
"device": "Device Name",
|
||||
"device": "Aygıt Adı",
|
||||
"autoRoute": "Otomatik Yönlendirme",
|
||||
"routeExcludeAddress": "Yönlendirme Hariç Adresler",
|
||||
"strictRoute": "Katı Yönlendirme",
|
||||
"autoDetectInterface": "Arayüzü Otomatik Algıla",
|
||||
"dnsHijack": "DNS Ele Geçirme",
|
||||
"mtu": "Maksimum İletim Birimi",
|
||||
"autoRedirect": "Auto Redirect"
|
||||
"autoRedirect": "Otomatik Yönlendirme"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
"dnsHijack": "Birden fazla DNS sunucusunu ayırmak için lütfen , kullanın",
|
||||
"autoRedirect": "nftables/iptables TCP yönlendirmelerini otomatik olarak yapılandırır"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "Ayarlar Uygulandı",
|
||||
@@ -511,11 +511,11 @@
|
||||
"warning": "Bu ayarlarla ilgili bilginiz yoksa, lütfen bunları değiştirmeyin ve DNS Üzerine Yazma'yı devre dışı tutun"
|
||||
},
|
||||
"protection": {
|
||||
"title": "Enable DNS Overwrite?",
|
||||
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
|
||||
"enableAnyway": "Enable anyway",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
|
||||
"title": "DNS Üzerine Yazma etkinleştirilsin mi?",
|
||||
"message": "Bu abonelik özel DNS ayarları içeriyor. DNS Üzerine Yazma bunların yerine geçebilir; bu da düğümlerin bağlanmasını veya bazı web sitelerinin yüklenmesini engelleyebilir. Yine de etkinleştirilsin mi?",
|
||||
"enableAnyway": "Yine de etkinleştir",
|
||||
"keepDisabled": "Devre dışı bırak",
|
||||
"autoDisabled": "Geçerli abonelikteki özel DNS ayarlarını korumak için DNS Üzerine Yazma devre dışı bırakıldı."
|
||||
},
|
||||
"sections": {
|
||||
"general": "DNS Ayarları",
|
||||
@@ -535,7 +535,7 @@
|
||||
},
|
||||
"preferH3": {
|
||||
"label": "H3'ü Tercih Et",
|
||||
"description": "DNS DOH uses HTTP/3"
|
||||
"description": "DNS DOH, HTTP/3 kullanır"
|
||||
},
|
||||
"respectRules": {
|
||||
"label": "Kurallara Uy",
|
||||
@@ -601,11 +601,11 @@
|
||||
},
|
||||
"messages": {
|
||||
"saved": "DNS ayarları kaydedildi",
|
||||
"configError": "DNS configuration error:"
|
||||
"configError": "DNS yapılandırma hatası:"
|
||||
},
|
||||
"errors": {
|
||||
"invalid": "Invalid configuration",
|
||||
"invalidYaml": "Invalid YAML format"
|
||||
"invalid": "Geçersiz yapılandırma",
|
||||
"invalidYaml": "Geçersiz YAML biçimi"
|
||||
}
|
||||
},
|
||||
"webUI": {
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Hizmet Başarıyla Kuruldu",
|
||||
"sidecarFallback": "Hizmet, yol izinleri nedeniyle çekirdeği başlatamadı. Çalışmaya devam etmek için yönetici moduna geçildi.",
|
||||
"uninstallSuccess": "Hizmet Başarıyla Kaldırıldı"
|
||||
},
|
||||
"updater": {
|
||||
@@ -693,12 +694,12 @@
|
||||
},
|
||||
"statuses": {
|
||||
"clash": {
|
||||
"stopping": "Stopping Core...",
|
||||
"restarting": "Restarting Core..."
|
||||
"stopping": "Çekirdek Durduruluyor...",
|
||||
"restarting": "Çekirdek Yeniden Başlatılıyor..."
|
||||
},
|
||||
"clashService": {
|
||||
"installing": "Hizmet Kuruluyor...",
|
||||
"uninstalling": "Uninstalling Service..."
|
||||
"uninstalling": "Hizmet Kaldırılıyor..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-17
@@ -12,23 +12,23 @@
|
||||
"restart": "Yeniden Başlat",
|
||||
"resetToDefault": "Varsayılana Sıfırla",
|
||||
"refresh": "Yenile",
|
||||
"retry": "Retry",
|
||||
"refreshPage": "Refresh Page",
|
||||
"showDetails": "Show Details",
|
||||
"hideDetails": "Hide Details",
|
||||
"retry": "Yeniden Dene",
|
||||
"refreshPage": "Sayfayı Yenile",
|
||||
"showDetails": "Ayrıntıları Göster",
|
||||
"hideDetails": "Ayrıntıları Gizle",
|
||||
"listView": "Liste Görünümü",
|
||||
"tableView": "Tablo Görünümü",
|
||||
"pause": "Duraklat",
|
||||
"resume": "Sürdür",
|
||||
"closeAll": "Tümünü Kapat",
|
||||
"clear": "Temizle",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"previous": "Önceki",
|
||||
"next": "Sonraki",
|
||||
"check": "Denetle"
|
||||
},
|
||||
"labels": {
|
||||
"updateAt": "Güncelleme Zamanı",
|
||||
"timeout": "Timeout",
|
||||
"timeout": "Zaman Aşımı",
|
||||
"icon": "Simge",
|
||||
"name": "İsim",
|
||||
"readOnly": "Salt Okunur",
|
||||
@@ -37,14 +37,14 @@
|
||||
"usedTotal": "Kullanılan / Toplam",
|
||||
"from": "Kaynak",
|
||||
"password": "Şifre",
|
||||
"retryAttempts": "Retry attempts",
|
||||
"retryAttempts": "Yeniden deneme sayısı",
|
||||
"downloaded": "İndirilen",
|
||||
"uploaded": "Yüklenen"
|
||||
},
|
||||
"statuses": {
|
||||
"enabled": "Etkin",
|
||||
"disabled": "Devre Dışı",
|
||||
"saving": "Saving...",
|
||||
"saving": "Kaydediliyor...",
|
||||
"empty": "Boş",
|
||||
"loading": "Yükleniyor..."
|
||||
},
|
||||
@@ -54,7 +54,7 @@
|
||||
"minutes": "dakika",
|
||||
"hours": "saat",
|
||||
"kilobytes": "KB",
|
||||
"files": "Files"
|
||||
"files": "Dosya"
|
||||
},
|
||||
"placeholders": {
|
||||
"resetInput": "Giriş alanını temizle",
|
||||
@@ -64,7 +64,7 @@
|
||||
"useRegex": "Düzenli İfade Kullan"
|
||||
},
|
||||
"validation": {
|
||||
"invalidRegex": "Invalid regular expression"
|
||||
"invalidRegex": "Geçersiz düzenli ifade"
|
||||
},
|
||||
"window": {
|
||||
"maximize": "Büyüt",
|
||||
@@ -84,8 +84,8 @@
|
||||
"componentStack": "Bileşen yığını",
|
||||
"trafficUnavailable": "Trafik verileri kullanılamıyor",
|
||||
"operationFailed": "İşlem başarısız oldu",
|
||||
"trafficStats": "Traffic Statistics Error",
|
||||
"trafficStatsDescription": "The traffic statistics component encountered an error and has been disabled to prevent crashes."
|
||||
"trafficStats": "Trafik İstatistikleri Hatası",
|
||||
"trafficStatsDescription": "Trafik istatistikleri bileşeni bir hatayla karşılaştı ve çökmeleri önlemek için devre dışı bırakıldı."
|
||||
},
|
||||
"notices": {
|
||||
"raw": "{{message}}",
|
||||
@@ -95,12 +95,12 @@
|
||||
"importSuccess": "Profil Başarıyla İçe Aktarıldı",
|
||||
"importSubscriptionSuccess": "Abonelik içe aktarımı başarılı",
|
||||
"importWithClashProxy": "Profil Clash vekil ile içe aktarıldı",
|
||||
"updateAvailable": "Update Available",
|
||||
"saved": "Saved successfully",
|
||||
"updateAvailable": "Güncelleme Mevcut",
|
||||
"saved": "Başarıyla kaydedildi",
|
||||
"common": {
|
||||
"copySuccess": "Kopyalama Başarılı",
|
||||
"saveSuccess": "Configuration saved successfully",
|
||||
"saveFailed": "Failed to save configuration",
|
||||
"saveSuccess": "Yapılandırma başarıyla kaydedildi",
|
||||
"saveFailed": "Yapılandırma kaydedilemedi",
|
||||
"refreshFailed": "Yenileme başarısız"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
"actions": {
|
||||
"testing": "Test Ediliyor..."
|
||||
},
|
||||
"empty": "No unlock test items",
|
||||
"empty": "Kilit açma testi öğesi yok",
|
||||
"messages": {
|
||||
"detectionFailedWithName": "{{name}} için tespit başarısız oldu",
|
||||
"detectionTimeout": "Detection timeout or failed"
|
||||
"detectionTimeout": "Algılama zaman aşımına uğradı veya başarısız oldu"
|
||||
},
|
||||
"title": "Kilit Açma Testi"
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Сервис уңышлы урнаштырылды",
|
||||
"sidecarFallback": "Хезмәт юлга керү хокуклары аркасында төшне эшләтеп җибәрә алмады. Эшне дәвам итү өчен администратор режимына күчелде.",
|
||||
"uninstallSuccess": "Сервис уңышлы салдырылды"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} 更新成功",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "已成功安装服务",
|
||||
"sidecarFallback": "服务因路径权限问题无法启动内核,已切回管理员模式继续运行。",
|
||||
"uninstallSuccess": "已成功卸载服务"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "Test URL must start with http:// or https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "已成功安裝服務",
|
||||
"sidecarFallback": "服務因路徑權限問題無法啟動核心,已切回管理員模式繼續執行。",
|
||||
"uninstallSuccess": "已成功解除安裝服務"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -13,7 +13,10 @@ vi.mock('react', () => ({ useEffect: (effect: () => void) => effect() }))
|
||||
vi.mock('@/hooks/use-profiles', () => ({ revalidateProfiles: vi.fn() }))
|
||||
vi.mock('@/hooks/use-system-state', () => ({ runStateQueryKey: ['state'] }))
|
||||
vi.mock('@/services/events', () => ({ subscribeVergeEvents: vi.fn() }))
|
||||
vi.mock('@/services/cmds', () => ({ takeDnsOverrideNotice: vi.fn() }))
|
||||
vi.mock('@/services/cmds', () => ({
|
||||
takeDnsOverrideNotice: vi.fn(),
|
||||
takeServiceFallbackNotice: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
vi.mock('@/services/notice-service', () => ({
|
||||
showNotice: { info: vi.fn() },
|
||||
}))
|
||||
|
||||
@@ -64,6 +64,7 @@ export const useLayoutEvents = (
|
||||
() => {
|
||||
revalidateKeys(['getRuntimeState', 'getVergeConfig'])
|
||||
handleNotice(['dns_override::auto_disabled', ''])
|
||||
handleNotice(['service_core::sidecar_fallback', ''])
|
||||
},
|
||||
)
|
||||
}, [handleNotice])
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { takeDnsOverrideNotice } from '@/services/cmds'
|
||||
import {
|
||||
takeDnsOverrideNotice,
|
||||
takeServiceFallbackNotice,
|
||||
} from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
type NavigateFunction = (path: string, options?: any) => void
|
||||
@@ -25,6 +28,22 @@ export const handleNoticeMessage = (
|
||||
showNotice.error(msg)
|
||||
},
|
||||
'set_config::error': () => showNotice.error(msg),
|
||||
'service_core::sidecar_fallback': () => {
|
||||
void takeServiceFallbackNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
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) => {
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
useThemeMode,
|
||||
} from '@/services/states'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
|
||||
// 与 src-tauri/src/main.rs 的 worker_limit 上限(8)保持一致,避免前后端更新风暴不对齐
|
||||
const PROFILE_UPDATE_WORKER_LIMIT = 8
|
||||
@@ -214,7 +215,7 @@ const ProfilePage = () => {
|
||||
|
||||
const onImport = async () => {
|
||||
if (!url) return
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
if (!isValidUrl(url)) {
|
||||
showNotice.error('profiles.page.feedback.errors.invalidUrl')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,6 +182,10 @@ export async function takeDnsOverrideNotice() {
|
||||
return invoke<boolean>('take_dns_override_notice')
|
||||
}
|
||||
|
||||
export async function takeServiceFallbackNotice() {
|
||||
return invoke<boolean>('take_service_fallback_notice')
|
||||
}
|
||||
|
||||
export async function getSystemProxy() {
|
||||
return invoke<{
|
||||
enable: boolean
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/types/proxy-view'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { classifyDelay, DEFAULT_DELAY_TIMEOUT } from '@/utils/delay'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
|
||||
export type DelaySnapshot = {
|
||||
of: (member: ResolvedProxyMember) => number
|
||||
@@ -152,6 +153,11 @@ class DelayManager {
|
||||
}
|
||||
|
||||
setUrl(group: string, url: string) {
|
||||
if (!isValidUrl(url)) {
|
||||
debugLog(`[DelayManager] 拒绝无效测试URL,组: ${group}, URL: ${url}`)
|
||||
this.urlMap.delete(group)
|
||||
return
|
||||
}
|
||||
debugLog(`[DelayManager] 设置测试URL,组: ${group}, URL: ${url}`)
|
||||
this.urlMap.set(group, url)
|
||||
}
|
||||
|
||||
@@ -324,6 +324,7 @@ export const translationKeys = [
|
||||
'proxies.page.messages.directMode',
|
||||
'proxies.page.title.default',
|
||||
'proxies.page.title.chainMode',
|
||||
'proxies.feedback.warnings.invalidTestUrl',
|
||||
'proxies.feedback.notifications.provider.updateSuccess',
|
||||
'proxies.feedback.notifications.provider.updateFailed',
|
||||
'proxies.feedback.notifications.provider.genericError',
|
||||
@@ -793,6 +794,7 @@ export const translationKeys = [
|
||||
'settings.feedback.notifications.clash.changeFailed',
|
||||
'settings.feedback.notifications.clash.geoDataUpdated',
|
||||
'settings.feedback.notifications.clashService.installSuccess',
|
||||
'settings.feedback.notifications.clashService.sidecarFallback',
|
||||
'settings.feedback.notifications.clashService.uninstallSuccess',
|
||||
'settings.feedback.notifications.updater.withClashProxySuccess',
|
||||
'settings.feedback.notifications.updater.withClashProxyFailed',
|
||||
|
||||
@@ -500,6 +500,9 @@ export interface TranslationResources {
|
||||
updateSuccess: string
|
||||
}
|
||||
}
|
||||
warnings: {
|
||||
invalidTestUrl: string
|
||||
}
|
||||
}
|
||||
page: {
|
||||
actions: {
|
||||
@@ -831,6 +834,7 @@ export interface TranslationResources {
|
||||
}
|
||||
clashService: {
|
||||
installSuccess: string
|
||||
sidecarFallback: string
|
||||
uninstallSuccess: string
|
||||
}
|
||||
updater: {
|
||||
|
||||
Reference in New Issue
Block a user