Compare commits

..
1 Commits
Author SHA1 Message Date
Tunglies 83a06e5716 feat(app): upgrade to tauri 3.0.0-alpha
Move the app onto the tauri v3 alpha line: runtime selection is now
explicit via tauri-runtime-wry, the Wry generics give way to the
DynRuntime defaults, and every official plugin rides 3.0.0-alpha.0
alongside the matching npm packages; pnpm moves to 12.4.2. The
tray-icon patch is dropped because upstream 0.25.1 ships the macOS 27
click-reentrancy fix. perf-harness is temporarily hollowed out until
tauri-plugin-wdio-webdriver publishes a tauri 3 release.
2026-09-16 17:41:52 +08:00
46 changed files with 1066 additions and 1341 deletions
+2 -2
View File
@@ -3,8 +3,8 @@ name: Auto Build
on:
workflow_dispatch:
schedule:
# UTC+8 06:00, 18:00 -> UTC 22:00, 10:00
- cron: '0 10,22 * * *'
# UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
- cron: '0 4,10 * * *'
permissions: write-all
env:
TAG_NAME: autobuild
Generated
+262 -438
View File
File diff suppressed because it is too large Load Diff
+3 -8
View File
@@ -46,12 +46,6 @@ 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" }
@@ -63,8 +57,9 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tracing = "0.1.44"
tauri = { version = "2.11.5" }
tauri-plugin-clipboard-manager = "2.3.3"
tauri = { version = "3.0.0-alpha.1" }
tauri-runtime-wry = "3.0.0-alpha.1"
tauri-plugin-clipboard-manager = "3.0.0-alpha.0"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.104"
chrono = "0.4.45"
-1
View File
@@ -27,7 +27,6 @@
**🖥️ Windows**
- 修复 Windows 管理员运行时,服务权限校验失败后无法继续使用代理的问题
- 修复 Windows 便携版或 Scoop 安装后服务内核无法启动的问题
- 修复 Windows 系统代理设置偶发失败的问题
- 修复 Windows 关机被应用阻止的问题
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.5.4-rc",
"version": "2.5.4",
"license": "GPL-3.0-only",
"scripts": {
"prepare": "husky || true",
@@ -52,13 +52,13 @@
"@mui/icons-material": "^9.0.1",
"@mui/material": "^9.0.1",
"@tanstack/react-virtual": "^3.13.25",
"@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",
"@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",
"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": "2.11.4",
"@tauri-apps/cli": "3.0.0-alpha.1",
"@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.1"
"packageManager": "pnpm@12.4.2"
}
+147 -142
View File
@@ -7,153 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.4.1
version: 12.4.1
specifier: 12.4.2
version: 12.4.2
packages:
'@pnpm/exe.android-arm64@12.4.1':
resolution: {integrity: sha512-/HwsqXMSmlOfgtV9+O0ratzjV6Vd/8n1hh4rGHpCGmDURvt52MwZxdbhyxP4K03ZfvgSDvotFPr8O+RzE5Eu8A==}
'@pnpm/exe.android-arm64@12.4.2':
resolution: {integrity: sha512-E255MbcQ0V1577M2BV0ajWuP7KmTPYA0jqZnk3rK6Lq3kTvZWulMMb7iqRmnLsQbyWTkMEB2CfyM70loVDA8xg==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.4.1':
resolution: {integrity: sha512-+l74Qb4c2YjOzNKHXJLg+1wr8xHM1ckkUhnU5KRUK9TJqiiczt6yeTZqqzHIlJ8i6pAoj5V0OJevDRwnQKLgrQ==}
'@pnpm/exe.android-x64@12.4.2':
resolution: {integrity: sha512-J1pSeCUwuKxMG70ZzpWn8JzElxiEa8NN/3N0SlZRtU2xbztChaJCKF3HaBNIj9yPfeofWzJo/lwNvDDjraQuZw==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.4.1':
resolution: {integrity: sha512-6rkZkT3iGfaxknUdGHraqSWFvTa6N0ajAHluv9Ax0GRWs0sIcGNiFhDopv6xSZCsJZmG483aNS/b6UEDy3blfw==}
'@pnpm/exe.darwin-arm64@12.4.2':
resolution: {integrity: sha512-A0WDo8iErfZBXgrLseQxw8i8Y9ctUpOEl/Uu+cubnTzpD8tT9ykIB548L8YTM2WD4OS+ZOHSxy8aGZcvKq8PaQ==}
cpu: [arm64]
os: [darwin]
'@pnpm/exe.darwin-x64@12.4.1':
resolution: {integrity: sha512-Vb1CHlR88HghC1qUxjxjs82zQSXnXacPD+btG2CmG8Q/hBU7Q0/b2YWJKSQqZXicunV5khFtfTlAwJddV9RkYA==}
'@pnpm/exe.darwin-x64@12.4.2':
resolution: {integrity: sha512-MSgJdovBWHcb5DEOvfPH9yNi/T5O1Xa4ess+E1ESGo/5yuty7S4JoiIjami+fsNXfnoQMlwsMUqYU4zAvBcNCw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.freebsd-x64@12.4.1':
resolution: {integrity: sha512-iT3iHz3Nl0Sxxj7UPOtZ/aQ81AFsQhjoeWMAlPkSRO04gsgDGAXv3UYxOFaesMWsfNxaGn0A+CITbuCHFF66FA==}
'@pnpm/exe.freebsd-x64@12.4.2':
resolution: {integrity: sha512-h2YumlQSNvgbRPv+RXwABohX65f9bOBZn+jMIt7bFDISZPCzQ+Nvpt6Awbp4ip5PwQgYxbu5iREJ1fHE39Fm8A==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.4.1':
resolution: {integrity: sha512-aBooZfNXM5f+OGUgCAMFWpE/kAhsWfvmqIyMtHy6zl3aNxyInWrcY/Saln/UElzo7lZWMC0Yktroxd/2J26lNQ==}
'@pnpm/exe.linux-arm64-musl@12.4.2':
resolution: {integrity: sha512-LwSEtSEDTv6S51YLs3YvSkPyun/QmfMic1UGICUkPWFu6ByP43RdMlkKvmVkfGhAYCpnxO057vrmyJqtfZrPCA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.4.1':
resolution: {integrity: sha512-TlOdacTTP09BgcMvwWBFRsu8VAjfwqwnslBj+XSq1JFM3ck4f3k+1O/747EuctxZxs3/o785b6Q3s7Pd92Ptsg==}
'@pnpm/exe.linux-arm64@12.4.2':
resolution: {integrity: sha512-2dSiDXyhx+RTHsewxex8f/jVjqQXWJ2oow4kCVHEWdZKeBpgMxvZ6fHkTAUBJXgqhbKIDHvuNlZBP7gJfUWL5A==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-ppc64@12.4.1':
resolution: {integrity: sha512-r/ab/MlIBo75oizUP5ITiziCCrnXz4SJwfErLQ+603AshB+Yq7xTMCoMtzTSCAMu6aaymIKkAFtZvd2J75Wq0w==}
'@pnpm/exe.linux-ppc64@12.4.2':
resolution: {integrity: sha512-8Itc+jQk+MTz04LS9D1RcH+VctAWmzM4l1cJQ+Sx7pAJNo7EKHdRMbC0Tok1jyQ7eEHNJZD5EleYneZqWfZM+g==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.4.1':
resolution: {integrity: sha512-C/D1QWdKMiB8+wv/spl1rGITFUuqC+aI/fb6h8NB5sqxsOaGXeYc/g891oCuzggHn6SODk9p+I09hI76x/cMNw==}
'@pnpm/exe.linux-riscv64@12.4.2':
resolution: {integrity: sha512-hleOeqhTVpH+z9RVMGnxvU4ZnrkBUClWjCbHD9u6kwyqhGSpevoU1wTGish+CBRhmIgMAy9pAfFpqhbAKOKNfw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.4.1':
resolution: {integrity: sha512-nxz5zD4yXt94uzbStDk0QTPKW+aE92hH1b5tFXK9ctB1HE9Xcq1vjTiA2LsZMFC6p4gdu5OwQeFqYNAVGa6QlA==}
'@pnpm/exe.linux-s390x@12.4.2':
resolution: {integrity: sha512-LAsQRRdP9aToENR6dtcIJ9l+e1zMYOX0tQcLGpRyLPVBQcYRLlvAPcmDshsiIHQjp05SDa9FI0czX1ZQ1a7a/A==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.4.1':
resolution: {integrity: sha512-5AwgFdGhVUg2kIweYGfxzSLEHiIG77PQhZAkXC3TwofQHsu1Wr+TrV5/rNX2PopFnHRzuE581zoB8F6Wle32yg==}
'@pnpm/exe.linux-x64-musl@12.4.2':
resolution: {integrity: sha512-kzfzH2/0BWdTABK14Yj5a1xsdkTEQUp2eXEPNakaD9jKL025lq3hyaKHIz/gIZaPDMe/1bFFK/En4ztFlbBJxw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.4.1':
resolution: {integrity: sha512-FJOZuuuQMhp0oLzBtcKkLXknBI92hfkmSnlKc47vfin4HrmfID5khY2lGekL9tCzk1cpR+HShEw/meFl+nHtzQ==}
'@pnpm/exe.linux-x64@12.4.2':
resolution: {integrity: sha512-/pbt0UVTa8NMDhzOWLQRfZ6G9ROKXlJPZtx845BqyWfc7hCrWXhTLBd70yO2y8+E+IWN3oHM1s/JsIQNGk1yvg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.4.1':
resolution: {integrity: sha512-OO7eKBL9S+xk5hRy+JUUZSNJknGuGe3GEUffsrlC2LbqKF02g+VX1anGIzSbJDvkkdnpJhSlxF5AUz2JzJu2Jw==}
'@pnpm/exe.win32-arm64@12.4.2':
resolution: {integrity: sha512-PsW19e4dAUNpZ0cS9flaxFuAmpt2dKlH/Vvi8TZ4qyJjjQzue/CEsWm+6wKVP4CJ7IT8RdL4qh+soOPWIgF2Zw==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.4.1':
resolution: {integrity: sha512-x7gJHZgHo6hp354xCYA2NvoFzYJkHwovt2kVsUBK5EmXULLcP51JGMq09CX+5FRFJUZFKoXjLu3mZWmfP7o6PQ==}
'@pnpm/exe.win32-x64@12.4.2':
resolution: {integrity: sha512-+xGoeE0g55ztWvl8i5QqdmNfW3nIrTVcoQrNshEOwxthm9Ag48oXton3uxOA3/SANyz5AXexEjj2KO20NnOrEw==}
cpu: [x64]
os: [win32]
pnpm@12.4.1:
resolution: {integrity: sha512-LoHjmdc/6DkNqyXgaqeIq3pZCCSNL1o3D4K0gRR6ano2e/gEj5pv22Rg8hpm8FQt7bi5TKLIcjWWdBkgsWVtTA==}
pnpm@12.4.2:
resolution: {integrity: sha512-CK3GYTGAJ1x8ntraOdzwjJxhrU5+rzMKTzRh8QKw+QdCNFTRF/mOctR/7wYWBwZE17/8lzpqV/UJCm18NosHyQ==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.android-arm64@12.4.1':
'@pnpm/exe.android-arm64@12.4.2':
optional: true
'@pnpm/exe.android-x64@12.4.1':
'@pnpm/exe.android-x64@12.4.2':
optional: true
'@pnpm/exe.darwin-arm64@12.4.1':
'@pnpm/exe.darwin-arm64@12.4.2':
optional: true
'@pnpm/exe.darwin-x64@12.4.1':
'@pnpm/exe.darwin-x64@12.4.2':
optional: true
'@pnpm/exe.freebsd-x64@12.4.1':
'@pnpm/exe.freebsd-x64@12.4.2':
optional: true
'@pnpm/exe.linux-arm64-musl@12.4.1':
'@pnpm/exe.linux-arm64-musl@12.4.2':
optional: true
'@pnpm/exe.linux-arm64@12.4.1':
'@pnpm/exe.linux-arm64@12.4.2':
optional: true
'@pnpm/exe.linux-ppc64@12.4.1':
'@pnpm/exe.linux-ppc64@12.4.2':
optional: true
'@pnpm/exe.linux-riscv64@12.4.1':
'@pnpm/exe.linux-riscv64@12.4.2':
optional: true
'@pnpm/exe.linux-s390x@12.4.1':
'@pnpm/exe.linux-s390x@12.4.2':
optional: true
'@pnpm/exe.linux-x64-musl@12.4.1':
'@pnpm/exe.linux-x64-musl@12.4.2':
optional: true
'@pnpm/exe.linux-x64@12.4.1':
'@pnpm/exe.linux-x64@12.4.2':
optional: true
'@pnpm/exe.win32-arm64@12.4.1':
'@pnpm/exe.win32-arm64@12.4.2':
optional: true
'@pnpm/exe.win32-x64@12.4.1':
'@pnpm/exe.win32-x64@12.4.2':
optional: true
pnpm@12.4.1:
pnpm@12.4.2:
optionalDependencies:
'@pnpm/exe.android-arm64': 12.4.1
'@pnpm/exe.android-x64': 12.4.1
'@pnpm/exe.darwin-arm64': 12.4.1
'@pnpm/exe.darwin-x64': 12.4.1
'@pnpm/exe.freebsd-x64': 12.4.1
'@pnpm/exe.linux-arm64': 12.4.1
'@pnpm/exe.linux-arm64-musl': 12.4.1
'@pnpm/exe.linux-ppc64': 12.4.1
'@pnpm/exe.linux-riscv64': 12.4.1
'@pnpm/exe.linux-s390x': 12.4.1
'@pnpm/exe.linux-x64': 12.4.1
'@pnpm/exe.linux-x64-musl': 12.4.1
'@pnpm/exe.win32-arm64': 12.4.1
'@pnpm/exe.win32-x64': 12.4.1
'@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
---
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: 2.11.1
version: 2.11.1
specifier: 3.0.0-alpha.0
version: 3.0.0-alpha.0
'@tauri-apps/plugin-clipboard-manager':
specifier: ^2.3.2
version: 2.3.2
specifier: ^3.0.0-alpha.0
version: 3.0.0-alpha.0
'@tauri-apps/plugin-dialog':
specifier: ^2.7.1
version: 2.7.2
specifier: ^3.0.0-alpha.0
version: 3.0.0-alpha.0
'@tauri-apps/plugin-fs':
specifier: ^2.5.1
version: 2.5.1
specifier: ^3.0.0-alpha.0
version: 3.0.0-alpha.0
'@tauri-apps/plugin-http':
specifier: ~2.6.0
version: 2.6.0
specifier: ~3.0.0-alpha.0
version: 3.0.0-alpha.0
'@tauri-apps/plugin-opener':
specifier: 2.5.5
version: 2.5.5
specifier: 3.0.0-alpha.0
version: 3.0.0-alpha.0
'@tauri-apps/plugin-updater':
specifier: 2.11.0
version: 2.11.0
specifier: 3.0.0-alpha.0
version: 3.0.0-alpha.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: 2.11.4
version: 2.11.4
specifier: 3.0.0-alpha.1
version: 3.0.0-alpha.1
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
@@ -2106,99 +2106,102 @@ packages:
'@tauri-apps/api@2.11.1':
resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==}
'@tauri-apps/cli-darwin-arm64@2.11.4':
resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==}
'@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==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.11.4':
resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==}
'@tauri-apps/cli-darwin-x64@3.0.0-alpha.1':
resolution: {integrity: sha512-eSBLEBJgwHZDLCsdR2fsDoEztd48mnM8U64rSlZCqULOr3sIRtXVPdoHGBoUoU4C84j+utGumXa+frIxbq7Log==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.4':
resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==}
'@tauri-apps/cli-linux-arm-gnueabihf@3.0.0-alpha.1':
resolution: {integrity: sha512-6peZspFoNmfoqzBKCiziOsntq2Dy5WI0RLXCdffqCrsC/CplIQB1xmldmOeI8xzuTjRCUqxEO2Vurv7HVluZhA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.11.4':
resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==}
'@tauri-apps/cli-linux-arm64-gnu@3.0.0-alpha.1':
resolution: {integrity: sha512-gp3CJqdmqPE1IEkH6sbFDI6yYhxqGn8OuHU6jkA7jAVdxJGdeGuQZE9c7XrEq8uFNNfjz6bOI3djfVOvqzgAvw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.11.4':
resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==}
'@tauri-apps/cli-linux-arm64-musl@3.0.0-alpha.1':
resolution: {integrity: sha512-t6lhy7kvBp5VKGoob0X8MBMAS/ah1NE3s0N9kwhMw06gAFMBvDVnq/het3CkZkbQm+uoUbDnWagZ+7buGkI6XQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.11.4':
resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==}
'@tauri-apps/cli-linux-riscv64-gnu@3.0.0-alpha.1':
resolution: {integrity: sha512-SHjvrK8DZ+FdIgLaisMgTVtDKCoR3m4639FgO/EeJcMM73LipUqgKIpkxxutqMyLoPJ6gxIQ6HHjNvNqUCmN3w==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.11.4':
resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==}
'@tauri-apps/cli-linux-x64-gnu@3.0.0-alpha.1':
resolution: {integrity: sha512-0nwRa3cD//pAIRm3xwA3+N9zI01WnRBVOGHN4dpnWoZZDwwbsvCHwzyl34FsJv5wwcvGLlzbjMART55IPpTdlQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.11.4':
resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==}
'@tauri-apps/cli-linux-x64-musl@3.0.0-alpha.1':
resolution: {integrity: sha512-8ljY9JY6iGhGEAm7YWfQTa5If88to4Tpk8HHtIo6bay3+IJW1XanAmykLQdHoBRxMJyuAeE7uk2Gmfu7plQifg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.11.4':
resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==}
'@tauri-apps/cli-win32-arm64-msvc@3.0.0-alpha.1':
resolution: {integrity: sha512-1L79hm8E6NhauTjl3WXkptZ1K8mfkgs3nB38YopgvThZI6ncVJ1NOMcaJfdxOzCkuliDAl1PSUkQnljNTNsmww==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.11.4':
resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==}
'@tauri-apps/cli-win32-ia32-msvc@3.0.0-alpha.1':
resolution: {integrity: sha512-YKhX4NS2xKyDduznVVhkiNVitwBVTe8erheiHrYzWVsSuHCQVyiQAajfuAaufdeCIv0cTFo6siCWmBu1YcFUcg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.11.4':
resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==}
'@tauri-apps/cli-win32-x64-msvc@3.0.0-alpha.1':
resolution: {integrity: sha512-/Bth9cwQOGgcoST8SJ5obTCYpVqqVYoRqo5RBUvbsIHRCbui1zL+giQDWdXDhCJEEXkGmSMiIxWTmg+v0Wp4uA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.11.4':
resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==}
'@tauri-apps/cli@3.0.0-alpha.1':
resolution: {integrity: sha512-7KSBFGBZk07THOp18HtMIntgxyYQNMDNcm0EEcmRmGTdcUS7X0BkrWDxtw8PRDMA9RZ1oYZKcjJjFyA8Lwr6dw==}
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-clipboard-manager@2.3.2':
resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==}
'@tauri-apps/plugin-clipboard-manager@3.0.0-alpha.0':
resolution: {integrity: sha512-doH+SBkHXvNKAfInPIFKtAZrmmZRHOA5yf4lFeu74PRbMB7tfH/qNhfepi3C7HRBsmfyWUht/UqfrwMWKfLTKw==}
'@tauri-apps/plugin-dialog@2.7.2':
resolution: {integrity: sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg==}
'@tauri-apps/plugin-dialog@3.0.0-alpha.0':
resolution: {integrity: sha512-ZnHHbwYlB/1nJqs0gZdi7eHumo3mArCSakx/7T1pqEIkqmXV4PHC+dSZLzsc20+A/HNgIQqa2uR/+yaXKWDzbw==}
'@tauri-apps/plugin-fs@2.5.1':
resolution: {integrity: sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==}
'@tauri-apps/plugin-fs@3.0.0-alpha.0':
resolution: {integrity: sha512-PlpcBFbWNZZrOvd0pUJbdTGaDFXuFVtW5tmF2D2w7mf/KwRI0w7jzrR1b7Sy6fs0lOR5zI/sXQHdt7ExKoEMcQ==}
'@tauri-apps/plugin-http@2.6.0':
resolution: {integrity: sha512-QYXwbGb4hQ9/8Riv/ejU/kPFFnbBIrBcWwV1LIXv2xBKfoj8lkWfGkd9pkCSsBI/pljPtz+IPqfrE3t3bVl3mg==}
'@tauri-apps/plugin-http@3.0.0-alpha.0':
resolution: {integrity: sha512-cRyrWu2fcrlSmebEc3XE3UGE/Hj20REKpgbtlpAEydk5/dvu0nfvC/wCKEgD2w8h0XwTu8ewXkRG0R/j18a+5w==}
'@tauri-apps/plugin-opener@2.5.5':
resolution: {integrity: sha512-xvzGai5aQds8j8R8RsUK/lW6pGG50YgOYIPLzvkqmkwAj7dfySOD7sGtejRzvVdMmv1EQfKVEFh1MvmDp8QR0g==}
'@tauri-apps/plugin-opener@3.0.0-alpha.0':
resolution: {integrity: sha512-jbAAtkBlJCRIujSN+oGioSSb7E+r2f4w+VNFcakwf6ZsaX1JXVawRZJmNQ/h2/tvHpMO7s6neX56HrlWc7Cnjg==}
'@tauri-apps/plugin-updater@2.11.0':
resolution: {integrity: sha512-AE36XkOoSna24G40jZMY15nzAnkXEPL/73tGoseGrtGOHuI/cZwWzHpZFLjKXDPgzYZ435z1gHu28LgrsBwIxQ==}
'@tauri-apps/plugin-updater@3.0.0-alpha.0':
resolution: {integrity: sha512-ArbrJFjrn0vtnMkHOPdEQZM6hD6EXUMNXNVcXFtxhnst1MrjO5SKjcZZIg3E6W3Y2DUBE8iIMMKJfYOLDRiBxQ==}
'@tootallnate/quickjs-emscripten@0.23.0':
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
@@ -6955,74 +6958,76 @@ snapshots:
'@tauri-apps/api@2.11.1': {}
'@tauri-apps/cli-darwin-arm64@2.11.4':
'@tauri-apps/api@3.0.0-alpha.0': {}
'@tauri-apps/cli-darwin-arm64@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-darwin-x64@2.11.4':
'@tauri-apps/cli-darwin-x64@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.4':
'@tauri-apps/cli-linux-arm-gnueabihf@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.11.4':
'@tauri-apps/cli-linux-arm64-gnu@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.11.4':
'@tauri-apps/cli-linux-arm64-musl@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.11.4':
'@tauri-apps/cli-linux-riscv64-gnu@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.11.4':
'@tauri-apps/cli-linux-x64-gnu@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.11.4':
'@tauri-apps/cli-linux-x64-musl@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.11.4':
'@tauri-apps/cli-win32-arm64-msvc@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.11.4':
'@tauri-apps/cli-win32-ia32-msvc@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.11.4':
'@tauri-apps/cli-win32-x64-msvc@3.0.0-alpha.1':
optional: true
'@tauri-apps/cli@2.11.4':
'@tauri-apps/cli@3.0.0-alpha.1':
optionalDependencies:
'@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/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/plugin-clipboard-manager@2.3.2':
'@tauri-apps/plugin-clipboard-manager@3.0.0-alpha.0':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-dialog@2.7.2':
'@tauri-apps/plugin-dialog@3.0.0-alpha.0':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-fs@2.5.1':
'@tauri-apps/plugin-fs@3.0.0-alpha.0':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-http@2.6.0':
'@tauri-apps/plugin-http@3.0.0-alpha.0':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-opener@2.5.5':
'@tauri-apps/plugin-opener@3.0.0-alpha.0':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-updater@2.11.0':
'@tauri-apps/plugin-updater@3.0.0-alpha.0':
dependencies:
'@tauri-apps/api': 2.11.1
+12
View File
@@ -10,3 +10,15 @@ 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'
+1 -11
View File
@@ -78,7 +78,7 @@
},
{
"description": "Group PNPM version updates across package.json, tool-versions, workflows, and devcontainer",
"matchPackageNames": ["pnpm"],
"matchPackageNames": ["pnpm", "ghcr.io/devcontainers/features/pnpm"],
"groupName": "pnpm"
},
{
@@ -131,16 +131,6 @@
],
"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"],
+20 -19
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.5.4-rc"
version = "2.5.4"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -23,13 +23,15 @@ tauri-dev = ["dep:tauri-plugin-devtools"]
tokio-trace = ["clash-verge-logging/tokio-trace"]
chrome-trace = ["clash-verge-logging/chrome-trace"]
clippy = ["tauri/test"]
perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
# The perf-harness feature and its main.rs wiring are parked until
# tauri-plugin-wdio-webdriver publishes a tauri 3 release; restore both
# from git history together with the crate dependency.
[package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.6.3", features = [] }
tauri-build = { version = "3.0.0-alpha.0", features = [] }
[dependencies]
clash-verge-draft = { workspace = true }
@@ -42,12 +44,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 }
@@ -74,13 +76,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 = "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"
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"
zip = "8.6.0"
flate2 = "1.1.10"
reqwest_dav = "0.3.3"
@@ -91,13 +93,13 @@ getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.4.0"
tauri-plugin-notification = "3.0.0-alpha.0"
tokio-stream = "0.1.19"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.6.0"
tauri-plugin-http = "3.0.0-alpha.0"
tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.1", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" }
tauri-plugin-devtools = { version = "3.0.0-alpha.0", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "feat/tauri-v3" }
async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.1", version = "2.7.1", features = [
"client",
@@ -114,7 +116,6 @@ 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",
@@ -146,9 +147,9 @@ windows = { version = "0.62.2", features = ["Win32_Globalization"] }
windows-sys.workspace = true
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.5.1"
tauri-plugin-global-shortcut = "2.3.2"
tauri-plugin-updater = "2.11.0"
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"
[dev-dependencies]
criterion = { workspace = true }
-1
View File
@@ -70,7 +70,6 @@
"dialog:allow-open",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-register-all",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all",
"process:allow-exit",
-5
View File
@@ -57,11 +57,6 @@ 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) {
+1 -60
View File
@@ -185,42 +185,6 @@ 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,
@@ -586,30 +550,7 @@ impl CoreManager {
}
let result = match startup {
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::Service => self.start_core_by_service().await,
StartupDecision::Sidecar => self.start_core_by_sidecar().await,
StartupDecision::Wait => Ok(()),
};
+24 -47
View File
@@ -18,44 +18,6 @@ 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.
@@ -307,15 +269,30 @@ impl CoreManager {
#[cfg(target_os = "windows")]
{
use crate::constants::timing;
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(())
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")))
}
#[cfg(not(target_os = "windows"))]
-15
View File
@@ -323,21 +323,6 @@ 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();
+4 -37
View File
@@ -39,17 +39,6 @@ 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.
@@ -1087,25 +1076,6 @@ 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<()> {
@@ -1141,12 +1111,10 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
err_msg
);
start_owner_monitor();
return Err(ServiceStartRefusal {
code: response.code,
core_path: request.runtime.core_path,
message: err_msg,
}
.into());
bail!(
"failed to start Service core at {}: {err_msg}",
request.runtime.core_path
);
}
let result = response.data.context("Clash Verge Service 未返回会话信息")?;
@@ -1165,7 +1133,6 @@ 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,
+34 -32
View File
@@ -24,7 +24,7 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::time::Duration;
use tauri::{
AppHandle, Wry,
AppHandle, DynRuntime,
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
};
@@ -35,7 +35,7 @@ use menu_def::{MenuIds, MenuTexts};
// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑)
type ProxyMenuItem = (Option<Submenu<Wry>>, Vec<Box<dyn IsMenuItem<Wry>>>);
type ProxyMenuItem = (Option<Submenu<DynRuntime>>, Vec<Box<dyn IsMenuItem<DynRuntime>>>);
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
pub const TRAY_ID: &str = "clash-verge-rev-tray";
@@ -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<Wry>>> {
) -> Result<Vec<CheckMenuItem<DynRuntime>>> {
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<Wry>> {
let proxy_submenus: Vec<Submenu<Wry>> = {
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
) -> Vec<Submenu<DynRuntime>> {
let proxy_submenus: Vec<Submenu<DynRuntime>> = {
let mut submenus: Vec<(String, usize, Submenu<DynRuntime>)> = 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<Wry>> = all_proxies
let group_items: Vec<CheckMenuItem<DynRuntime>> = all_proxies
.iter()
.filter_map(|proxy_str| {
let is_selected = *proxy_str == now_proxy;
@@ -515,8 +515,10 @@ fn create_subcreate_proxy_menu_item(
let group_display_name = group_name.to_string();
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> =
group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>).collect();
let group_items_refs: Vec<&dyn IsMenuItem<DynRuntime>> = group_items
.iter()
.map(|item| item as &dyn IsMenuItem<DynRuntime>)
.collect();
if let Ok(submenu) = Submenu::with_id_and_items(
app_handle,
@@ -552,7 +554,7 @@ fn create_subcreate_proxy_menu_item(
fn create_proxy_menu_item(
app_handle: &AppHandle,
show_proxy_groups_inline: bool,
proxy_submenus: Vec<Submenu<Wry>>,
proxy_submenus: Vec<Submenu<DynRuntime>>,
proxies_text: &str,
) -> Result<ProxyMenuItem> {
let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline {
@@ -560,13 +562,13 @@ fn create_proxy_menu_item(
None,
proxy_submenus
.into_iter()
.map(|submenu| Box::new(submenu) as Box<dyn IsMenuItem<Wry>>)
.map(|submenu| Box::new(submenu) as Box<dyn IsMenuItem<DynRuntime>>)
.collect(),
)
} else if !proxy_submenus.is_empty() {
let proxy_submenu_refs: Vec<&dyn IsMenuItem<Wry>> = proxy_submenus
let proxy_submenu_refs: Vec<&dyn IsMenuItem<DynRuntime>> = proxy_submenus
.iter()
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
.map(|submenu| submenu as &dyn IsMenuItem<DynRuntime>)
.collect();
(
@@ -593,7 +595,7 @@ async fn create_tray_menu(
tun_mode_available: bool,
profiles_preview: Vec<IProfilePreview<'_>>,
options: TrayMenuOptions,
) -> Result<tauri::menu::Menu<Wry>> {
) -> Result<tauri::menu::Menu<DynRuntime>> {
let current_proxy_mode = mode.unwrap_or("");
let mut verge_settings = Config::verge().await.latest_arc();
@@ -647,12 +649,12 @@ async fn create_tray_menu(
let hotkeys = create_hotkeys(&verge_settings.hotkeys);
let profile_menu_items: Vec<CheckMenuItem<Wry>> = create_profile_menu_item(app_handle, profiles_preview)?;
let profile_menu_items: Vec<CheckMenuItem<DynRuntime>> = create_profile_menu_item(app_handle, profiles_preview)?;
let texts = MenuTexts::new();
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
let profile_menu_items_refs: Vec<&dyn IsMenuItem<DynRuntime>> = profile_menu_items
.iter()
.map(|item| item as &dyn IsMenuItem<Wry>)
.map(|item| item as &dyn IsMenuItem<DynRuntime>)
.collect();
let open_window = &MenuItem::with_id(
@@ -705,9 +707,9 @@ async fn create_tray_menu(
outbound_modes_label.as_str(),
true,
&[
rule_mode as &dyn IsMenuItem<Wry>,
global_mode as &dyn IsMenuItem<Wry>,
direct_mode as &dyn IsMenuItem<Wry>,
rule_mode as &dyn IsMenuItem<DynRuntime>,
global_mode as &dyn IsMenuItem<DynRuntime>,
direct_mode as &dyn IsMenuItem<DynRuntime>,
],
)?)
};
@@ -812,7 +814,7 @@ async fn create_tray_menu(
&texts.more,
true,
&[
copy_env as &dyn IsMenuItem<Wry>,
copy_env as &dyn IsMenuItem<DynRuntime>,
close_all_connections,
restart_clash,
restart_app,
@@ -829,13 +831,13 @@ async fn create_tray_menu(
let separator = &PredefinedMenuItem::separator(app_handle)?;
let mut menu_items: Vec<&dyn IsMenuItem<Wry>> = vec![open_window, separator];
let mut menu_items: Vec<&dyn IsMenuItem<DynRuntime>> = vec![open_window, separator];
if show_outbound_modes_inline {
menu_items.extend_from_slice(&[
rule_mode as &dyn IsMenuItem<Wry>,
global_mode as &dyn IsMenuItem<Wry>,
direct_mode as &dyn IsMenuItem<Wry>,
rule_mode as &dyn IsMenuItem<DynRuntime>,
global_mode as &dyn IsMenuItem<DynRuntime>,
direct_mode as &dyn IsMenuItem<DynRuntime>,
]);
} else if let Some(ref outbound_modes) = outbound_modes {
menu_items.push(outbound_modes);
@@ -845,7 +847,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<_>));
menu_items.extend(proxies_menu.iter().map(|item| item as &dyn IsMenuItem<DynRuntime>));
}
"inline" if !inline_proxy_items.is_empty() => {
menu_items.extend(inline_proxy_items.iter().map(|item| item.as_ref()));
@@ -855,14 +857,14 @@ async fn create_tray_menu(
menu_items.extend_from_slice(&[
separator,
system_proxy as &dyn IsMenuItem<Wry>,
tun_mode as &dyn IsMenuItem<Wry>,
system_proxy as &dyn IsMenuItem<DynRuntime>,
tun_mode as &dyn IsMenuItem<DynRuntime>,
separator,
lightweight_mode as &dyn IsMenuItem<Wry>,
open_dir as &dyn IsMenuItem<Wry>,
more as &dyn IsMenuItem<Wry>,
lightweight_mode as &dyn IsMenuItem<DynRuntime>,
open_dir as &dyn IsMenuItem<DynRuntime>,
more as &dyn IsMenuItem<DynRuntime>,
separator,
quit as &dyn IsMenuItem<Wry>,
quit as &dyn IsMenuItem<DynRuntime>,
]);
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
+3 -4
View File
@@ -39,7 +39,7 @@ mod app_init {
}
/// Setup plugins for the Tauri builder
pub fn setup_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
pub fn setup_plugins(builder: tauri::Builder) -> tauri::Builder {
#[allow(unused_mut)]
let mut builder = builder
.plugin(tauri_plugin_clash_verge_sysinfo::init())
@@ -116,7 +116,7 @@ mod app_init {
Ok(())
}
pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static {
tauri::generate_handler![
tauri_plugin_clash_verge_sysinfo::commands::get_system_info,
tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime,
@@ -163,7 +163,6 @@ 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,
@@ -256,7 +255,7 @@ pub fn run() -> std::process::ExitCode {
#[cfg(target_os = "linux")]
utils::linux::workarounds::apply_wayland_webkit_fix();
let builder = app_init::setup_plugins(tauri::Builder::default())
let builder = app_init::setup_plugins(tauri::Builder::default().runtime(tauri_runtime_wry::Wry::default()))
.setup(|app| {
// Logger may not be ready yet, so mirror setup panics to stderr.
fn log_setup_panic(stage: &str, panic: Box<dyn std::any::Any + Send>) {
-11
View File
@@ -1,19 +1,8 @@
#![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);
+4 -1
View File
@@ -132,7 +132,10 @@ pub fn take_webview_needs_reload() -> bool {
/// reload visible webviews immediately, and defer a hidden main-window reload until activation.
/// Registering this callback replaces Tauri's default automatic reload.
#[cfg(target_os = "macos")]
pub fn on_web_content_process_terminated(webview: &tauri::Webview) {
pub fn on_web_content_process_terminated(
webview: &tauri::Webview,
_termination: &tauri::webview::WebContentProcessTermination,
) {
if handle::Handle::global().is_exiting() {
return;
}
+9 -9
View File
@@ -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, Wry};
use tauri::{Manager as _, WebviewWindow};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowOperationResult {
@@ -50,7 +50,7 @@ impl WindowManager {
handle::Handle::global().set_activation_policy_regular();
}
fn get_main_window_with_state() -> (Option<WebviewWindow<Wry>>, WindowState) {
fn get_main_window_with_state() -> (Option<WebviewWindow>, WindowState) {
let Some(window) = Self::get_main_window() else {
return (None, WindowState::NotExist);
};
@@ -76,7 +76,7 @@ impl WindowManager {
Self::get_main_window_with_state().1
}
pub fn get_main_window() -> Option<WebviewWindow<Wry>> {
pub fn get_main_window() -> Option<WebviewWindow> {
let app_handle = handle::Handle::app_handle();
app_handle.get_webview_window("main")
}
@@ -147,7 +147,7 @@ impl WindowManager {
}
}
fn hide_main_window(window: Option<&WebviewWindow<Wry>>) -> WindowOperationResult {
fn hide_main_window(window: Option<&WebviewWindow>) -> WindowOperationResult {
logging!(debug, Type::Window, "窗口可见,将隐藏窗口");
if let Some(window) = window {
match window.close() {
@@ -166,7 +166,7 @@ impl WindowManager {
}
}
fn activate_existing_main_window(window: Option<&WebviewWindow<Wry>>) -> WindowOperationResult {
fn activate_existing_main_window(window: Option<&WebviewWindow>) -> WindowOperationResult {
logging!(debug, Type::Window, "窗口存在但被隐藏或最小化,将激活窗口");
if let Some(window) = window {
Self::activate_window(window)
@@ -176,7 +176,7 @@ impl WindowManager {
}
}
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult {
fn activate_window(window: &WebviewWindow) -> WindowOperationResult {
logging!(debug, Type::Window, "开始激活窗口");
#[cfg(target_os = "macos")]
Self::set_macos_activation_policy_regular();
@@ -233,15 +233,15 @@ impl WindowManager {
}
}
pub fn is_main_window_visible(window: Option<&WebviewWindow<Wry>>) -> bool {
pub fn is_main_window_visible(window: Option<&WebviewWindow>) -> bool {
window.map(|w| w.is_visible().unwrap_or(false)).unwrap_or(false)
}
pub fn is_main_window_focused(window: Option<&WebviewWindow<Wry>>) -> bool {
pub fn is_main_window_focused(window: Option<&WebviewWindow>) -> bool {
window.map(|w| w.is_focused().unwrap_or(false)).unwrap_or(false)
}
fn is_main_window_minimized(window: Option<&WebviewWindow<Wry>>) -> bool {
fn is_main_window_minimized(window: Option<&WebviewWindow>) -> bool {
window.map(|w| w.is_minimized().unwrap_or(false)).unwrap_or(false)
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "2.5.4-rc",
"version": "2.5.4",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
+3 -16
View File
@@ -13,24 +13,12 @@ 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,
measureElement,
dataIndex,
children,
style,
} = props
const { id, index, group, disabled, children, style } = props
const { ref, handleRef, sortable, isDragging } = useSortable({
id,
index,
@@ -42,9 +30,8 @@ export const SortableItem = (props: SortableItemProps) => {
(element: HTMLDivElement | null) => {
handleRef(element?.querySelector('[data-sortable-handle]') ?? null)
ref(element)
measureElement?.(element)
},
[handleRef, ref, measureElement],
[handleRef, ref],
)
const mergedStyle: CSSProperties = {
@@ -66,7 +53,7 @@ export const SortableItem = (props: SortableItemProps) => {
}
return (
<div ref={setRef} data-index={dataIndex} style={mergedStyle}>
<div ref={setRef} style={mergedStyle}>
{children}
</div>
)
@@ -20,7 +20,6 @@ import {
type ServiceRequest,
type ServiceRequestReason,
} from '@/services/service-request'
import getSystem from '@/utils/get-system'
type Remedy = 'installAndRestart' | 'restartOnly'
@@ -99,25 +98,19 @@ export const SysproxyPrivilegeDialog = () => {
setStep('restarting')
await restartCore()
// Verify that restart actually moved the core into the service.
const runState = await getRuntimeState()
const usingAdminFallback =
getSystem() === 'windows' &&
runState.mode === 'Sidecar' &&
runState.isAdmin &&
runState.sidecarAllowed &&
runState.service === 'unavailable'
if (runState.mode === 'Service' || usingAdminFallback) {
if (runState.mode === 'Service') {
// Retry the original request now that the service can apply it.
if (restoring !== undefined) {
setStep('applying')
await patchVergeConfig(restoring)
}
if (!usingAdminFallback) {
showNotice.success(
restoring === undefined
? 'settings.sections.proxyControl.messages.installedCheckProxy'
: 'settings.sections.proxyControl.messages.installedProxyRestored',
)
}
showNotice.success(
restoring === undefined
? 'settings.sections.proxyControl.messages.installedCheckProxy'
: 'settings.sections.proxyControl.messages.installedProxyRestored',
)
close()
} else {
showNotice.error(
@@ -1,184 +0,0 @@
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>
)
}
+170 -71
View File
@@ -1,4 +1,12 @@
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,7 +43,13 @@ import {
import { Controller, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { BaseSearchBox, MonacoEditor, Switch } from '@/components/base'
import {
BaseSearchBox,
MonacoEditor,
SortableItem,
Switch,
VirtualList,
} from '@/components/base'
import { GroupItem } from '@/components/profile/group-item'
import {
getNetworkInterfaces,
@@ -49,12 +63,6 @@ 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
@@ -195,71 +203,106 @@ export const GroupsEditorViewer = (props: Props) => {
[appendSeq, match],
)
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)
const renderItem = (index: number): React.ReactNode => {
const shift = filteredPrependSeq.length > 0 ? 1 : 0
if (filteredPrependSeq.length > 0 && index === 0) {
return (
<GroupItem
type={isDeleted ? 'delete' : 'original'}
group={item}
onDelete={() => {
if (isDeleted) {
setDeleteSeq(deleteSeq.filter((v) => v !== item.name))
} else {
setDeleteSeq((prev) => [...prev, item.name])
<>
{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'
}
}}
/>
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>
)
}
if (category === 'prepend') {
} else {
return (
<GroupItem
type="prepend"
group={item}
onDelete={() => {
setPrependSeq(prependSeq.filter((v) => v.name !== item.name))
}}
/>
<>
{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>
)
})}
</>
)
}
return (
<GroupItem
type="append"
group={item}
onDelete={() => {
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
}}
/>
)
}
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)
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,
)
if (
activeRealIndex < 0 ||
overRealIndex < 0 ||
@@ -268,9 +311,55 @@ export const GroupsEditorViewer = (props: Props) => {
return
}
setList(arrayMove(list, activeRealIndex, overRealIndex))
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))
}
// 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)
@@ -1063,12 +1152,22 @@ export const GroupsEditorViewer = (props: Props) => {
}}
>
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
<GroupedVirtualList
items={items}
renderItem={renderItem}
onReorder={onReorder}
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
/>
<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>
</List>
</>
) : (
+166 -71
View File
@@ -1,4 +1,12 @@
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,
@@ -27,7 +35,12 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { BaseSearchBox, MonacoEditor } from '@/components/base'
import {
BaseSearchBox,
MonacoEditor,
SortableItem,
VirtualList,
} from '@/components/base'
import { ProxyItem } from '@/components/profile/proxy-item'
import { readProfileFile, saveProfileFile } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
@@ -37,12 +50,6 @@ 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
@@ -100,71 +107,106 @@ export const ProxiesEditorViewer = (props: Props) => {
[appendSeq, match],
)
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)
const renderItem = (index: number): React.ReactNode => {
const shift = filteredPrependSeq.length > 0 ? 1 : 0
if (filteredPrependSeq.length > 0 && index === 0) {
return (
<ProxyItem
type={isDeleted ? 'delete' : 'original'}
proxy={item}
onDelete={() => {
if (isDeleted) {
setDeleteSeq(deleteSeq.filter((v) => v !== item.name))
} else {
setDeleteSeq((prev) => [...prev, item.name])
<>
{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'
}
}}
/>
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>
)
}
if (category === 'prepend') {
} else {
return (
<ProxyItem
type="prepend"
proxy={item}
onDelete={() => {
setPrependSeq(prependSeq.filter((v) => v.name !== item.name))
}}
/>
<>
{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>
)
})}
</>
)
}
return (
<ProxyItem
type="append"
proxy={item}
onDelete={() => {
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
}}
/>
)
}
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)
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,
)
if (
activeRealIndex < 0 ||
overRealIndex < 0 ||
@@ -173,9 +215,52 @@ export const ProxiesEditorViewer = (props: Props) => {
return
}
setList(arrayMove(list, activeRealIndex, overRealIndex))
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))
}
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[] = []
@@ -437,12 +522,22 @@ export const ProxiesEditorViewer = (props: Props) => {
}}
>
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
<GroupedVirtualList
items={items}
renderItem={renderItem}
onReorder={onReorder}
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
/>
<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>
</List>
</>
) : (
+180 -86
View File
@@ -1,4 +1,11 @@
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,
@@ -29,7 +36,13 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { BaseSearchBox, MonacoEditor, Switch } from '@/components/base'
import {
BaseSearchBox,
MonacoEditor,
SortableItem,
Switch,
VirtualList,
} from '@/components/base'
import { RuleItem } from '@/components/profile/rule-item'
import { readProfileFile, saveProfileFile } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
@@ -41,12 +54,6 @@ 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
@@ -295,83 +302,117 @@ export const RulesEditorViewer = (props: Props) => {
[appendSeq, match],
)
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)
const renderItem = (index: number): React.ReactNode => {
const shift = filteredPrependSeq.length > 0 ? 1 : 0
if (filteredPrependSeq.length > 0 && index === 0) {
return (
<RuleItem
type={isDeleted ? 'delete' : 'original'}
ruleRaw={item}
onDelete={() => {
if (isDeleted) {
setDeleteSeq(deleteSeq.filter((v) => v !== item))
} else {
setDeleteSeq((prev) => [...prev, item])
}
}}
/>
)
}
if (category === 'prepend') {
return (
<RuleItem
type="prepend"
ruleRaw={item}
onDelete={() => {
setPrependSeq(prependSeq.filter((v) => v !== item))
}}
onAppend={() => {
setAppendSeq((prev) =>
prev.includes(item) ? prev : [...prev, item],
<>
{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>
)
setPrependSeq(prependSeq.filter((v) => v !== item))
}}
/>
})}
</>
)
} 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'
}
ruleRaw={filteredRuleList[newIndex]}
onDelete={() => {
if (deleteSeq.includes(filteredRuleList[newIndex])) {
setDeleteSeq(
deleteSeq.filter((v) => v !== filteredRuleList[newIndex]),
)
} else {
setDeleteSeq((prev) => [...prev, filteredRuleList[newIndex]])
}
}}
/>
</Box>
)
} else {
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>
)
})}
</>
)
}
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 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)
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,
)
if (
activeRealIndex < 0 ||
overRealIndex < 0 ||
@@ -380,9 +421,52 @@ export const RulesEditorViewer = (props: Props) => {
return
}
setList(arrayMove(list, activeRealIndex, overRealIndex))
setPrependSeq(reorder(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(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)
@@ -770,12 +854,22 @@ export const RulesEditorViewer = (props: Props) => {
}}
>
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
<GroupedVirtualList
items={items}
renderItem={renderItem}
onReorder={onReorder}
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
/>
<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>
</List>
</>
) : (
-1
View File
@@ -683,7 +683,6 @@
},
"clashService": {
"installSuccess": "تم تثبيت الخدمة بنجاح",
"sidecarFallback": "تعذّر على الخدمة تشغيل النواة بسبب أذونات المسار. تم التبديل إلى وضع المسؤول لمواصلة التشغيل.",
"uninstallSuccess": "تم إلغاء تثبيت الخدمة بنجاح"
},
"updater": {
-1
View File
@@ -683,7 +683,6 @@
},
"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": {
-1
View File
@@ -683,7 +683,6 @@
},
"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": {
-1
View File
@@ -683,7 +683,6 @@
},
"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": {
-1
View File
@@ -683,7 +683,6 @@
},
"clashService": {
"installSuccess": "سرویس با موفقیت نصب شد",
"sidecarFallback": "سرویس به‌دلیل مجوزهای مسیر نتوانست هسته را اجرا کند. برای ادامهٔ اجرا به حالت مدیر تغییر یافت.",
"uninstallSuccess": "سرویس با موفقیت حذف نصب شد"
},
"updater": {
-1
View File
@@ -683,7 +683,6 @@
},
"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": {
-1
View File
@@ -683,7 +683,6 @@
},
"clashService": {
"installSuccess": "サービスのインストールに成功しました。",
"sidecarFallback": "パスの権限によりサービスがコアを起動できなかったため、管理者モードに切り替えて動作を継続しています。",
"uninstallSuccess": "サービスのアンインストールに成功しました。"
},
"updater": {
-1
View File
@@ -683,7 +683,6 @@
},
"clashService": {
"installSuccess": "서비스가 설치되었습니다",
"sidecarFallback": "경로 권한 문제로 서비스가 코어를 시작하지 못해 관리자 모드로 전환하여 계속 실행합니다.",
"uninstallSuccess": "서비스가 제거되었습니다"
},
"updater": {
-1
View File
@@ -683,7 +683,6 @@
},
"clashService": {
"installSuccess": "Служба успешно установлена",
"sidecarFallback": "Служба не смогла запустить ядро из-за прав доступа к пути. Работа продолжена в режиме администратора.",
"uninstallSuccess": "Служба успешно удалена"
},
"updater": {
-1
View File
@@ -683,7 +683,6 @@
},
"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": {
-1
View File
@@ -683,7 +683,6 @@
},
"clashService": {
"installSuccess": "Сервис уңышлы урнаштырылды",
"sidecarFallback": "Хезмәт юлга керү хокуклары аркасында төшне эшләтеп җибәрә алмады. Эшне дәвам итү өчен администратор режимына күчелде.",
"uninstallSuccess": "Сервис уңышлы салдырылды"
},
"updater": {
-1
View File
@@ -683,7 +683,6 @@
},
"clashService": {
"installSuccess": "已成功安装服务",
"sidecarFallback": "服务因路径权限问题无法启动内核,已切回管理员模式继续运行。",
"uninstallSuccess": "已成功卸载服务"
},
"updater": {
-1
View File
@@ -683,7 +683,6 @@
},
"clashService": {
"installSuccess": "已成功安裝服務",
"sidecarFallback": "服務因路徑權限問題無法啟動核心,已切回管理員模式繼續執行。",
"uninstallSuccess": "已成功解除安裝服務"
},
"updater": {
@@ -13,10 +13,7 @@ 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(),
takeServiceFallbackNotice: vi.fn().mockResolvedValue(false),
}))
vi.mock('@/services/cmds', () => ({ takeDnsOverrideNotice: vi.fn() }))
vi.mock('@/services/notice-service', () => ({
showNotice: { info: vi.fn() },
}))
@@ -64,7 +64,6 @@ export const useLayoutEvents = (
() => {
revalidateKeys(['getRuntimeState', 'getVergeConfig'])
handleNotice(['dns_override::auto_disabled', ''])
handleNotice(['service_core::sidecar_fallback', ''])
},
)
}, [handleNotice])
@@ -1,7 +1,4 @@
import {
takeDnsOverrideNotice,
takeServiceFallbackNotice,
} from '@/services/cmds'
import { takeDnsOverrideNotice } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
type NavigateFunction = (path: string, options?: any) => void
@@ -28,22 +25,6 @@ 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) => {
-4
View File
@@ -182,10 +182,6 @@ 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
-1
View File
@@ -793,7 +793,6 @@ 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',
-1
View File
@@ -831,7 +831,6 @@ export interface TranslationResources {
}
clashService: {
installSuccess: string
sidecarFallback: string
uninstallSuccess: string
}
updater: {