Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot]andTunglies 366893a368 docs(readme): clarify development service prerequisite
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-11-03 13:15:57 +00:00
108 changed files with 1728 additions and 3702 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"features": { "features": {
"ghcr.io/devcontainers/features/node:2": { "ghcr.io/devcontainers/features/node:2": {
"version": "26.13.0", "version": "26.13.0",
"pnpmVersion": "12.18.0" "pnpmVersion": "12.16.1"
}, },
"ghcr.io/devcontainers/features/rust:1": { "ghcr.io/devcontainers/features/rust:1": {
"version": "1.99.1", "version": "1.99.1",
+5 -5
View File
@@ -43,7 +43,7 @@ jobs:
run: bash ./scripts/extract_update_logs.sh run: bash ./scripts/extract_update_logs.sh
shell: bash shell: bash
- uses: pnpm/action-setup@v6.1.1 - uses: pnpm/action-setup@v6.1.0
name: Install pnpm name: Install pnpm
with: with:
run_install: false run_install: false
@@ -174,7 +174,7 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- uses: pnpm/action-setup@v6.1.1 - uses: pnpm/action-setup@v6.1.0
name: Install pnpm name: Install pnpm
with: with:
run_install: false run_install: false
@@ -255,7 +255,7 @@ jobs:
cache-workspace-crates: true cache-workspace-crates: true
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v6.1.1 uses: pnpm/action-setup@v6.1.0
with: with:
run_install: false run_install: false
@@ -388,7 +388,7 @@ jobs:
cache-workspace-crates: true cache-workspace-crates: true
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v6.1.1 uses: pnpm/action-setup@v6.1.0
with: with:
run_install: false run_install: false
@@ -481,7 +481,7 @@ jobs:
with: with:
node-version: '26.13.0' node-version: '26.13.0'
- uses: pnpm/action-setup@v6.1.1 - uses: pnpm/action-setup@v6.1.0
name: Install pnpm name: Install pnpm
with: with:
run_install: false run_install: false
+1 -9
View File
@@ -4,13 +4,13 @@ on:
pull_request: pull_request:
paths: paths:
- "src/**" - "src/**"
- "scripts/**"
- "**/*.js" - "**/*.js"
- "**/*.ts" - "**/*.ts"
- "**/*.tsx" - "**/*.tsx"
- "**/*.css" - "**/*.css"
- "**/*.scss" - "**/*.scss"
- "**/*.json" - "**/*.json"
- "**/*.md"
- "pnpm-lock.yaml" - "pnpm-lock.yaml"
- "pnpm-workspace.yaml" - "pnpm-workspace.yaml"
- "vite.config.*" - "vite.config.*"
@@ -41,14 +41,6 @@ jobs:
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Check generated i18n types are up to date
run: |
pnpm i18n:types
if ! git diff --exit-code -- src/types/generated; then
echo "::error::src/types/generated is stale; run pnpm i18n:types and commit the result"
exit 1
fi
- name: Run Prettier check - name: Run Prettier check
run: pnpm format:check run: pnpm format:check
+1 -1
View File
@@ -1,3 +1,3 @@
nodejs 26.13.0 nodejs 26.13.0
pnpm 12.18.0 pnpm 12.16.1
rust 1.99.1 rust 1.99.1
Generated
+153 -190
View File
@@ -150,7 +150,7 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation", "objc2-foundation",
"parking_lot 0.12.5", "parking_lot",
"percent-encoding", "percent-encoding",
"windows-sys 0.60.2", "windows-sys 0.60.2",
"wl-clipboard-rs", "wl-clipboard-rs",
@@ -159,9 +159,9 @@ dependencies = [
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.9.3" version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e" checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
dependencies = [ dependencies = [
"rustversion", "rustversion",
] ]
@@ -1089,6 +1089,7 @@ dependencies = [
"async-trait", "async-trait",
"backon", "backon",
"base64 0.23.1", "base64 0.23.1",
"bitflags 2.13.2",
"boa_engine", "boa_engine",
"bytes", "bytes",
"chrono", "chrono",
@@ -1099,6 +1100,7 @@ dependencies = [
"clash-verge-media-unlock", "clash-verge-media-unlock",
"clash-verge-signal", "clash-verge-signal",
"clash_verge_service_ipc", "clash_verge_service_ipc",
"criterion",
"dark-light", "dark-light",
"deelevate", "deelevate",
"dirs 7.0.0", "dirs 7.0.0",
@@ -1115,8 +1117,9 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation",
"open", "open",
"parking_lot 0.13.0", "parking_lot",
"percent-encoding", "percent-encoding",
"regex",
"reqwest 0.13.5", "reqwest 0.13.5",
"reqwest_dav", "reqwest_dav",
"rfd", "rfd",
@@ -1146,6 +1149,7 @@ dependencies = [
"tauri-plugin-mihomo", "tauri-plugin-mihomo",
"tauri-plugin-notification", "tauri-plugin-notification",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-wdio-webdriver", "tauri-plugin-wdio-webdriver",
@@ -1153,6 +1157,7 @@ dependencies = [
"tauri-specta", "tauri-specta",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tokio-stream",
"tokio-util", "tokio-util",
"tracing", "tracing",
"warp", "warp",
@@ -1170,7 +1175,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"criterion", "criterion",
"parking_lot 0.13.0", "parking_lot",
"tokio", "tokio",
] ]
@@ -1193,7 +1198,7 @@ dependencies = [
"anyhow", "anyhow",
"flexi_logger", "flexi_logger",
"log", "log",
"parking_lot 0.13.0", "parking_lot",
"tracing-estuary", "tracing-estuary",
] ]
@@ -1250,8 +1255,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "facfbc410a385e0b0cbca8179dd9e363ed70e8455960bf7545137e90ca753b66" checksum = "facfbc410a385e0b0cbca8179dd9e363ed70e8455960bf7545137e90ca753b66"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
"lock_api 0.4.14", "lock_api",
"parking_lot_core 0.9.12", "parking_lot_core",
"polonius-the-crab", "polonius-the-crab",
] ]
@@ -1328,7 +1333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1762,18 +1767,8 @@ version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [ dependencies = [
"darling_core 0.23.0", "darling_core",
"darling_macro 0.23.0", "darling_macro",
]
[[package]]
name = "darling"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
dependencies = [
"darling_core 0.24.1",
"darling_macro 0.24.1",
] ]
[[package]] [[package]]
@@ -1789,41 +1784,17 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "darling_core"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 3.0.5",
]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.23.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [ dependencies = [
"darling_core 0.23.0", "darling_core",
"quote", "quote",
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "darling_macro"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [
"darling_core 0.24.1",
"quote",
"syn 3.0.5",
]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.2.1" version = "6.2.1"
@@ -1833,9 +1804,9 @@ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
"hashbrown 0.14.5", "hashbrown 0.14.5",
"lock_api 0.4.14", "lock_api",
"once_cell", "once_cell",
"parking_lot_core 0.9.12", "parking_lot_core",
] ]
[[package]] [[package]]
@@ -1913,9 +1884,9 @@ dependencies = [
[[package]] [[package]]
name = "devtools-core" name = "devtools-core"
version = "0.4.2" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e85926bda763dd434fcfbcfb24af4b2c1a8c3e3c34bcd5d844c48f655cca51" checksum = "c53d416e473377276435d49d731a75d253b96ee4339831cd85653702b94d8298"
dependencies = [ dependencies = [
"bytes", "bytes",
"devtools-wire-format", "devtools-wire-format",
@@ -2043,7 +2014,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users 0.5.2", "redox_users 0.5.2",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -2302,7 +2273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -2818,7 +2789,7 @@ dependencies = [
"gobject-sys 0.21.5", "gobject-sys 0.21.5",
"libc", "libc",
"system-deps 7.0.8", "system-deps 7.0.8",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -3372,7 +3343,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.62.2", "windows-core 0.61.2",
] ]
[[package]] [[package]]
@@ -3386,12 +3357,12 @@ dependencies = [
[[package]] [[package]]
name = "ico" name = "ico"
version = "0.6.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"png", "png 0.17.16",
] ]
[[package]] [[package]]
@@ -3577,7 +3548,7 @@ dependencies = [
"byteorder-lite", "byteorder-lite",
"moxcms", "moxcms",
"num-traits", "num-traits",
"png", "png 0.18.1",
"tiff", "tiff",
] ]
@@ -3886,7 +3857,7 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"libc", "libc",
"parking_lot 0.12.5", "parking_lot",
"path-tree", "path-tree",
"pin-project-lite", "pin-project-lite",
"rand 0.10.2", "rand 0.10.2",
@@ -4046,20 +4017,11 @@ dependencies = [
"scopeguard", "scopeguard",
] ]
[[package]]
name = "lock_api"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65d91b22da179613dcf4d6ec8349563f52a363f6849e03b00dd0a185ec4a59af"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.35" version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]] [[package]]
name = "lru-slab" name = "lru-slab"
@@ -4069,9 +4031,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "lzma-rust2" name = "lzma-rust2"
version = "0.24.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490750560fa7851fbbf5d3ab1a8750f09350ed361ed8aed85b1e1e40587812ee" checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
dependencies = [ dependencies = [
"sha2 0.11.1", "sha2 0.11.1",
] ]
@@ -4249,10 +4211,10 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation",
"once_cell", "once_cell",
"png", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.20", "thiserror 2.0.20",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -4419,7 +4381,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -4689,6 +4651,17 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-open-directory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d"
dependencies = [
"objc2",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]] [[package]]
name = "objc2-osa-kit" name = "objc2-osa-kit"
version = "0.3.2" version = "0.3.2"
@@ -4790,7 +4763,7 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [ dependencies = [
"parking_lot_core 0.9.12", "parking_lot_core",
] ]
[[package]] [[package]]
@@ -4813,9 +4786,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "open" name = "open"
version = "5.4.5" version = "5.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95" checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
dependencies = [ dependencies = [
"dunce", "dunce",
"is-wsl", "is-wsl",
@@ -4885,7 +4858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -4949,18 +4922,8 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [ dependencies = [
"lock_api 0.4.14", "lock_api",
"parking_lot_core 0.9.12", "parking_lot_core",
]
[[package]]
name = "parking_lot"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9cf937ca4e1d0b7578784559063422fc39ae7c5a00f02471f2614d5486fd226"
dependencies = [
"lock_api 0.5.0",
"parking_lot_core 0.10.0",
] ]
[[package]] [[package]]
@@ -4971,19 +4934,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.5.18", "redox_syscall",
"smallvec",
"windows-link 0.2.1",
]
[[package]]
name = "parking_lot_core"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cfff060068cc18aa7815010fcaf80c68c7fb9e85fc7f5c0f9bf142c21d19826"
dependencies = [
"libc",
"redox_syscall 0.9.4",
"smallvec", "smallvec",
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
@@ -5274,6 +5225,19 @@ dependencies = [
"plotters-backend", "plotters-backend",
] ]
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.9",
]
[[package]] [[package]]
name = "png" name = "png"
version = "0.18.1" version = "0.18.1"
@@ -5559,7 +5523,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -5703,15 +5667,6 @@ dependencies = [
"bitflags 2.13.2", "bitflags 2.13.2",
] ]
[[package]]
name = "redox_syscall"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e"
dependencies = [
"bitflags 2.13.2",
]
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.4.6" version = "0.4.6"
@@ -5909,9 +5864,9 @@ dependencies = [
[[package]] [[package]]
name = "rfd" name = "rfd"
version = "0.17.2" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [ dependencies = [
"block2", "block2",
"dispatch2", "dispatch2",
@@ -5919,18 +5874,16 @@ dependencies = [
"gobject-sys 0.18.0", "gobject-sys 0.18.0",
"gtk-sys", "gtk-sys",
"js-sys", "js-sys",
"libc",
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation",
"percent-encoding",
"raw-window-handle", "raw-window-handle",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -6074,7 +6027,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6087,7 +6040,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.12.1", "linux-raw-sys 0.12.1",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6146,7 +6099,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6524,7 +6477,7 @@ version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
dependencies = [ dependencies = [
"darling 0.23.0", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.119", "syn 2.0.119",
@@ -6749,9 +6702,9 @@ dependencies = [
[[package]] [[package]]
name = "smartstring" name = "smartstring"
version = "2.0.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a155190f9c039a1e9b6e92efbfa50ae68daad98a0fd6d5521a5a09ef782383e8" checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"serde", "serde",
@@ -6766,7 +6719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -6784,7 +6737,7 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-quartz-core", "objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"redox_syscall 0.5.18", "redox_syscall",
"tracing", "tracing",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
@@ -6819,9 +6772,9 @@ dependencies = [
[[package]] [[package]]
name = "specta" name = "specta"
version = "2.0.0-rc.26" version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed155e448f42fc4be7a5cbc12bd43f6c549981f7c32fcba429ff6bc57cc4f30c" checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50"
dependencies = [ dependencies = [
"paste", "paste",
"rustc_version 0.4.1", "rustc_version 0.4.1",
@@ -6831,9 +6784,9 @@ dependencies = [
[[package]] [[package]]
name = "specta-macros" name = "specta-macros"
version = "2.0.0-rc.26" version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b34b2516e16a9ddd3dc4e73cd6454cfe21fe209f8cb5a6f37346fe3774d759b3" checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"proc-macro2", "proc-macro2",
@@ -6843,9 +6796,9 @@ dependencies = [
[[package]] [[package]]
name = "specta-serde" name = "specta-serde"
version = "0.0.13" version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4983177ed0eb28235080261953de5f9ebae8a2d107851b5852420bd2ab246245" checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029"
dependencies = [ dependencies = [
"specta", "specta",
"specta-macros", "specta-macros",
@@ -6853,18 +6806,18 @@ dependencies = [
[[package]] [[package]]
name = "specta-typescript" name = "specta-typescript"
version = "0.0.13" version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ecbe75dc1d27b36141b50b02b7c73bed00945710cee6edffa86d296a159a6f9" checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0"
dependencies = [ dependencies = [
"specta", "specta",
] ]
[[package]] [[package]]
name = "specta-util" name = "specta-util"
version = "0.0.13" version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bf531215ea70fba8264edecf98f12600462cf359f2038602a47aeda5ecd5b8" checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502"
dependencies = [ dependencies = [
"specta", "specta",
] ]
@@ -6894,7 +6847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
dependencies = [ dependencies = [
"new_debug_unreachable", "new_debug_unreachable",
"parking_lot 0.12.5", "parking_lot",
"phf_shared 0.13.1", "phf_shared 0.13.1",
"precomputed-hash", "precomputed-hash",
] ]
@@ -7030,15 +6983,16 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.40.1" version = "0.39.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8" checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
dependencies = [ dependencies = [
"libc", "libc",
"memchr", "memchr",
"ntapi", "ntapi",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-io-kit", "objc2-io-kit",
"objc2-open-directory",
"windows 0.62.2", "windows 0.62.2",
] ]
@@ -7140,7 +7094,7 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation",
"objc2-ui-kit", "objc2-ui-kit",
"parking_lot 0.12.5", "parking_lot",
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
"tao-macros", "tao-macros",
@@ -7194,9 +7148,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.12.5" version = "2.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072" checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -7249,9 +7203,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614" checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -7272,16 +7226,16 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316" checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
dependencies = [ dependencies = [
"base64 0.23.1", "base64 0.23.1",
"brotli", "brotli",
"ico", "ico",
"json-patch", "json-patch",
"plist", "plist",
"png", "png 0.18.1",
"proc-macro2", "proc-macro2",
"quote", "quote",
"semver 1.0.28", "semver 1.0.28",
@@ -7299,9 +7253,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1" checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -7347,7 +7301,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"deelevate", "deelevate",
"libc", "libc",
"parking_lot 0.13.0", "parking_lot",
"serde", "serde",
"specta", "specta",
"sysinfo", "sysinfo",
@@ -7372,9 +7326,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-deep-link" name = "tauri-plugin-deep-link"
version = "2.6.3" version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab551d9bec27bb79d1c7fe5ed38c5cfd1cebb86efa745acc65f5853f345f6e56" checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
dependencies = [ dependencies = [
"dunce", "dunce",
"plist", "plist",
@@ -7393,9 +7347,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-devtools" name = "tauri-plugin-devtools"
version = "2.2.3" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299a62101a16c14b914ebfaf529b4b64ad9ca534a0ece1c21fc9308d1e3ba9db" checksum = "1acb4d1b8490ee79166b72c3f8c989e066650a11aa94dfd0d71fb05c61cdbed0"
dependencies = [ dependencies = [
"async-stream", "async-stream",
"bytes", "bytes",
@@ -7420,9 +7374,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.8.4" version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce0fb9c2b6d34a9720627ed930ebc8af26f61e648695aecaeb03d5c33c5896f5" checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@@ -7526,13 +7480,13 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-notification" name = "tauri-plugin-notification"
version = "2.8.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc6c194a95a535abdfbe3bc1af9a43935b8b9b29c216b8e71af8b59132703cf" checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
dependencies = [ dependencies = [
"log", "log",
"notify-rust", "notify-rust",
"rand 0.10.2", "rand 0.9.5",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
@@ -7567,10 +7521,20 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-process"
version = "2.6.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7375b25f72f70f6168f27f1ba3a413e30eaa0a2098cff53a4046ebfe9e2f142d" checksum = "74ae3d8549ae564c0397377373e262f340908eb23a5b9fbe03f2a00007dc0038"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"libc", "libc",
@@ -7625,9 +7589,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-wdio-webdriver" name = "tauri-plugin-wdio-webdriver"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a775b0b59f6f12b38d1f28667311a89cd07f532f540e27d65c6eadfba1156d8" checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -7698,9 +7662,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.12.3" version = "2.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f" checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@@ -7708,6 +7672,7 @@ dependencies = [
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
"once_cell",
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
"softbuffer", "softbuffer",
@@ -7724,12 +7689,11 @@ dependencies = [
[[package]] [[package]]
name = "tauri-specta" name = "tauri-specta"
version = "2.0.0-rc.26" version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe71fc209fa4c49f959644f1ae2d70f72d5675bcb3d3ad8518122fa7b330994e" checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"paste",
"serde", "serde",
"serde_json", "serde_json",
"specta", "specta",
@@ -7743,15 +7707,15 @@ dependencies = [
[[package]] [[package]]
name = "tauri-specta-macros" name = "tauri-specta-macros"
version = "2.0.0-rc.26" version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf5689a1dbf2611e2351b79ba042ca089d0a3ce45274695e79084cd732a5aec" checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012"
dependencies = [ dependencies = [
"darling 0.24.1", "darling",
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.5", "syn 2.0.119",
] ]
[[package]] [[package]]
@@ -7821,10 +7785,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.3", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix 1.1.4", "rustix 1.1.4",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -8077,9 +8041,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.54.1" version = "1.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@@ -8435,7 +8399,7 @@ dependencies = [
"flexi_logger", "flexi_logger",
"log", "log",
"nu-ansi-term", "nu-ansi-term",
"parking_lot 0.12.5", "parking_lot",
"tokio", "tokio",
"tracing", "tracing",
"tracing-log", "tracing-log",
@@ -8487,10 +8451,10 @@ dependencies = [
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation", "objc2-foundation",
"once_cell", "once_cell",
"png", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.20", "thiserror 2.0.20",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -8599,7 +8563,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [ dependencies = [
"memoffset", "memoffset",
"tempfile", "tempfile",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -9045,9 +9009,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.11" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
@@ -9141,7 +9105,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -9162,7 +9126,7 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-quartz-core", "objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"windows-sys 0.61.2", "windows-sys 0.60.2",
"windows-version", "windows-version",
] ]
@@ -9737,7 +9701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -10069,9 +10033,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "9.3.0" version = "9.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaeb7f16d2605b49aef586996ded29e65642fab7259a3077e537351df6d80426" checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
dependencies = [ dependencies = [
"aes", "aes",
"bzip2", "bzip2",
@@ -10082,7 +10046,6 @@ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"hmac", "hmac",
"indexmap 2.14.0", "indexmap 2.14.0",
"libc",
"lzma-rust2", "lzma-rust2",
"memchr", "memchr",
"nix", "nix",
+7 -5
View File
@@ -61,20 +61,21 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" } tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tracing = "0.1.44" tracing = "0.1.44"
tauri = { version = "2.12.5" } tauri = { version = "2.12.4" }
tauri-plugin-clipboard-manager = "2.5.0" tauri-plugin-clipboard-manager = "2.5.0"
parking_lot = "0.13.0" parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.104" anyhow = "1.0.104"
chrono = "0.4.45" chrono = "0.4.45"
criterion = { version = "0.8.2", features = ["async_tokio"] } criterion = { version = "0.8.2", features = ["async_tokio"] }
tokio = { version = "1.54.1", features = [ tokio = { version = "1.53.3", features = [
"rt-multi-thread", "rt-multi-thread",
"macros", "macros",
"time", "time",
"sync", "sync",
] } ] }
flexi_logger = "0.31.10" flexi_logger = "0.31.10"
log = "0.4.35" log = "0.4.34"
regex = "1.13.1"
reqwest = { version = "0.13.5", features = [ reqwest = { version = "0.13.5", features = [
"json", "json",
"cookies", "cookies",
@@ -84,11 +85,12 @@ reqwest = { version = "0.13.5", features = [
] } ] }
rust_iso3166 = "0.2.0" rust_iso3166 = "0.2.0"
smartstring = { version = "2.0.0" } smartstring = { version = "1.0.1" }
serde = { version = "1.0.229" } serde = { version = "1.0.229" }
serde_json = { version = "1.0.154" } serde_json = { version = "1.0.154" }
serde_yaml_ng = { version = "0.10.0" } serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.13.2" }
socket2 = "0.6.5" socket2 = "0.6.5"
# *** For Windows platform only *** # *** For Windows platform only ***
-12
View File
@@ -25,13 +25,6 @@
<summary><strong> 🐞 修复问题 </strong></summary> <summary><strong> 🐞 修复问题 </strong></summary>
- 修复检测更新后又发布新版时,应用内更新失败 - 修复检测更新后又发布新版时,应用内更新失败
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
**🍎 macOS**
- 修复 macOS TUN 模式下原有 DNS 被清除、关闭后 DNS 未恢复及联网后设置未生效
- 修复 macOS 断网时误报代理已回退直连,恢复联网后自动重试系统代理
</details> </details>
@@ -44,10 +37,5 @@
<details> <details>
<summary><strong> 🚀 优化改进 </strong></summary> <summary><strong> 🚀 优化改进 </strong></summary>
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
**🍎 macOS**
- 优化 macOS TUN 模式的系统 DNS,增加阿里和 Cloudflare 地址
</details> </details>
+4
View File
@@ -102,6 +102,10 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
To run the development server, execute the following commands after all prerequisites for **Tauri** are installed: To run the development server, execute the following commands after all prerequisites for **Tauri** are installed:
`pnpm dev` also requires a matching `clash-verge-service-ipc` source checkout at
`../clash-verge-service-ipc`; `pnpm prebuild` downloads binaries but does not
provide this checkout. See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup details.
```shell ```shell
pnpm i pnpm i
pnpm run prebuild pnpm run prebuild
+1 -1
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] } specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
clash-verge-logging = { workspace = true } clash-verge-logging = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
log = { workspace = true } log = { workspace = true }
@@ -5,14 +5,14 @@ edition = "2024"
rust-version = "1.91" rust-version = "1.91"
[dependencies] [dependencies]
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] } specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
tauri = { workspace = true } tauri = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true } tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
# sysinfo 0.38.2 conflicts with dark-light # sysinfo 0.38.2 conflicts with dark-light
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623 # see https://github.com/GuillaumeGomez/sysinfo/issues/1623
sysinfo = { version = "0.40.1", default-features = false, features = ["network", "system"] } sysinfo = { version = "0.39.6", features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
libc = "0.2.191" libc = "0.2.191"
@@ -27,9 +27,9 @@ pub struct SysInfo {
impl Default for SysInfo { impl Default for SysInfo {
#[inline] #[inline]
fn default() -> Self { fn default() -> Self {
let system_name = System::name().unwrap_or_else(|_| "Null".into()); let system_name = System::name().unwrap_or_else(|| "Null".into());
let system_version = System::long_os_version().unwrap_or_else(|_| "Null".into()); let system_version = System::long_os_version().unwrap_or_else(|| "Null".into());
let system_kernel_version = System::kernel_version().unwrap_or_else(|_| "Null".into()); let system_kernel_version = System::kernel_version().unwrap_or_else(|| "Null".into());
let system_arch = System::cpu_arch(); let system_arch = System::cpu_arch();
Self { Self {
system_name, system_name,
@@ -130,14 +130,9 @@ pub fn current_gid() -> u32 {
#[inline] #[inline]
pub fn list_network_interfaces() -> Vec<String> { pub fn list_network_interfaces() -> Vec<String> {
let Ok(mut networks) = Networks::new() else { let mut networks = Networks::new();
return Vec::new();
};
networks.refresh(false); networks.refresh(false);
networks networks.keys().map(|name| name.to_owned()).collect()
.keys()
.map(|name| name.to_string_lossy().into_owned())
.collect()
} }
#[inline] #[inline]
+6 -5
View File
@@ -56,7 +56,7 @@
"@tanstack/react-virtual": "^3.14.13", "@tanstack/react-virtual": "^3.14.13",
"@tauri-apps/api": "2.12.4", "@tauri-apps/api": "2.12.4",
"@tauri-apps/plugin-clipboard-manager": "2.5.0", "@tauri-apps/plugin-clipboard-manager": "2.5.0",
"@tauri-apps/plugin-dialog": "2.8.4", "@tauri-apps/plugin-dialog": "2.8.3",
"@tauri-apps/plugin-fs": "2.7.1", "@tauri-apps/plugin-fs": "2.7.1",
"@tauri-apps/plugin-http": "2.8.3", "@tauri-apps/plugin-http": "2.8.3",
"@tauri-apps/plugin-opener": "2.7.2", "@tauri-apps/plugin-opener": "2.7.2",
@@ -79,7 +79,7 @@
"react-dom": "19.3.1", "react-dom": "19.3.1",
"react-error-boundary": "6.1.6", "react-error-boundary": "6.1.6",
"react-hook-form": "^7.88.0", "react-hook-form": "^7.88.0",
"react-i18next": "17.1.10", "react-i18next": "17.1.9",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-router": "^8.4.0", "react-router": "^8.4.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
@@ -94,7 +94,7 @@
"@biomejs/biome": "^2.5.14", "@biomejs/biome": "^2.5.14",
"@eslint-react/eslint-plugin": "^5.20.5", "@eslint-react/eslint-plugin": "^5.20.5",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.12.4", "@tauri-apps/cli": "2.12.3",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^26.0.0", "@types/node": "^26.0.0",
"@types/react": "19.3.0", "@types/react": "19.3.0",
@@ -102,7 +102,7 @@
"@types/validator": "^13.15.10", "@types/validator": "^13.15.10",
"@vitejs/plugin-legacy": "^8.2.3", "@vitejs/plugin-legacy": "^8.2.3",
"@vitejs/plugin-react": "^6.1.1", "@vitejs/plugin-react": "^6.1.1",
"@wdio/tauri-service": "1.5.1", "@wdio/tauri-service": "1.5.0",
"adm-zip": "^0.6.1", "adm-zip": "^0.6.1",
"axios": "^1.20.0", "axios": "^1.20.0",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
@@ -117,6 +117,7 @@
"eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-react-refresh": "^0.5.7",
"eslint-plugin-sukka": "^10.2.0", "eslint-plugin-sukka": "^10.2.0",
"eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-unused-imports": "^4.4.1",
"glob": "^14.0.0",
"globals": "^17.12.0", "globals": "^17.12.0",
"https-proxy-agent": "^9.1.0", "https-proxy-agent": "^9.1.0",
"husky": "^9.1.7", "husky": "^9.1.7",
@@ -145,5 +146,5 @@
] ]
}, },
"type": "module", "type": "module",
"packageManager": "pnpm@12.18.0" "packageManager": "pnpm@12.16.1"
} }
+203 -167
View File
@@ -7,153 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.18.0 specifier: 12.16.1
version: 12.18.0 version: 12.16.1
packages: packages:
'@pnpm/exe.android-arm64@12.18.0': '@pnpm/exe.android-arm64@12.16.1':
resolution: {integrity: sha512-fOF6IwgvnUKEmVedGqiAsNPfN2Ry8GvaqzSQOiF3RJFbpBfPKO/6YT54s1GjToyYinRxw9preHHPe4y2N3SzhA==} resolution: {integrity: sha512-PKf78Ew+i0CBYf0Xc+eMcqQiqyBT+u8XCVg1F5roc98/6wWfdYlWhrUMmEBiKWaJ3JqHxrtt8l1NwZ7qfJt2/Q==}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@pnpm/exe.android-x64@12.18.0': '@pnpm/exe.android-x64@12.16.1':
resolution: {integrity: sha512-6OiolnaCsB3/GcJ9XNrenbUH3zFHRJ+5QynrMUUEyThbgnnCzWxa199hpQlOmxN4rAXuYQ0IYgWFNUZxyzg31Q==} resolution: {integrity: sha512-xdu7gMbB98pdvhwMeA6612Of515FDlRUtfA/1f7nWPZfPLnEdSRBO2u+1N0XBtQ/ennhPH/lrxB1gC6gnR/cdg==}
cpu: [x64] cpu: [x64]
os: [android] os: [android]
'@pnpm/exe.darwin-arm64@12.18.0': '@pnpm/exe.darwin-arm64@12.16.1':
resolution: {integrity: sha512-zkq+4YzVIX39ETYLyC3W7LY7MpHEU/J3nRhHl4i2yPld2Cf2+rEKRh3ejJ4AmaQ77HHJLCktY4Z3jq0fpcxFXg==} resolution: {integrity: sha512-pVbmIIbhixiLvADLu1WA3TaW8FZy/D72R1De27WbbFx8/2mxvnsTyFVm4fO9kjpjMY7RzJd48CiQ96p8fO3ebQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.18.0': '@pnpm/exe.darwin-x64@12.16.1':
resolution: {integrity: sha512-qMsL9qx92unOTa6hgkfiTJv8ceZaTx0IQg7InhYDVX2ZrTECujaVEEVypnJK5m3eGUsP1KD7Y4qIgleCwfRsYw==} resolution: {integrity: sha512-xZvQBFqcZjO7vFKTK3XsJpqY6lnBL9EWSLCzVUPq3kZ94+D96gwJCa76xxGNK8Cj32f4eeM91Qg+yNRR4sRpDw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.freebsd-x64@12.18.0': '@pnpm/exe.freebsd-x64@12.16.1':
resolution: {integrity: sha512-/9vy/4ms52cU4MjZ5NhHd6N2d9L9W0kARf5/ff2tweAHu0VeUzs/0qp/7ZHFuddm2Wa/m0AKQoPum9AVUCqKsw==} resolution: {integrity: sha512-zTtunfi9mBhRXiXEJYfwiJj0tk7MgTSfqDSRh+Az9FZ9THazVIiQiVNS8jpcsftDOqu7LKNffQdjmgwHDThfUg==}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.18.0': '@pnpm/exe.linux-arm64-musl@12.16.1':
resolution: {integrity: sha512-Vp+TIMPPyackVCXkOdEKH/RmAn/r07EcIyrgZYQK7VrkksqZOie0GnWIx+85mPqeFEYCRtXIZAFJ9L4ceTndDg==} resolution: {integrity: sha512-BJ3mOmcmQgcUo92Ee/WfEOt1pRiv3y2MMAkhEiYHd85UiPpHTfFhv+SpCbALmfExzXk3UlaRtMYwHlkQymc4VA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.18.0': '@pnpm/exe.linux-arm64@12.16.1':
resolution: {integrity: sha512-2dpyVBCHT+FfpuQh9i+7MXAqNWYLCvD5vYr5p6Eimxoga8USLnjbUgyj72AOrO0hjc5ri3TEFP3jiyBHjkSZpQ==} resolution: {integrity: sha512-JaRd5gNxwdNGBBkrZFt8bknrK4YmHVJBW0JMhUQ0fJ0dEYp6C2GT9ExaCqOO8EwYEyqM+jpKgJIK+8WFaJy3ew==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-ppc64@12.18.0': '@pnpm/exe.linux-ppc64@12.16.1':
resolution: {integrity: sha512-BBOOOfNJ2QwIIya3eKqOjiN4WNTszfRz7HCvw9XggcT7+ftMjNP8XJ/GfW+ltCBeUFR9d8WBVsr2m/NLjX2CZQ==} resolution: {integrity: sha512-FnSV0Jv40S1cMv7vFNWwvu0X567Lec4L4HCRead3CFcD6zQDNbGw81s8N5bcKPahtPD9ApVuhsbmFCx4rw6RMw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-riscv64@12.18.0': '@pnpm/exe.linux-riscv64@12.16.1':
resolution: {integrity: sha512-7kMN77xSOYJX8GPSH5RbNAc5oM61LMmcSUukDDUwllnZPXm8GRPskCNsTWy8SCw01Kr1FB+5SwGNMiTdiziLNQ==} resolution: {integrity: sha512-C1Sww4FN5IttD4atRRhAKmQtSGWVp04ZAKNxO8pfu2bkRPwOW+7v+2DH1Xs1W+kZYYpO86BYDvUcadefF+AgaQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-s390x@12.18.0': '@pnpm/exe.linux-s390x@12.16.1':
resolution: {integrity: sha512-htuSCpEcmDzhKoJqXbPkvfr176dBHU0QIF5mWcRiqxRXAB/1JoYiMYotfjWpk+ztgpB9qlx4XnWlLFAf7gzc6g==} resolution: {integrity: sha512-kn2fautsTcxIMODoovQVqZyfpDRdNgGkoeVqNdrKqbeBBvrEl+fXLkmZiq5xDht1LMr4sYiCNcsMfNMT6JPFmg==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.18.0': '@pnpm/exe.linux-x64-musl@12.16.1':
resolution: {integrity: sha512-Uj+vzfIQ62kDP10x36yHZIXVs5ohdNtiPYwn5FhCLlNScHPo0OAI+ShGDtLofiBXKE7umu71Jmjm7N8a8zVcDw==} resolution: {integrity: sha512-PM6ccHCOe/O9SsTflTJTYWfwogJZa6EmUvpnwZgiPguJBddh20wDQKgxUPXf48rDsPm9vMsIij0uHswwwbXL8g==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.18.0': '@pnpm/exe.linux-x64@12.16.1':
resolution: {integrity: sha512-OJ+X2wcraldCwj75pCIerWARQSRpdfj0eBTs/2ODyhfeW46ylOnjTIE/GKra6Mn6E72/gBmXS8igrUioa+F9fQ==} resolution: {integrity: sha512-RK//8ZLcXGW8A2k8bQOXZDBN1Qu92zN4dYnG1bzj1zoQ1fkZ6jNzZ1AvXtsfGVma795CJtvoaF5bLXf4l/FIUw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.18.0': '@pnpm/exe.win32-arm64@12.16.1':
resolution: {integrity: sha512-wEY6jiACGcbhhi5yD59doHqNTndS7AaujaPmtWAu1DKkHru2hy5fJ9yUGecW72Ypaw2/I3BEprfMOopap2NQtw==} resolution: {integrity: sha512-Ox3kNNVxtGw6pK7SVejmeOUgHhUTZgAM7NQQ6cU7frPw3ix9+j7d2MGbRg3fyEFAaKWq8ZAZLTQGcDyLnAu+qA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.18.0': '@pnpm/exe.win32-x64@12.16.1':
resolution: {integrity: sha512-5FJkghO7RLRkQpWxeVEygW9XsmnNuxzPfXbgwFWCHsFyFhpDn/vOdL88ToQEM0Kt5igdW5s6U0/IDH8Dvkt3pg==} resolution: {integrity: sha512-k+FhRJ0ttphFoFIVB1Sk4v76YJAulafmA/Ly57cysesUhhIzVlApeOShUz0jXubKx317TThlEfpQrukldnqORA==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.18.0: pnpm@12.16.1:
resolution: {integrity: sha512-nXiC1dyCFH7PxOBiqjSDTgrQKWcQoYN6ERWuASEn02txCeMhrBWYod0KZVl2gbHpmRfhLtDXSerpEYeUM2O+sw==} resolution: {integrity: sha512-CR0d7nknV1Zhsl2rPCrjHK/BxU1rce1qZgDPDByxBDSkR+rhQBXxsj4ua+3u1535SpMuIWTSgyKA3OP7MvZW0Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.android-arm64@12.18.0': '@pnpm/exe.android-arm64@12.16.1':
optional: true optional: true
'@pnpm/exe.android-x64@12.18.0': '@pnpm/exe.android-x64@12.16.1':
optional: true optional: true
'@pnpm/exe.darwin-arm64@12.18.0': '@pnpm/exe.darwin-arm64@12.16.1':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.18.0': '@pnpm/exe.darwin-x64@12.16.1':
optional: true optional: true
'@pnpm/exe.freebsd-x64@12.18.0': '@pnpm/exe.freebsd-x64@12.16.1':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.18.0': '@pnpm/exe.linux-arm64-musl@12.16.1':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.18.0': '@pnpm/exe.linux-arm64@12.16.1':
optional: true optional: true
'@pnpm/exe.linux-ppc64@12.18.0': '@pnpm/exe.linux-ppc64@12.16.1':
optional: true optional: true
'@pnpm/exe.linux-riscv64@12.18.0': '@pnpm/exe.linux-riscv64@12.16.1':
optional: true optional: true
'@pnpm/exe.linux-s390x@12.18.0': '@pnpm/exe.linux-s390x@12.16.1':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.18.0': '@pnpm/exe.linux-x64-musl@12.16.1':
optional: true optional: true
'@pnpm/exe.linux-x64@12.18.0': '@pnpm/exe.linux-x64@12.16.1':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.18.0': '@pnpm/exe.win32-arm64@12.16.1':
optional: true optional: true
'@pnpm/exe.win32-x64@12.18.0': '@pnpm/exe.win32-x64@12.16.1':
optional: true optional: true
pnpm@12.18.0: pnpm@12.16.1:
optionalDependencies: optionalDependencies:
'@pnpm/exe.android-arm64': 12.18.0 '@pnpm/exe.android-arm64': 12.16.1
'@pnpm/exe.android-x64': 12.18.0 '@pnpm/exe.android-x64': 12.16.1
'@pnpm/exe.darwin-arm64': 12.18.0 '@pnpm/exe.darwin-arm64': 12.16.1
'@pnpm/exe.darwin-x64': 12.18.0 '@pnpm/exe.darwin-x64': 12.16.1
'@pnpm/exe.freebsd-x64': 12.18.0 '@pnpm/exe.freebsd-x64': 12.16.1
'@pnpm/exe.linux-arm64': 12.18.0 '@pnpm/exe.linux-arm64': 12.16.1
'@pnpm/exe.linux-arm64-musl': 12.18.0 '@pnpm/exe.linux-arm64-musl': 12.16.1
'@pnpm/exe.linux-ppc64': 12.18.0 '@pnpm/exe.linux-ppc64': 12.16.1
'@pnpm/exe.linux-riscv64': 12.18.0 '@pnpm/exe.linux-riscv64': 12.16.1
'@pnpm/exe.linux-s390x': 12.18.0 '@pnpm/exe.linux-s390x': 12.16.1
'@pnpm/exe.linux-x64': 12.18.0 '@pnpm/exe.linux-x64': 12.16.1
'@pnpm/exe.linux-x64-musl': 12.18.0 '@pnpm/exe.linux-x64-musl': 12.16.1
'@pnpm/exe.win32-arm64': 12.18.0 '@pnpm/exe.win32-arm64': 12.16.1
'@pnpm/exe.win32-x64': 12.18.0 '@pnpm/exe.win32-x64': 12.16.1
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'
@@ -206,8 +206,8 @@ importers:
specifier: 2.5.0 specifier: 2.5.0
version: 2.5.0 version: 2.5.0
'@tauri-apps/plugin-dialog': '@tauri-apps/plugin-dialog':
specifier: 2.8.4 specifier: 2.8.3
version: 2.8.4 version: 2.8.3
'@tauri-apps/plugin-fs': '@tauri-apps/plugin-fs':
specifier: 2.7.1 specifier: 2.7.1
version: 2.7.1 version: 2.7.1
@@ -275,8 +275,8 @@ importers:
specifier: ^7.88.0 specifier: ^7.88.0
version: 7.88.0(react@19.3.1) version: 7.88.0(react@19.3.1)
react-i18next: react-i18next:
specifier: 17.1.10 specifier: 17.1.9
version: 17.1.10(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3) version: 17.1.9(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3)
react-markdown: react-markdown:
specifier: 10.1.0 specifier: 10.1.0
version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)
@@ -315,8 +315,8 @@ importers:
specifier: ^10.0.1 specifier: ^10.0.1
version: 10.0.1(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0)) version: 10.0.1(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))
'@tauri-apps/cli': '@tauri-apps/cli':
specifier: 2.12.4 specifier: 2.12.3
version: 2.12.4 version: 2.12.3
'@types/lodash-es': '@types/lodash-es':
specifier: ^4.17.12 specifier: ^4.17.12
version: 4.17.12 version: 4.17.12
@@ -339,8 +339,8 @@ importers:
specifier: ^6.1.1 specifier: ^6.1.1
version: 6.1.1(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) version: 6.1.1(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
'@wdio/tauri-service': '@wdio/tauri-service':
specifier: 1.5.1 specifier: 1.5.0
version: 1.5.1(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)) version: 1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
adm-zip: adm-zip:
specifier: ^0.6.1 specifier: ^0.6.1
version: 0.6.1 version: 0.6.1
@@ -383,6 +383,9 @@ importers:
eslint-plugin-unused-imports: eslint-plugin-unused-imports:
specifier: ^4.4.1 specifier: ^4.4.1
version: 4.4.1(@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0)) version: 4.4.1(@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))
glob:
specifier: ^14.0.0
version: 14.0.3
globals: globals:
specifier: ^17.12.0 specifier: ^17.12.0
version: 17.12.0 version: 17.12.0
@@ -1381,6 +1384,9 @@ packages:
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@isaacs/nonrecursive-glob-match@1.0.2':
resolution: {integrity: sha512-GJPHIgPPtFl+nLhFHyF0zZoFcOFLsZWbHFFzo+Zr+PWDXJbl9hHxb2w+9SiF2fHmLgKaXIpUzQo3SbBBZ9WObw==}
'@jest/diff-sequences@30.5.0': '@jest/diff-sequences@30.5.0':
resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==} resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -2121,87 +2127,87 @@ packages:
'@tauri-apps/api@2.12.4': '@tauri-apps/api@2.12.4':
resolution: {integrity: sha512-pVmfis+4zT8H0Qhp3xPZx9Ju7kpzvA6rr/PSJPyuwfvucTQ8sVt61NYU9KMkK703UFlYadFmjKA4HhybOtN0gg==} resolution: {integrity: sha512-pVmfis+4zT8H0Qhp3xPZx9Ju7kpzvA6rr/PSJPyuwfvucTQ8sVt61NYU9KMkK703UFlYadFmjKA4HhybOtN0gg==}
'@tauri-apps/cli-darwin-arm64@2.12.4': '@tauri-apps/cli-darwin-arm64@2.12.3':
resolution: {integrity: sha512-OxqKSckIkoJ6vX2AjifIJpTasFC0Hbdoo7gEiCmAjauABO3U8m2MRsLWe+QTiBwDx1mIrvgA+rvI7tvDnbnHVg==} resolution: {integrity: sha512-xmVNoJOOKzKLj7GLHXmXLimRby9M42aBIJa0sB80N7o24eKV5JiVmToKTRLVH9yhL05GZ/B6rSSOmbzNFJwBAA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@tauri-apps/cli-darwin-x64@2.12.4': '@tauri-apps/cli-darwin-x64@2.12.3':
resolution: {integrity: sha512-fz76/77wMMJOaaUN6n3CybRG0P/MTEjjqYk81mGPyhi9m6Ue5T+LWBnJP/AkPooNatBPI2vtulGqomyMJ44lCQ==} resolution: {integrity: sha512-URd+0usfmUQQyb5kNPiKt2OT8gp7oGU7QR+qOppB6e35mpVBFFVNhWc9oWb3p0u71fDJ09DpF4T6m9xYxoIB8g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.4': '@tauri-apps/cli-linux-arm-gnueabihf@2.12.3':
resolution: {integrity: sha512-K0w8Bof6Lt8QKLAYhRy0yy36ZH5iA/35VuSOvnxyjvCUZ+A3JKsUTlALviH/sVgAlisaPzzmeJSH8XOn4Q2HBA==} resolution: {integrity: sha512-0kwUVx2LbmOoTRlwF1oDww7th4/NSluv4ZBQ6KRZsFKg4g0TlTcIiJ0VehqkQYUeC9xIT1Mk6XtF52mI155VSQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.12.4': '@tauri-apps/cli-linux-arm64-gnu@2.12.3':
resolution: {integrity: sha512-j5rOnjmRUwWrm1oosf0zBYl4D+fqydgUROFCk0hvKZxvbTT8SOERes0QB7rBV/XWNy+kM6pCKzb3oSm2sRglzg==} resolution: {integrity: sha512-gKSGDx6fx+HBLQmG6guChFdisbrCSdx9Yli+j5teOa64gm1CeM+C6NLru5Pj3ZkkEep05BmCFjxvX+cjqAycZQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.12.4': '@tauri-apps/cli-linux-arm64-musl@2.12.3':
resolution: {integrity: sha512-aOue3Fy6Jb7+oWcfAVUsVHfeTybSCt2CZoU06aO4jv4d2Ks/KnaTPwMAK3CLEduGOvBLY1lbBBrIyomqLLH5Sg==} resolution: {integrity: sha512-Yr59qMy/gcvAfBuFdCGlAmGiLsYWMFDbsP4bAI6iNBzcJTGrYWZGeiQhGDo29MQwfF/NUBqyM2qljXu5tZ6wjQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.12.4': '@tauri-apps/cli-linux-riscv64-gnu@2.12.3':
resolution: {integrity: sha512-Fx6GShY/RvST+t6LMlTHAwd66+1tEaAAGrnU7cysVfaiRP1BKng5Vl6rVmO6rSLOA1Sp1dRzNYM4f0OWQGb7qA==} resolution: {integrity: sha512-jKr0/REBMnLKC0OwPQZkyge0mKDaNZ0b/AbfGwVyCgFfwS730EVLfVDo3nt7MUu6jJaytRq08+xSj5qfy+U1+Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.12.4': '@tauri-apps/cli-linux-x64-gnu@2.12.3':
resolution: {integrity: sha512-tNzZSa79Z1nRpHxEpQ38lsAFpKlFlYvoXzjTV+lRRn/zSf3wrdUC3wnLcq2XEadchPu4ttK0zivhYzr+XGD19Q==} resolution: {integrity: sha512-gTkEqJAGCMROGanxHnzaJzQ+/rXl39HWVg0/hDODS1wDWBGqoceqGC6Qm1+mQbyT11xnsB8mKu/gyVID/YpFIw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.12.4': '@tauri-apps/cli-linux-x64-musl@2.12.3':
resolution: {integrity: sha512-jmZ1pr0xEqUhuEusWtt9Yt+GF6lCnGHUmIL+KIknpbjK/XQ2qkhDBhaxK/JMHM2A8dAacVXCxXJk2EXpwhQUfA==} resolution: {integrity: sha512-T2dTxL1svRym71fZtaW4pZYXbthbfFsEJr1NeOUg/p7laWXZbUt/8DMSZf8recu9f7+1F/Lv00HtzsEu6vDO9w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.12.4': '@tauri-apps/cli-win32-arm64-msvc@2.12.3':
resolution: {integrity: sha512-fGo8pgEHZxdSYgZ/J98m1usJ7NHKmKbq7k6uT0x1rpOq6Bg2YVzzpaAblWk710f5LHYBJIL5QIFS5IrD+4gmTg==} resolution: {integrity: sha512-eoiQwpEZWbpsVHcM9BAzqqe4TCHszvr3rsM7pUQ4ofLK0lF1i1d/jX0Qo5DYMqcrUtwQuDdDuOCbcT0Izf1VwQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.12.4': '@tauri-apps/cli-win32-ia32-msvc@2.12.3':
resolution: {integrity: sha512-ezl8sQIcjYsye3TUnTH5VcWmWzccZASlQm7hlZt0dYyj1XD0qpVW47u2xYVrIKui0F8uQv/7QGZixdWiClBsbg==} resolution: {integrity: sha512-kE+KUg6Dbbkgvcw0m99wJVIToFQzJqpMBAXah74WqxaLzssNwSTUccrHp6+QVzMhjQP330tvekFmnQF+0NbE6Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.12.4': '@tauri-apps/cli-win32-x64-msvc@2.12.3':
resolution: {integrity: sha512-DJhDeCqjLDNMWYFy7ZS8cfeAHX2RK0RbQMLiA3bMVVkXRDJ4NBYiHriGqsUhsr9iG41aTb+dJGkFPmPw1gWwVw==} resolution: {integrity: sha512-L7wndWAgX2mLW7alswpL3d1R0P9FN2WYBgHaQkAYEBxHHGkY4vFX9CjcRQ1LAxDAMIJxrY7/aDziFJeChS84NQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@tauri-apps/cli@2.12.4': '@tauri-apps/cli@2.12.3':
resolution: {integrity: sha512-JI9bHfudSRJikGOrjbXEm6RjzzvmKuLc+JixB1NE8hNzQkNlCCIC3pTES3uypkR9lxHrsbxzcnSwhI2IxBQphw==} resolution: {integrity: sha512-f7/LsY74SVb1ChmOPw1ajOpxii0GSzDAgmWHRci8JUYuuai04rXsSbFobpmB0sP2DdlvMD/B5ZotEn8GOGMRUg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
hasBin: true hasBin: true
'@tauri-apps/plugin-clipboard-manager@2.5.0': '@tauri-apps/plugin-clipboard-manager@2.5.0':
resolution: {integrity: sha512-dibUy1nKURYDXRMvjNk4deAbTZ5jPQ8wCE4nolt5xniryWgXQ4sK2GkFXoXlHbRdPzfZptZj95BRLfalA7m/2Q==} resolution: {integrity: sha512-dibUy1nKURYDXRMvjNk4deAbTZ5jPQ8wCE4nolt5xniryWgXQ4sK2GkFXoXlHbRdPzfZptZj95BRLfalA7m/2Q==}
'@tauri-apps/plugin-dialog@2.8.4': '@tauri-apps/plugin-dialog@2.8.3':
resolution: {integrity: sha512-+6ZOiXenOaxXwb8qCbBPdT2jX2cFPaVVHGIskrpmJLpmWj+qKRTD6X/z9x7hTdSQn7NedcoXCkWGej4a7dC4lw==} resolution: {integrity: sha512-Hd/yBfk2R5v2MFrz/0VArSXeh8xKLLXGunqSiTtcHKn1zNZwaKujuA0VSwUvNfwC4hDoF1PveS+gBDNe3nHspw==}
'@tauri-apps/plugin-fs@2.7.1': '@tauri-apps/plugin-fs@2.7.1':
resolution: {integrity: sha512-4TBVjpi/q8PuNzjpkAc2iBGXMTLjOrAyfTiiOL8ORaJCZJRG3tjzmQ4sqs2CMG75FvLyRkOkERfP7AyY4iBNwQ==} resolution: {integrity: sha512-4TBVjpi/q8PuNzjpkAc2iBGXMTLjOrAyfTiiOL8ORaJCZJRG3tjzmQ4sqs2CMG75FvLyRkOkERfP7AyY4iBNwQ==}
@@ -2617,20 +2623,20 @@ packages:
resolution: {integrity: sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==} resolution: {integrity: sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/native-core@1.3.1': '@wdio/native-core@1.3.0':
resolution: {integrity: sha512-fdGk2lEr2vS+bClJXuZEnsG30cLzQv+B9W+dAt/Yte/fE33s/vDBn7hHwFx8aZLTOS0pPExErUxYiZEMXX+04g==} resolution: {integrity: sha512-wUwc0QTdvhD0LwH4YcRaN9JWLX/yac8sovddUR5/4sqHMNJr3yaN11bpIfYoD/bEk+1nTLjWuDCFpe5l5wUgLQ==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/native-spy@1.4.1': '@wdio/native-spy@1.4.0':
resolution: {integrity: sha512-KGOmgaicHqDkqj+s2bxXsRsb27IzW6carlyTu1xcrJI34yUbC0v14F8mbhuZcewW+mt2xhmC17yITwAbNa8PKw==} resolution: {integrity: sha512-unsC2Vgu5T9atD478Zj/p2N+rVSK4i/NibIaaEw4rZF/T5u5bmAHChFntEiBEfyMzd62VwbhKhzwwOSKjzrfJA==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/native-types@2.6.1': '@wdio/native-types@2.6.0':
resolution: {integrity: sha512-Rm5chX+MrRhVgJYcYdcX/yyTZaLjxvi0w/fnSgPaDT16gMIntXlk22K2x3hwEdaUonOKepZyhh4jr9ZgRz7UaQ==} resolution: {integrity: sha512-MVMVpZTH+UY6nUtLClCzY4BQSIs/nCCHo81i4ClLR+b2NehyBvwccttXDAGPgf0icyHUhd+PgIPXaLgRcm4M9A==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/native-utils@2.8.1': '@wdio/native-utils@2.8.0':
resolution: {integrity: sha512-QbmZjGkLUC2bmUJSzyqsHM11HND1D7tkxHFssUV1Kmx43+oPAeHHVR0NSZu2hMzSNrDZMBAmNYycR+3X8rqWcA==} resolution: {integrity: sha512-TmIkPAd19FkJ747AdFVEtawbakVV3HHCh8zk3Kl/FLc6B2bbea7hn90jtLxqBZgCVqUuJIBcMNFg/nV/hgBFOw==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
peerDependencies: peerDependencies:
tsx: ^4.22.4 tsx: ^4.22.4
@@ -2654,8 +2660,8 @@ packages:
resolution: {integrity: sha512-lNLLPUk4y1Yp5pWwBFF+tjZIy7bOv0TPTk4HDWV4LnnHnkc24abI0nN+kb2y5/JV8ZgwLCCVKUFRdRUxfKsAaQ==} resolution: {integrity: sha512-lNLLPUk4y1Yp5pWwBFF+tjZIy7bOv0TPTk4HDWV4LnnHnkc24abI0nN+kb2y5/JV8ZgwLCCVKUFRdRUxfKsAaQ==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/tauri-service@1.5.1': '@wdio/tauri-service@1.5.0':
resolution: {integrity: sha512-nfzJU2x7bxvIXtYo+yvrX4Xwuba8h5hXOlJ0IyHanGg0ELavY/2UfyxygJDTcSGO+pCQMdC9JJEyMvaS50AtZQ==} resolution: {integrity: sha512-S3fs8T+jgNOFi4x/X2z69unS/1K3ZB28+owBouhHtVnrMKeBWAKP46IHnp8c8AZXppn6fiPNC4M1jfRZbLKeKw==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
peerDependencies: peerDependencies:
webdriverio: ^9.0.0 webdriverio: ^9.0.0
@@ -3502,8 +3508,8 @@ packages:
resolution: {integrity: sha512-dUnV6Uok578HuGYTC/EM0LqeXewXBCHsh/kmLkdcqN1zgSy7K3gCIrshPBS5JwW20XSaRv4Vnz7z8lMjOV8jWw==} resolution: {integrity: sha512-dUnV6Uok578HuGYTC/EM0LqeXewXBCHsh/kmLkdcqN1zgSy7K3gCIrshPBS5JwW20XSaRv4Vnz7z8lMjOV8jWw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
follow-redirects@1.16.2: follow-redirects@1.16.0:
resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==} resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
peerDependencies: peerDependencies:
debug: '*' debug: '*'
@@ -3584,6 +3590,10 @@ packages:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
glob@14.0.3:
resolution: {integrity: sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==}
engines: {node: 18 || 20 || >=22}
globals@17.12.0: globals@17.12.0:
resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==} resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -4121,6 +4131,10 @@ packages:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
minimatch@11.0.2:
resolution: {integrity: sha512-pWTgz5CN3tjqyZNSK2tePlSmu9IwM41q8yn8Usdqqwe9TIHh+9dEpLLjF4uApsfPEAZY71qtSiA2B2APEr+1Yw==}
engines: {node: 18 || 20 || >=22}
minipass@7.1.3: minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
@@ -4282,6 +4296,10 @@ packages:
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
path-scurry@3.0.0:
resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==}
engines: {node: 18 || 20 || >=22}
path-type@4.0.0: path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -4388,8 +4406,8 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19 react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@17.1.10: react-i18next@17.1.9:
resolution: {integrity: sha512-XuUuA/rxukE4hXuR//PN308DuSQkWqKNNWBUj0dDTR087l3+6WLid3NC/w55ALrXA/dtWb7lychJ4rQLR3DCDg==} resolution: {integrity: sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==}
peerDependencies: peerDependencies:
i18next: '>= 26.2.0' i18next: '>= 26.2.0'
react: '>= 16.8.0' react: '>= 16.8.0'
@@ -4584,8 +4602,8 @@ packages:
snake-case@3.0.4: snake-case@3.0.4:
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
source-map-js@1.2.2: source-map-js@1.2.1:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
source-map-support@0.5.21: source-map-support@0.5.21:
@@ -4807,16 +4825,16 @@ packages:
undici-types@8.9.0: undici-types@8.9.0:
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
undici@6.29.1: undici@6.28.1:
resolution: {integrity: sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==} resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==}
engines: {node: '>=18.17'} engines: {node: '>=18.17'}
undici@7.30.1: undici@7.29.1:
resolution: {integrity: sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==} resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==}
engines: {node: '>=20.18.1'} engines: {node: '>=20.18.1'}
undici@8.12.3: undici@8.11.2:
resolution: {integrity: sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==} resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==}
engines: {node: '>=22.19.0'} engines: {node: '>=22.19.0'}
unicode-canonical-property-names-ecmascript@2.0.1: unicode-canonical-property-names-ecmascript@2.0.1:
@@ -5138,12 +5156,12 @@ snapshots:
'@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.8) '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.8)
'@octokit/request': 10.0.16 '@octokit/request': 10.0.16
'@octokit/request-error': 7.1.2 '@octokit/request-error': 7.1.2
undici: 6.29.1 undici: 6.28.1
'@actions/http-client@3.0.2': '@actions/http-client@3.0.2':
dependencies: dependencies:
tunnel: 0.0.6 tunnel: 0.0.6
undici: 6.29.1 undici: 6.28.1
'@babel/code-frame@7.29.7': '@babel/code-frame@7.29.7':
dependencies: dependencies:
@@ -6242,6 +6260,8 @@ snapshots:
dependencies: dependencies:
minipass: 7.1.3 minipass: 7.1.3
'@isaacs/nonrecursive-glob-match@1.0.2': {}
'@jest/diff-sequences@30.5.0': {} '@jest/diff-sequences@30.5.0': {}
'@jest/expect-utils@30.5.2': '@jest/expect-utils@30.5.2':
@@ -6804,58 +6824,58 @@ snapshots:
'@tauri-apps/api@2.12.4': {} '@tauri-apps/api@2.12.4': {}
'@tauri-apps/cli-darwin-arm64@2.12.4': '@tauri-apps/cli-darwin-arm64@2.12.3':
optional: true optional: true
'@tauri-apps/cli-darwin-x64@2.12.4': '@tauri-apps/cli-darwin-x64@2.12.3':
optional: true optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.4': '@tauri-apps/cli-linux-arm-gnueabihf@2.12.3':
optional: true optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.12.4': '@tauri-apps/cli-linux-arm64-gnu@2.12.3':
optional: true optional: true
'@tauri-apps/cli-linux-arm64-musl@2.12.4': '@tauri-apps/cli-linux-arm64-musl@2.12.3':
optional: true optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.12.4': '@tauri-apps/cli-linux-riscv64-gnu@2.12.3':
optional: true optional: true
'@tauri-apps/cli-linux-x64-gnu@2.12.4': '@tauri-apps/cli-linux-x64-gnu@2.12.3':
optional: true optional: true
'@tauri-apps/cli-linux-x64-musl@2.12.4': '@tauri-apps/cli-linux-x64-musl@2.12.3':
optional: true optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.12.4': '@tauri-apps/cli-win32-arm64-msvc@2.12.3':
optional: true optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.12.4': '@tauri-apps/cli-win32-ia32-msvc@2.12.3':
optional: true optional: true
'@tauri-apps/cli-win32-x64-msvc@2.12.4': '@tauri-apps/cli-win32-x64-msvc@2.12.3':
optional: true optional: true
'@tauri-apps/cli@2.12.4': '@tauri-apps/cli@2.12.3':
optionalDependencies: optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.12.4 '@tauri-apps/cli-darwin-arm64': 2.12.3
'@tauri-apps/cli-darwin-x64': 2.12.4 '@tauri-apps/cli-darwin-x64': 2.12.3
'@tauri-apps/cli-linux-arm-gnueabihf': 2.12.4 '@tauri-apps/cli-linux-arm-gnueabihf': 2.12.3
'@tauri-apps/cli-linux-arm64-gnu': 2.12.4 '@tauri-apps/cli-linux-arm64-gnu': 2.12.3
'@tauri-apps/cli-linux-arm64-musl': 2.12.4 '@tauri-apps/cli-linux-arm64-musl': 2.12.3
'@tauri-apps/cli-linux-riscv64-gnu': 2.12.4 '@tauri-apps/cli-linux-riscv64-gnu': 2.12.3
'@tauri-apps/cli-linux-x64-gnu': 2.12.4 '@tauri-apps/cli-linux-x64-gnu': 2.12.3
'@tauri-apps/cli-linux-x64-musl': 2.12.4 '@tauri-apps/cli-linux-x64-musl': 2.12.3
'@tauri-apps/cli-win32-arm64-msvc': 2.12.4 '@tauri-apps/cli-win32-arm64-msvc': 2.12.3
'@tauri-apps/cli-win32-ia32-msvc': 2.12.4 '@tauri-apps/cli-win32-ia32-msvc': 2.12.3
'@tauri-apps/cli-win32-x64-msvc': 2.12.4 '@tauri-apps/cli-win32-x64-msvc': 2.12.3
'@tauri-apps/plugin-clipboard-manager@2.5.0': '@tauri-apps/plugin-clipboard-manager@2.5.0':
dependencies: dependencies:
'@tauri-apps/api': 2.12.4 '@tauri-apps/api': 2.12.4
'@tauri-apps/plugin-dialog@2.8.4': '@tauri-apps/plugin-dialog@2.8.3':
dependencies: dependencies:
'@tauri-apps/api': 2.12.4 '@tauri-apps/api': 2.12.4
@@ -7255,11 +7275,11 @@ snapshots:
safe-regex2: 5.1.1 safe-regex2: 5.1.1
strip-ansi: 7.2.0 strip-ansi: 7.2.0
'@wdio/native-core@1.3.1(supports-color@7.2.0)': '@wdio/native-core@1.3.0(supports-color@7.2.0)':
dependencies: dependencies:
'@wdio/logger': 9.29.1 '@wdio/logger': 9.29.1
'@wdio/native-types': 2.6.1 '@wdio/native-types': 2.6.0
'@wdio/native-utils': 2.8.1(supports-color@7.2.0) '@wdio/native-utils': 2.8.0(supports-color@7.2.0)
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
get-port: 7.2.0 get-port: 7.2.0
tslib: 2.8.1 tslib: 2.8.1
@@ -7267,11 +7287,11 @@ snapshots:
- supports-color - supports-color
- tsx - tsx
'@wdio/native-spy@1.4.1': {} '@wdio/native-spy@1.4.0': {}
'@wdio/native-types@2.6.1': {} '@wdio/native-types@2.6.0': {}
'@wdio/native-utils@2.8.1(supports-color@7.2.0)': '@wdio/native-utils@2.8.0(supports-color@7.2.0)':
dependencies: dependencies:
'@wdio/logger': 9.29.1 '@wdio/logger': 9.29.1
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
@@ -7305,14 +7325,14 @@ snapshots:
easy-table: 1.2.0 easy-table: 1.2.0
pretty-ms: 9.3.1 pretty-ms: 9.3.1
'@wdio/tauri-service@1.5.1(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))': '@wdio/tauri-service@1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))':
dependencies: dependencies:
'@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)) '@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
'@wdio/logger': 9.29.1 '@wdio/logger': 9.29.1
'@wdio/native-core': 1.3.1(supports-color@7.2.0) '@wdio/native-core': 1.3.0(supports-color@7.2.0)
'@wdio/native-spy': 1.4.1 '@wdio/native-spy': 1.4.0
'@wdio/native-types': 2.6.1 '@wdio/native-types': 2.6.0
'@wdio/native-utils': 2.8.1(supports-color@7.2.0) '@wdio/native-utils': 2.8.0(supports-color@7.2.0)
'@wdio/spec-reporter': 9.31.2 '@wdio/spec-reporter': 9.31.2
'@wdio/types': 9.31.2 '@wdio/types': 9.31.2
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
@@ -7438,7 +7458,7 @@ snapshots:
axios@1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): axios@1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0):
dependencies: dependencies:
follow-redirects: 1.16.2(debug@4.4.3(supports-color@7.2.0)) follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
form-data: 4.0.6 form-data: 4.0.6
https-proxy-agent: 5.0.1(supports-color@7.2.0) https-proxy-agent: 5.0.1(supports-color@7.2.0)
proxy-from-env: 2.1.0 proxy-from-env: 2.1.0
@@ -7608,7 +7628,7 @@ snapshots:
parse5: 7.3.0 parse5: 7.3.0
parse5-htmlparser2-tree-adapter: 7.1.0 parse5-htmlparser2-tree-adapter: 7.1.0
parse5-parser-stream: 7.1.2 parse5-parser-stream: 7.1.2
undici: 7.30.1 undici: 7.29.1
whatwg-mimetype: 4.0.0 whatwg-mimetype: 4.0.0
chokidar@5.0.0: chokidar@5.0.0:
@@ -7830,7 +7850,7 @@ snapshots:
'@wdio/logger': 10.1.0 '@wdio/logger': 10.1.0
'@zip.js/zip.js': 2.24.0 '@zip.js/zip.js': 2.24.0
decamelize: 6.0.1 decamelize: 6.0.1
undici: 8.12.3 undici: 8.11.2
electron-to-chromium@1.5.444: {} electron-to-chromium@1.5.444: {}
@@ -8289,7 +8309,7 @@ snapshots:
fnv1a52@1.0.0: {} fnv1a52@1.0.0: {}
follow-redirects@1.16.2(debug@4.4.3(supports-color@7.2.0)): follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)):
optionalDependencies: optionalDependencies:
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
@@ -8331,7 +8351,7 @@ snapshots:
'@zip.js/zip.js': 2.24.0 '@zip.js/zip.js': 2.24.0
decamelize: 6.0.1 decamelize: 6.0.1
modern-tar: 0.8.5 modern-tar: 0.8.5
undici: 8.12.3 undici: 8.11.2
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
@@ -8373,6 +8393,12 @@ snapshots:
minipass: 7.1.3 minipass: 7.1.3
path-scurry: 2.0.2 path-scurry: 2.0.2
glob@14.0.3:
dependencies:
minimatch: 11.0.2
minipass: 7.1.3
path-scurry: 3.0.0
globals@17.12.0: {} globals@17.12.0: {}
gopd@1.2.0: {} gopd@1.2.0: {}
@@ -9063,6 +9089,11 @@ snapshots:
dependencies: dependencies:
brace-expansion: 5.0.12 brace-expansion: 5.0.12
minimatch@11.0.2:
dependencies:
'@isaacs/nonrecursive-glob-match': 1.0.2
brace-expansion: 5.0.12
minipass@7.1.3: {} minipass@7.1.3: {}
minizlib@3.1.0: minizlib@3.1.0:
@@ -9263,6 +9294,11 @@ snapshots:
lru-cache: 11.5.3 lru-cache: 11.5.3
minipass: 7.1.3 minipass: 7.1.3
path-scurry@3.0.0:
dependencies:
lru-cache: 11.5.3
minipass: 7.1.3
path-type@4.0.0: {} path-type@4.0.0: {}
pathe@2.0.3: {} pathe@2.0.3: {}
@@ -9278,7 +9314,7 @@ snapshots:
dependencies: dependencies:
nanoid: 3.3.19 nanoid: 3.3.19
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.2 source-map-js: 1.2.1
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
@@ -9342,7 +9378,7 @@ snapshots:
dependencies: dependencies:
react: 19.3.1 react: 19.3.1
react-i18next@17.1.10(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3): react-i18next@17.1.9(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3):
dependencies: dependencies:
'@babel/runtime': 7.29.7 '@babel/runtime': 7.29.7
html-parse-stringify: 4.0.1 html-parse-stringify: 4.0.1
@@ -9525,7 +9561,7 @@ snapshots:
dependencies: dependencies:
chokidar: 5.0.0 chokidar: 5.0.0
immutable: 5.1.9 immutable: 5.1.9
source-map-js: 1.2.2 source-map-js: 1.2.1
optionalDependencies: optionalDependencies:
'@parcel/watcher': 2.6.0 '@parcel/watcher': 2.6.0
@@ -9563,7 +9599,7 @@ snapshots:
dot-case: 3.0.4 dot-case: 3.0.4
tslib: 2.8.1 tslib: 2.8.1
source-map-js@1.2.2: {} source-map-js@1.2.1: {}
source-map-support@0.5.21: source-map-support@0.5.21:
dependencies: dependencies:
@@ -9781,11 +9817,11 @@ snapshots:
undici-types@8.9.0: {} undici-types@8.9.0: {}
undici@6.29.1: {} undici@6.28.1: {}
undici@7.30.1: {} undici@7.29.1: {}
undici@8.12.3: {} undici@8.11.2: {}
unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-canonical-property-names-ecmascript@2.0.1: {}
@@ -9986,7 +10022,7 @@ snapshots:
'@wdio/utils': 10.1.0(supports-color@7.2.0)(yauzl@2.10.0) '@wdio/utils': 10.1.0(supports-color@7.2.0)(yauzl@2.10.0)
deepmerge-ts: 8.0.2 deepmerge-ts: 8.0.2
https-proxy-agent: 9.1.0(supports-color@7.2.0) https-proxy-agent: 9.1.0(supports-color@7.2.0)
undici: 8.12.3 undici: 8.11.2
ws: 8.22.1 ws: 8.22.1
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
-21
View File
@@ -113,27 +113,6 @@
"ghcr.io/devcontainers/features/rust" "ghcr.io/devcontainers/features/rust"
], ],
"groupName": "Rust toolchain" "groupName": "Rust toolchain"
},
{
"description": "Update the Specta contract toolchain together",
"matchManagers": ["cargo"],
"matchPackageNames": ["specta", "tauri-specta", "specta-typescript"],
"groupName": "specta",
"groupSlug": "specta",
"prCreation": "immediate",
"automerge": false
},
{
"description": "Keep Specta aligned with the supported tauri-specta release",
"matchManagers": ["cargo"],
"matchPackageNames": ["specta", "tauri-specta"],
"allowedVersions": "/^2\\.0\\.0-rc\\.26$/"
},
{
"description": "Keep the TypeScript exporter compatible with Specta rc.26",
"matchManagers": ["cargo"],
"matchPackageNames": ["specta-typescript"],
"allowedVersions": "/^0\\.0\\.13$/"
} }
], ],
"customManagers": [ "customManagers": [
+2 -1
View File
@@ -6,6 +6,7 @@ import path from 'node:path'
import zlib from 'node:zlib' import zlib from 'node:zlib'
import AdmZip from 'adm-zip' import AdmZip from 'adm-zip'
import { glob } from 'glob'
import { HttpsProxyAgent } from 'https-proxy-agent' import { HttpsProxyAgent } from 'https-proxy-agent'
import { extract } from 'tar' import { extract } from 'tar'
@@ -518,7 +519,7 @@ const resolveServicePermission = async () => {
let hasChanges = false let hasChanges = false
for (const f of serviceExecutables) { for (const f of serviceExecutables) {
const files = fs.globSync(path.join(SERVICE_DIR, f)) const files = glob.sync(path.join(SERVICE_DIR, f))
for (const filePath of files) { for (const filePath of files) {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath) const currentHash = await calculateFileHash(filePath)
+17 -50
View File
@@ -32,9 +32,7 @@ function is_valid_ip() {
# Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent. # Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent.
function sc_value() { function sc_value() {
local dictionary printf 'show %s\nquit\n' "$1" | scutil | sed -n "s/^ *$2 : //p"
dictionary=$(printf 'show %s\nquit\n' "$1" | scutil) || return 1
sed -n "s/^ *$2 : //p" <<<"$dictionary"
} }
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer. # Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
@@ -52,62 +50,31 @@ function dns_of() {
echo "$dns" echo "$dns"
} }
[ $# -lt 1 ] && echo "Usage: $0 <newline-separated DNS servers> [legacy state directory]" >&2 && exit 1 [ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1
dns_servers=() ! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1
while read -r server; do
! is_valid_ip "$server" && echo "$server is not a valid IP address." >&2 && exit 1
dns_servers+=("$server")
done <<<"$1"
# Backups are keyed by service ID, which survives renaming the service and renumbering its interface. # Backups are keyed by service ID, which survives renaming the service and renumbering its interface.
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService) || exit 1 service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName) || exit 1 hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName)
[ -z "$hardware_port" ] && echo "No primary network service; waiting for the network." >&2 && exit 75 [ -z "$hardware_port" ] && echo "No primary network service." && exit 1
original_dns=$(dns_of "$hardware_port") || { echo "Cannot read DNS servers of $hardware_port." >&2; exit 1; } original_dns=$(dns_of "$hardware_port") || { echo "Cannot read DNS servers of $hardware_port." >&2; exit 1; }
backup=".original_dns/$service_id" backup=".original_dns/$service_id"
mkdir -p .original_dns || exit 1 mkdir -p .original_dns || exit 1
# Key old single-slot backups before import so a later network switch cannot reassign them. # Older releases kept a single backup for whichever service was primary.
for state_dir in "$PWD" "${2:-$PWD}"; do if [ -f .original_dns.txt ]; then
if [ -f "$state_dir/.original_dns.txt" ]; then if [ -f "$backup" ]; then
mkdir -p "$state_dir/.original_dns" || exit 1 rm -f .original_dns.txt || exit 1
if [ -f "$state_dir/.original_dns/$service_id" ]; then else
rm -f "$state_dir/.original_dns.txt" || exit 1 mv .original_dns.txt "$backup" || exit 1
else
mv "$state_dir/.original_dns.txt" "$state_dir/.original_dns/$service_id" || exit 1
fi
fi fi
done
if [ ! -f "$backup" ] && [ -n "$2" ] && [ -f "$2/$backup" ]; then
cp "$2/$backup" ".original_dns/.$service_id" || exit 1
mv ".original_dns/.$service_id" "$backup" || exit 1
fi
backup_header='# clash-verge-dns-managed'
function save_backup() {
printf '%s\n%s\n' "$backup_header" "$1" >".original_dns/.$service_id" &&
mv ".original_dns/.$service_id" "$backup"
}
saved_dns=
legacy_backup=false
if [ -f "$backup" ]; then
saved_dns=$(cat "$backup") || exit 1
case "$saved_dns" in
"$backup_header"$'\n'*) saved_dns=${saved_dns#*$'\n'} ;;
*) legacy_backup=true ;;
esac
fi fi
# The placeholder is ours, not the user's: keep the backup taken before it was applied.
if [ "$original_dns" != "$1" ]; then if [ "$original_dns" != "$1" ]; then
# Only an unmarked historical backup owns the old single-address placeholder. echo "$original_dns" >"$backup" || exit 1
if [ "$original_dns" != "114.114.114.114" ] || [ "$legacy_backup" != true ]; then networksetup -setdnsservers "$hardware_port" "$1"
save_backup "$original_dns" || exit 1
legacy_backup=false
fi
networksetup -setdnsservers "$hardware_port" "${dns_servers[@]}"
[ "$(dns_of "$hardware_port")" = "$1" ] || { echo "Failed to set DNS for $hardware_port." >&2; exit 1; } [ "$(dns_of "$hardware_port")" = "$1" ] || { echo "Failed to set DNS for $hardware_port." >&2; exit 1; }
fi elif [ ! -f "$backup" ]; then
# Keep legacy ownership until the new list is verified, including a retry after a failed marker write. echo "empty" >"$backup" || exit 1
if [ "$legacy_backup" = true ]; then
save_backup "$saved_dns" || exit 1
fi fi
+36 -65
View File
@@ -1,11 +1,9 @@
#!/bin/bash #!/bin/bash
[ $# -lt 1 ] && echo "Usage: $0 <newline-separated DNS servers> [legacy state directory]" >&2 && exit 1 [ $# -lt 1 ] && echo "Usage: $0 <placeholder IP address>" && exit 1
# Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent. # Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent.
function sc_value() { function sc_value() {
local dictionary printf 'show %s\nquit\n' "$1" | scutil | sed -n "s/^ *$2 : //p"
dictionary=$(printf 'show %s\nquit\n' "$1" | scutil) || return 1
sed -n "s/^ *$2 : //p" <<<"$dictionary"
} }
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer. # Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
@@ -23,69 +21,42 @@ function dns_of() {
echo "$dns" echo "$dns"
} }
backup_header='# clash-verge-dns-managed' result=0
completed_backups=() # Older releases kept a single backup for whichever service was primary.
if [ -f .original_dns.txt ]; then
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
if [ -z "$service_id" ] || ! mkdir -p .original_dns; then
result=1
elif [ -f ".original_dns/$service_id" ]; then
rm -f .original_dns.txt || result=1
elif ! mv .original_dns.txt ".original_dns/$service_id"; then
result=1
fi
fi
function restore_dns() { for backup in .original_dns/*; do
local result=0 [ -f "$backup" ] || continue
# Older releases kept a single backup for whichever service was primary. hardware_port=$(sc_value "Setup:/Network/Service/${backup##*/}" UserDefinedName)
if [ -f .original_dns.txt ]; then # Kept for a service that is gone: there is nothing to write it back to.
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService) [ -n "$hardware_port" ] || continue
if [ -z "$service_id" ] || ! mkdir -p .original_dns; then if ! current_dns=$(dns_of "$hardware_port"); then
result=1 echo "Cannot read DNS servers of $hardware_port." >&2
elif [ -f ".original_dns/$service_id" ]; then result=1
rm -f .original_dns.txt || result=1 continue
elif ! mv .original_dns.txt ".original_dns/$service_id"; then fi
# A service no longer on the placeholder was changed by someone else, and that change stands.
if [ "$current_dns" = "$1" ]; then
original_dns=$(cat "$backup") || { result=1; continue; }
# Older releases could back up the placeholder itself.
[ "$original_dns" = "$1" ] && original_dns=empty
networksetup -setdnsservers "$hardware_port" $original_dns
if [ "$(dns_of "$hardware_port")" != "$original_dns" ]; then
echo "Failed to restore DNS for $hardware_port." >&2
result=1 result=1
continue
fi fi
fi fi
rm -f "$backup" || result=1
for backup in .original_dns/*; do done
[ -f "$backup" ] || continue rmdir .original_dns 2>/dev/null
# Current backups take precedence even after their DNS has already been restored.
if [ "$PWD" != "$state_dir" ] && [ -f "$state_dir/$backup" ]; then
completed_backups+=("$PWD/$backup")
continue
fi
hardware_port=$(sc_value "Setup:/Network/Service/${backup##*/}" UserDefinedName) || { result=1; continue; }
# Kept for a service that is gone: there is nothing to write it back to.
[ -n "$hardware_port" ] || continue
if ! current_dns=$(dns_of "$hardware_port"); then
echo "Cannot read DNS servers of $hardware_port." >&2
result=1
continue
fi
original_dns=$(cat "$backup") || { result=1; continue; }
legacy_backup=false
case "$original_dns" in
"$backup_header"$'\n'*) original_dns=${original_dns#*$'\n'} ;;
*) legacy_backup=true ;;
esac
# A service no longer on its recorded placeholder was changed by someone else.
if [ "$current_dns" = "$1" ] || { [ "$legacy_backup" = true ] && [ "$current_dns" = "114.114.114.114" ]; }; then
networksetup -setdnsservers "$hardware_port" $original_dns
if [ "$(dns_of "$hardware_port")" != "$original_dns" ]; then
echo "Failed to restore DNS for $hardware_port." >&2
result=1
continue
fi
fi
completed_backups+=("$PWD/$backup")
done
return $result
}
result=0
state_dir=$PWD
restore_dns "$1" || result=1
if [ -n "$2" ] && [ "$2" != "$PWD" ]; then
{ cd "$2" && restore_dns "$1"; } || result=1
fi
# Remove superseded legacy backups first; a cleanup failure must leave current authority intact.
if [ "$result" -eq 0 ]; then
for ((index=${#completed_backups[@]}-1; index>=0; index--)); do
rm -f "${completed_backups[index]}" || { result=1; break; }
done
fi
rmdir "$state_dir/.original_dns" "${2:-$state_dir}/.original_dns" 2>/dev/null
exit $result exit $result
+20 -15
View File
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
identifier = "io.github.clash-verge-rev.clash-verge-rev" identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.7.4", features = [] } tauri-build = { version = "2.7.3", features = [] }
[dependencies] [dependencies]
clash-verge-draft = { workspace = true } clash-verge-draft = { workspace = true }
@@ -55,10 +55,11 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_yaml_ng = { workspace = true } serde_yaml_ng = { workspace = true }
smartstring = { workspace = true, features = ["serde"] } smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
socket2 = { workspace = true } socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] } warp = { version = "0.4.3", features = ["server"] }
open = "5.4.5" open = "5.4.4"
rfd = { version = "0.17.2", default-features = false, features = ["common-controls-v6"] } rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] }
dirs = "7.0.0" dirs = "7.0.0"
dunce = "1.0.5" dunce = "1.0.5"
nanoid = "0.5" nanoid = "0.5"
@@ -66,17 +67,19 @@ chrono = { workspace = true }
boa_engine = "0.22.0" boa_engine = "0.22.0"
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
reqwest = { workspace = true } reqwest = { workspace = true }
regex = { workspace = true }
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch = "main", features = [ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch = "main", features = [
"guard", "guard",
] } ] }
network-interface = { version = "2.0.5", features = ["serde"] } network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.6.0" tauri-plugin-shell = "2.5.0"
tauri-plugin-opener = "2.7.2" tauri-plugin-opener = "2.7.2"
tauri-plugin-dialog = "2.8.4" tauri-plugin-dialog = "2.8.3"
tauri-plugin-fs = "2.7.1" tauri-plugin-fs = "2.7.1"
tauri-plugin-deep-link = "2.6.3" tauri-plugin-process = "2.4.0"
tauri-plugin-deep-link = "2.6.2"
tauri-plugin-window-state = "2.5.0" tauri-plugin-window-state = "2.5.0"
zip = "9.3.0" zip = "9.1.3"
flate2 = "1.1.10" flate2 = "1.1.10"
reqwest_dav = "0.3.3" reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" } aes-gcm = { version = "0.11.1" }
@@ -86,26 +89,27 @@ getrandom = "0.4.3"
futures = "0.3.34" futures = "0.3.34"
gethostname = "1.1.0" gethostname = "1.1.0"
scopeguard = "1.2.0" scopeguard = "1.2.0"
tauri-plugin-notification = "2.8.0" tauri-plugin-notification = "2.7.0"
tokio-stream = "0.1.19"
backon = { version = "1.6.0", features = ["tokio-sleep"] } backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.3" tauri-plugin-http = "2.8.3"
tracing = { workspace = true } tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.3", optional = true } tauri-plugin-devtools = { version = "2.2.2", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" } tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
async-trait = "0.1.92" async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [ clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
"client", "client",
] } ] }
arc-swap = "1.9.3" arc-swap = "1.9.2"
tokio-rustls = "0.26.6" tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] } rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.11" webpki-roots = "1.0.9"
dark-light = "3.0.0" dark-light = "3.0.0"
bytes = "1.12.1" bytes = "1.12.1"
tokio-util = { version = "0.7.20", features = ["time"] } tokio-util = { version = "0.7.20", features = ["time"] }
specta = { version = "=2.0.0-rc.26", features = ["derive", "function", "serde_json"] } specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
tauri-specta = { version = "=2.0.0-rc.26", features = ["derive", "typescript"] } tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
specta-typescript = "=0.0.13" specta-typescript = "=0.0.12"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.191" libc = "0.2.191"
@@ -115,7 +119,7 @@ libc = "0.2.191"
clash-verge-signal = { workspace = true } clash-verge-signal = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-wdio-webdriver = { version = "=1.5.1", optional = true } tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true }
objc2 = "0.6.5" objc2 = "0.6.5"
objc2-foundation = { version = "0.3.2", features = [ objc2-foundation = { version = "0.3.2", features = [
"NSString", "NSString",
@@ -152,6 +156,7 @@ tauri-plugin-global-shortcut = "2.4.1"
tauri-plugin-updater = "2.14.1" tauri-plugin-updater = "2.14.1"
[dev-dependencies] [dev-dependencies]
criterion = { workspace = true }
tokio = { workspace = true, features = ["test-util"] } tokio = { workspace = true, features = ["test-util"] }
warp = { version = "0.4.3", features = ["test"] } warp = { version = "0.4.3", features = ["test"] }
+1
View File
@@ -73,6 +73,7 @@
"global-shortcut:allow-register-all", "global-shortcut:allow-register-all",
"global-shortcut:allow-unregister", "global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all", "global-shortcut:allow-unregister-all",
"process:allow-exit",
"clipboard-manager:allow-read-text", "clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text", "clipboard-manager:allow-write-text",
"dialog:default" "dialog:default"
+57 -3
View File
@@ -5,10 +5,16 @@ use crate::{
core::{ core::{
handle::Handle, handle::Handle,
proxy_view::{GroupScope, ProxyViewBuilder, ProxyViewInput, ProxyViewV1}, proxy_view::{GroupScope, ProxyViewBuilder, ProxyViewInput, ProxyViewV1},
tray::Tray,
}, },
process::AsyncHandler,
}; };
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
use std::collections::{BTreeMap, HashSet}; use std::{
collections::{BTreeMap, HashSet},
sync::atomic::{AtomicBool, Ordering},
};
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites. /// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
#[tauri::command] #[tauri::command]
@@ -21,12 +27,15 @@ pub async fn record_selected_node(group_name: String, node: String) -> CmdResult
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn switch_proxy_node(group_name: String, proxy_name: String, unfix: bool) -> CmdResult<()> { pub async fn forget_selected_node(group_name: String) -> CmdResult<()> {
crate::feat::switch_proxy_node(&group_name, &proxy_name, unfix) crate::config::profiles::forget_selected_node(&group_name)
.await .await
.stringify_err() .stringify_err()
} }
static TRAY_SYNC_RUNNING: AtomicBool = AtomicBool::new(false);
static TRAY_SYNC_PENDING: AtomicBool = AtomicBool::new(false);
fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> { fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> {
let mut seen = HashSet::new(); let mut seen = HashSet::new();
@@ -115,3 +124,48 @@ pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
providers: providers.ok(), providers: providers.ok(),
})) }))
} }
#[tauri::command]
#[specta::specta]
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
if TRAY_SYNC_RUNNING
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
AsyncHandler::spawn(move || async move {
run_tray_sync_loop().await;
});
} else {
TRAY_SYNC_PENDING.store(true, Ordering::Release);
}
Ok(())
}
async fn run_tray_sync_loop() {
// Permanent watcher: keeps syncing until a pass finds no pending sync request.
loop {
match Tray::global().update_menu().await {
Ok(_) => {
logging!(debug, Type::Cmd, "Tray proxy selection synced successfully");
}
Err(e) => {
logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e:#}");
}
}
if !TRAY_SYNC_PENDING.swap(false, Ordering::AcqRel) {
TRAY_SYNC_RUNNING.store(false, Ordering::Release);
if TRAY_SYNC_PENDING.swap(false, Ordering::AcqRel)
&& TRAY_SYNC_RUNNING
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
continue;
}
break;
}
}
}
-53
View File
@@ -614,53 +614,6 @@ async fn select_node_with_timeout(group_name: &String, node: &String) -> Result<
.with_context(|| format!("failed to select node [{node}] for group [{group_name}]")) .with_context(|| format!("failed to select node [{node}] for group [{group_name}]"))
} }
// mihomo lists these first in GLOBAL, so an unpicked GLOBAL starts on DIRECT.
const GLOBAL_BUILTINS: [&str; 2] = ["DIRECT", "REJECT"];
/// GLOBAL's first visible group, else its first node, while GLOBAL is still on a built-in.
fn global_exit_target(proxies: &Proxies) -> Option<&str> {
let global = proxies.proxies.get("GLOBAL")?;
if !global.now.as_deref().is_some_and(|now| GLOBAL_BUILTINS.contains(&now)) {
return None;
}
let members = global.all.as_deref()?;
let is_visible_group = |name: &str| {
proxies.proxies.get(name).is_some_and(|proxy| {
proxy.hidden != Some(true)
&& matches!(
proxy.proxy_type,
ProxyType::Selector | ProxyType::URLTest | ProxyType::Fallback | ProxyType::LoadBalance
)
})
};
members
.iter()
.find(|name| is_visible_group(name))
.or_else(|| members.iter().find(|name| !GLOBAL_BUILTINS.contains(&name.as_str())))
.map(|name| name.as_str())
}
/// Moves GLOBAL off a built-in unless the profile records any GLOBAL choice.
pub(crate) async fn leave_default_global_exit() -> Result<()> {
let profiles = Config::profiles().await.data_arc();
let has_record = profiles
.current
.as_ref()
.and_then(|current| profiles.get_item(current).ok())
.and_then(|item| item.selected.as_deref())
.is_some_and(|selected| selected.iter().any(|item| item.name.as_deref() == Some("GLOBAL")));
if has_record {
return Ok(());
}
let snapshot = fetch_proxies_with_timeout().await?;
let Some(target) = global_exit_target(&snapshot) else {
return Ok(());
};
select_node_with_timeout(&String::from("GLOBAL"), &String::from(target)).await?;
logging!(info, Type::Config, "moved GLOBAL off its built-in default to {target}");
Ok(())
}
fn remaining_activations( fn remaining_activations(
activations: &[(String, String)], activations: &[(String, String)],
completed: &HashMap<String, String>, completed: &HashMap<String, String>,
@@ -1066,12 +1019,6 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot
.clone() .clone()
.unwrap_or_default(); .unwrap_or_default();
if Config::clash().await.latest_arc().get_mode().as_deref() == Some("global")
&& let Err(err) = leave_default_global_exit().await
{
logging!(warn, Type::Config, "failed to move GLOBAL off its default: {err:#}");
}
if selected.is_empty() { if selected.is_empty() {
if is_activation_current(generation) { if is_activation_current(generation) {
announce(Refresh::Clash); announce(Refresh::Clash);
+5 -19
View File
@@ -559,13 +559,12 @@ impl CoreManager {
})?; })?;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
crate::utils::resolve::dns::sync_public_dns().await; crate::utils::resolve::dns::sync_public_dns().await;
// Only automatic restores can wait for the watcher; explicit requests must report failure. // At login the app usually beats the network. Leave the write to the network watcher
// rather than fail — only if the watcher is actually live; a user toggling while
// offline still fails fast.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let defer_until_network = crate::core::network_watch::is_armed() if crate::core::network_watch::is_armed()
&& crate::core::notification::what_was_asked() && !crate::utils::resolve::is_resolve_done()
== crate::core::notification::FailedOperation::SystemProxyRestore;
#[cfg(target_os = "macos")]
if defer_until_network
&& Config::verge() && Config::verge()
.await .await
.latest_arc() .latest_arc()
@@ -580,20 +579,7 @@ impl CoreManager {
); );
return Ok(()); return Ok(());
} }
#[cfg(not(target_os = "macos"))]
proxy_control::apply().await?; proxy_control::apply().await?;
#[cfg(target_os = "macos")]
if let Err(error) = proxy_control::apply().await {
if defer_until_network && proxy_control::is_missing_network_service_failure(&error) {
logging!(
debug,
Type::Core,
"network service disappeared; the system proxy waits for its return"
);
return Ok(());
}
return Err(error);
}
if !expectation.is_valid( if !expectation.is_valid(
self.get_running_mode().as_ref(), self.get_running_mode().as_ref(),
self.current_core_readiness_generation(), self.current_core_readiness_generation(),
-1
View File
@@ -49,7 +49,6 @@ async fn reapply() {
.enable_system_proxy .enable_system_proxy
.unwrap_or_default() .unwrap_or_default()
{ {
crate::utils::resolve::dns::sync_public_dns().await;
return; return;
} }
if !proxy_control::has_network_service().await { if !proxy_control::has_network_service().await {
+4 -24
View File
@@ -180,14 +180,6 @@ pub const fn is_missing_network_service(_error: &sysproxy::Error) -> bool {
false false
} }
pub fn is_missing_network_service_failure(error: &anyhow::Error) -> bool {
!SystemProxyStateUnknown::is_in(error)
&& SysproxyFailure::from_chain(error).is_none()
&& error
.downcast_ref::<sysproxy::Error>()
.is_some_and(is_missing_network_service)
}
/// Distinguish a misplaced sidecar from a missing privileged service. /// Distinguish a misplaced sidecar from a missing privileged service.
async fn classify_local_apply_failure(error: anyhow::Error) -> anyhow::Error { async fn classify_local_apply_failure(error: anyhow::Error) -> anyhow::Error {
if SysproxyFailure::from_chain(&error).is_some() || !was_refused_locally(&error) { if SysproxyFailure::from_chain(&error).is_some() || !was_refused_locally(&error) {
@@ -367,13 +359,6 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
ProxyApplyOutcome::Applied | ProxyApplyOutcome::NotRequested => Ok(()), ProxyApplyOutcome::Applied | ProxyApplyOutcome::NotRequested => Ok(()),
// Direct fallback satisfies a disable request. // Direct fallback satisfies a disable request.
ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()), ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()),
// Older helpers encode a failed service lookup as fallback even when no OS write occurred.
#[cfg(target_os = "macos")]
ProxyApplyOutcome::DirectFallback { message }
if message == sysproxy::Error::NoActiveNetworkService.to_string() =>
{
Err(sysproxy::Error::NoActiveNetworkService.into())
}
ProxyApplyOutcome::DirectFallback { message } => { ProxyApplyOutcome::DirectFallback { message } => {
Err(SysproxyFailure::DirectFallback { detail: message }.into()) Err(SysproxyFailure::DirectFallback { detail: message }.into())
} }
@@ -624,10 +609,6 @@ pub async fn refresh_guard() -> Result<()> {
logging!(debug, Type::Core, "proxy guard superseded (generation {generation})"); logging!(debug, Type::Core, "proxy guard superseded (generation {generation})");
break; break;
} }
Err(error) if is_missing_network_service_failure(&error) => {
consecutive_failures = 0;
logging!(debug, Type::Core, "system proxy guard waits for a network service");
}
Err(error) => { Err(error) => {
consecutive_failures += 1; consecutive_failures += 1;
logging!( logging!(
@@ -1129,11 +1110,10 @@ mod tests {
.err() .err()
.unwrap_or_else(|| anyhow::anyhow!("an enable that never reached the OS must fail")); .unwrap_or_else(|| anyhow::anyhow!("an enable that never reached the OS must fail"));
assert!(super::is_missing_network_service_failure(&error)); assert_eq!(
assert!(SysproxyFailure::from_chain(&error).is_none()); SysproxyFailure::from_chain(&error).map(SysproxyFailure::code),
assert!(!super::is_missing_network_service_failure( Some("SYSPROXY_DIRECT_FALLBACK")
&error.context(SystemProxyStateUnknown) );
));
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
+1 -1
View File
@@ -1970,7 +1970,7 @@ impl ServiceManager {
async fn run_operation(&self, operation: impl Future<Output = Result<()>>) -> Result<()> { async fn run_operation(&self, operation: impl Future<Output = Result<()>>) -> Result<()> {
run_operation_and_then(&RUN_STATE, operation, || async { run_operation_and_then(&RUN_STATE, operation, || async {
if let Err(error) = Tray::global().update_part().await { if let Err(error) = Tray::global().update_menu().await {
logging!( logging!(
warn, warn,
Type::Service, Type::Service,
+2 -2
View File
@@ -7,7 +7,6 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
use clash_verge_logging::{Type, logging, logging_error}; use clash_verge_logging::{Type, logging, logging_error};
use futures::StreamExt as _;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use smartstring::alias::String; use smartstring::alias::String;
use std::{ use std::{
@@ -22,6 +21,7 @@ use tokio::{
sync::mpsc, sync::mpsc,
time::{sleep, timeout}, time::{sleep, timeout},
}; };
use tokio_stream::StreamExt as _;
use tokio_util::time::{DelayQueue, delay_queue::Key}; use tokio_util::time::{DelayQueue, delay_queue::Key};
enum TimerCommand { enum TimerCommand {
@@ -501,7 +501,6 @@ impl Timer {
mod tests { mod tests {
use super::{TaskSchedule, Timer, run_timer_profile_update_transition}; use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
use crate::config::{PrfItem, PrfOption}; use crate::config::{PrfItem, PrfOption};
use futures::StreamExt as _;
use parking_lot::Mutex; use parking_lot::Mutex;
use smartstring::alias::String; use smartstring::alias::String;
use std::collections::HashMap; use std::collections::HashMap;
@@ -514,6 +513,7 @@ mod tests {
time::Duration, time::Duration,
}; };
use tokio::sync::Barrier; use tokio::sync::Barrier;
use tokio_stream::StreamExt as _;
use tokio_util::time::DelayQueue; use tokio_util::time::DelayQueue;
struct CancellationProbe { struct CancellationProbe {
+1 -3
View File
@@ -1000,9 +1000,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
Some((g, p)) => (g, p), Some((g, p)) => (g, p),
None => return, None => return,
}; };
if let Err(err) = feat::switch_proxy_node(group_name, proxy_name, false).await { feat::switch_proxy_node(group_name, proxy_name).await;
logging!(error, Type::Tray, "Failed to switch proxy: {err:#}");
}
} }
_ => { _ => {
logging!(debug, Type::Tray, "Unhandled tray menu event: {:?}", event.id); logging!(debug, Type::Tray, "Unhandled tray menu event: {:?}", event.id);
+2 -31
View File
@@ -1,13 +1,10 @@
use anyhow::Result; use anyhow::Result;
use nanoid::nanoid;
use scopeguard::defer; use scopeguard::defer;
use serde::Serialize; use serde::Serialize;
use smartstring::alias::String; use smartstring::alias::String;
use std::{ use std::{
borrow::Cow,
fmt, fmt,
hash::{DefaultHasher, Hash as _, Hasher as _}, hash::{DefaultHasher, Hash as _, Hasher as _},
path::Path,
sync::LazyLock, sync::LazyLock,
sync::atomic::{AtomicBool, Ordering}, sync::atomic::{AtomicBool, Ordering},
}; };
@@ -332,7 +329,7 @@ impl CoreConfigValidator {
Ok(result) => result, Ok(result) => result,
Err(err) => { Err(err) => {
logging!(warn, Type::Validate, "无法确定文件类型: {}, 错误: {}", config_path, err); logging!(warn, Type::Validate, "无法确定文件类型: {}, 错误: {}", config_path, err);
return Self::validate_profile_yaml_outcome(config_path).await; return Self::validate_config_internal_outcome(config_path).await;
} }
}; };
@@ -347,33 +344,7 @@ impl CoreConfigValidator {
} }
logging!(info, Type::Validate, "使用Clash内核验证配置文件: {}", config_path); logging!(info, Type::Validate, "使用Clash内核验证配置文件: {}", config_path);
Self::validate_profile_yaml_outcome(config_path).await Self::validate_config_internal_outcome(config_path).await
}
/// The user file is saved as written, so the core checks a copy carrying the tolerated short-ids.
async fn validate_profile_yaml_outcome(config_path: &str) -> Result<ValidationOutcome> {
let Some(tolerant) = Self::short_id_tolerant_text(config_path).await else {
return Self::validate_config_internal_outcome(config_path).await;
};
let Ok(app_dir) = dirs::app_home_dir() else {
return Self::validate_config_internal_outcome(config_path).await;
};
let check_path = app_dir.join(format!("short-id-check-{}.yaml", nanoid!()));
defer! {
let _ = std::fs::remove_file(&check_path);
}
if help::save_yaml_str(&check_path, &tolerant).await.is_err() {
return Self::validate_config_internal_outcome(config_path).await;
}
Self::validate_config_internal_outcome(dirs::path_to_str(&check_path)?).await
}
async fn short_id_tolerant_text(config_path: &str) -> Option<std::string::String> {
let yaml = fs::read_to_string(config_path).await.ok()?;
match help::short_id_tolerant(&yaml, Path::new(config_path)) {
Ok((Cow::Owned(tolerant), _)) => Some(tolerant),
_ => None,
}
} }
async fn validate_config_internal_outcome(config_path: &str) -> Result<ValidationOutcome> { async fn validate_config_internal_outcome(config_path: &str) -> Result<ValidationOutcome> {
+1 -52
View File
@@ -3,10 +3,8 @@ use crate::{
config::PrfItem, config::PrfItem,
utils::{dirs, help}, utils::{dirs, help},
}; };
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
use smartstring::alias::String; use smartstring::alias::String;
use std::{borrow::Cow, path::Path};
use tokio::fs; use tokio::fs;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -50,8 +48,7 @@ impl AsyncChainItemFrom for Option<ChainItem> {
data: ChainType::Merge(help::read_mapping(&path).await.ok()?), data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
}), }),
"rules" | "proxies" | "groups" => { "rules" | "proxies" | "groups" => {
let yaml = fs::read_to_string(&path).await.ok()?; let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
let seq_map = parse_seq_map(&yaml, &path)?;
let data = match itype { let data = match itype {
"rules" => ChainType::Rules(seq_map), "rules" => ChainType::Rules(seq_map),
"proxies" => ChainType::Proxies(seq_map), "proxies" => ChainType::Proxies(seq_map),
@@ -63,51 +60,3 @@ impl AsyncChainItemFrom for Option<ChainItem> {
} }
} }
} }
/// Typed parsing reads `delete: [1234]` as strings where a `Value` round trip would reject it.
fn parse_seq_map(yaml: &str, path: &Path) -> Option<SeqMap> {
let original = serde_yaml_ng::from_str::<SeqMap>(yaml).ok()?;
let Ok((Cow::Owned(tolerant), _)) = help::short_id_tolerant(yaml, path) else {
return Some(original);
};
match serde_yaml_ng::from_str::<SeqMap>(&tolerant) {
Ok(seq_map) => Some(seq_map),
Err(_) => {
logging!(
warn,
Type::Config,
"kept numeric short-id in {}: quoted text does not load",
path.display()
);
Some(original)
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
mod tests {
use super::parse_seq_map;
use serde_yaml_ng::Value;
use std::path::Path;
#[test]
fn seq_short_id_keeps_typed_parse() {
for (input, delete) in [
("prepend: []\nappend: []\ndelete: [1234]\n", "1234"),
("prepend: []\nappend: []\ndelete: [true]\n", "true"),
] {
let seq_map = parse_seq_map(input, Path::new("b8.yaml")).expect("seq file should load");
assert_eq!(seq_map.delete, [delete]);
}
let input = "prepend:\n - name: r\n reality-opts:\n short-id: 1234e123\nappend: []\ndelete: []\n";
let seq_map = parse_seq_map(input, Path::new("seq.yaml")).expect("seq file should load");
let short_id = seq_map
.prepend
.first()
.and_then(|proxy| proxy.get("reality-opts"))
.and_then(|opts| opts.get("short-id"));
assert_eq!(short_id, Some(&Value::String("1234e123".into())));
}
}
-6
View File
@@ -66,12 +66,6 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
return Err(err.to_string().into()); return Err(err.to_string().into());
} }
if mode.as_str() == "global"
&& let Err(err) = crate::config::profiles::leave_default_global_exit().await
{
logging!(warn, Type::Core, "failed to move GLOBAL off its default: {err:#}");
}
let clash = Config::clash().await; let clash = Config::clash().await;
let save_result = { let save_result = {
let _config_write = Config::lock_config_write().await; let _config_write = Config::lock_config_write().await;
+24 -73
View File
@@ -4,7 +4,6 @@ use crate::{
cmd, cmd,
config::{Config, PrfItem, PrfOption, profiles::profiles_update_item_safe}, config::{Config, PrfItem, PrfOption, profiles::profiles_update_item_safe},
core::{CoreManager, handle, tray, validate::ValidationOutcome}, core::{CoreManager, handle, tray, validate::ValidationOutcome},
process::AsyncHandler,
utils::help::{mask_err, mask_url}, utils::help::{mask_err, mask_url},
}; };
use anyhow::{Result, bail}; use anyhow::{Result, bail};
@@ -22,85 +21,37 @@ pub async fn toggle_proxy_profile(profile_index: String) {
} }
} }
pub async fn switch_proxy_node(group_name: &str, proxy_name: &str, unfix: bool) -> Result<()> { /// Tell the profile which node this group is on now.
let mihomo = handle::Handle::mihomo(); ///
let previous_proxy = if Config::verge().await.data_arc().auto_close_connection.unwrap_or(false) { /// The core is already switched by the time this runs, so failing to record is not a reason to
match mihomo.get_proxy_by_name(group_name).await { /// report the switch as failed — but it is worth a log line, because what the profile holds is
Ok(group) => group.now.filter(|name| !name.is_empty()), /// what gets re-applied the next time a core starts.
Err(err) => { async fn record_switched_node(group_name: &str, proxy_name: &str) {
logging!( if let Err(error) = crate::config::profiles::record_selected_node(group_name, proxy_name).await {
warn,
Type::Core,
"Failed to read previous proxy selection for {group_name}: {err}"
);
None
}
}
} else {
None
};
if unfix {
mihomo.unfixed_proxy(group_name).await?;
} else {
mihomo.select_node_for_group(group_name, proxy_name).await?;
}
let save_result = if unfix {
crate::config::profiles::forget_selected_node(group_name).await
} else {
crate::config::profiles::record_selected_node(group_name, proxy_name).await
};
// The core selection is already applied; persistence failure must not report it as failed.
if let Err(error) = save_result {
logging!( logging!(
warn, warn,
Type::Config, Type::Config,
"Failed to persist proxy selection for {group_name}: {error:#}" "切换代理成功但未能记录到配置: {} -> {}, 错误: {error:#}",
group_name,
proxy_name
); );
} }
announce(Refresh::Proxies); }
if let Some(previous_proxy) = previous_proxy { pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
AsyncHandler::spawn(move || async move { match handle::Handle::mihomo()
let connections = match mihomo.get_connections().await { .select_node_for_group(group_name, proxy_name)
Ok(connections) => connections, .await
Err(err) => { {
logging!( Ok(_) => {
warn, record_switched_node(group_name, proxy_name).await;
Type::Core, announce(Refresh::Proxies);
"Failed to read connections after switching proxy: {err}" let _ = tray::Tray::global().update_menu().await;
); }
return; Err(err) => {
} logging!(error, Type::Tray, "切换代理失败: {err:?}");
}; }
futures::future::join_all(
connections
.connections
.unwrap_or_default()
.into_iter()
.filter(|connection| connection.chains.contains(&previous_proxy))
.map(|connection| async move {
if let Err(err) = mihomo.close_connection(&connection.id).await {
logging!(
warn,
Type::Core,
"Failed to close connection {} after switching proxy: {err}",
connection.id
);
}
}),
)
.await;
});
} }
if let Err(err) = tray::Tray::global().update_menu().await {
logging!(
warn,
Type::Tray,
"Failed to refresh tray after switching proxy: {err:#}"
);
}
Ok(())
} }
async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> { async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> {
+2 -1
View File
@@ -45,8 +45,9 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
cmd::update_proxy_chain_config_in_runtime, cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool, cmd::invoke_uwp_tool,
cmd::copy_clash_env, cmd::copy_clash_env,
cmd::sync_tray_proxy_selection,
cmd::record_selected_node, cmd::record_selected_node,
cmd::switch_proxy_node, cmd::forget_selected_node,
cmd::save_dns_config, cmd::save_dns_config,
cmd::apply_dns_config, cmd::apply_dns_config,
cmd::set_dns_override, cmd::set_dns_override,
+1
View File
@@ -54,6 +54,7 @@ mod app_init {
.build(), .build(),
) )
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build()) .plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
+2 -285
View File
@@ -7,8 +7,6 @@ use scopeguard::{ScopeGuard, guard};
use serde::{Serialize, de::DeserializeOwned}; use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::{Mapping, Value}; use serde_yaml_ng::{Mapping, Value};
use std::{ use std::{
borrow::Cow,
ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
@@ -47,8 +45,8 @@ pub async fn read_mapping(path: &Path) -> Result<Mapping> {
} }
fn parse_mapping(yaml_str: String, path: PathBuf) -> Result<Mapping> { fn parse_mapping(yaml_str: String, path: PathBuf) -> Result<Mapping> {
match short_id_tolerant(&yaml_str, &path) { match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&yaml_str) {
Ok((_, mut val)) => { Ok(mut val) => {
val.apply_merge() val.apply_merge()
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?; .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
@@ -68,150 +66,6 @@ fn parse_mapping(yaml_str: String, path: PathBuf) -> Result<Mapping> {
} }
} }
const SHORT_ID_KEY: &str = "short-id:";
/// A float like `1234e123` loses its hex text, so quote short-ids before parsing and keep that only if nothing else changed.
pub(crate) fn short_id_tolerant<'a>(yaml: &'a str, path: &Path) -> serde_yaml_ng::Result<(Cow<'a, str>, Value)> {
let original = serde_yaml_ng::from_str::<Value>(yaml)?;
if !has_numeric_short_id(&original) {
return Ok((Cow::Borrowed(yaml), original));
}
let quoted = quote_plain_short_ids(yaml);
if matches!(quoted, Cow::Borrowed(_)) {
return Ok((quoted, original));
}
match serde_yaml_ng::from_str::<Value>(&quoted) {
Ok(tolerant) if same_except_short_id_text(&original, &tolerant) => Ok((quoted, tolerant)),
_ => {
logging!(
warn,
Type::Config,
"kept numeric short-id in {}: quoting it would change other values",
path.display()
);
Ok((Cow::Borrowed(yaml), original))
}
}
}
fn has_numeric_short_id(value: &Value) -> bool {
match value {
Value::Mapping(map) => map
.iter()
.any(|(key, value)| (is_short_id_key(key) && value.is_number()) || has_numeric_short_id(value)),
Value::Sequence(seq) => seq.iter().any(has_numeric_short_id),
Value::Tagged(tagged) => has_numeric_short_id(&tagged.value),
_ => false,
}
}
fn same_except_short_id_text(original: &Value, tolerant: &Value) -> bool {
match (original, tolerant) {
(Value::Mapping(original), Value::Mapping(tolerant)) => {
original.len() == tolerant.len()
&& original
.iter()
.zip(tolerant.iter())
.all(|((key, old), (new_key, new))| {
key == new_key && (same_except_short_id_text(old, new) || is_short_id_text(key, old, new))
})
}
(Value::Sequence(original), Value::Sequence(tolerant)) => {
original.len() == tolerant.len()
&& original
.iter()
.zip(tolerant.iter())
.all(|(old, new)| same_except_short_id_text(old, new))
}
(Value::Tagged(original), Value::Tagged(tolerant)) => {
original.tag == tolerant.tag && same_except_short_id_text(&original.value, &tolerant.value)
}
_ => original == tolerant,
}
}
fn is_short_id_text(key: &Value, old: &Value, new: &Value) -> bool {
is_short_id_key(key)
&& old.is_number()
&& matches!(new, Value::String(text) if serde_yaml_ng::from_str::<Value>(text).ok().as_ref() == Some(old))
}
fn is_short_id_key(key: &Value) -> bool {
key.as_str() == Some("short-id")
}
fn quote_plain_short_ids(yaml: &str) -> Cow<'_, str> {
let mut quoted = String::new();
let mut copied = 0;
for (key, _) in yaml.match_indices(SHORT_ID_KEY) {
let Some(value) = plain_short_id_at(yaml, key) else {
continue;
};
let (Some(head), Some(id)) = (yaml.get(copied..value.start), yaml.get(value.clone())) else {
continue;
};
quoted.push_str(head);
quoted.push('"');
quoted.push_str(id);
quoted.push('"');
copied = value.end;
}
if copied == 0 {
return Cow::Borrowed(yaml);
}
quoted.push_str(yaml.get(copied..).unwrap_or_default());
Cow::Owned(quoted)
}
fn plain_short_id_at(yaml: &str, key: usize) -> Option<Range<usize>> {
let bytes = yaml.as_bytes();
let before = bytes.get(..key)?;
let flow = matches!(
before.iter().rev().find(|byte| !byte.is_ascii_whitespace()),
Some(b'{' | b',')
);
let line_start = before
.iter()
.rev()
.take_while(|&&byte| byte != b'\n')
.all(|&byte| is_blank(byte));
if !flow && !line_start {
return None;
}
let start = key + SHORT_ID_KEY.len();
let gap = bytes.get(start..)?.iter().take_while(|&&byte| is_blank(byte)).count();
if gap == 0 {
return None;
}
let start = start + gap;
let len = bytes
.get(start..)?
.iter()
.take_while(|byte| byte.is_ascii_hexdigit())
.count();
if len == 0 || !ends_plain_short_id(bytes.get(start + len..)?, flow) {
return None;
}
Some(start..start + len)
}
fn ends_plain_short_id(rest: &[u8], flow: bool) -> bool {
let blanks = rest.iter().take_while(|&&byte| is_blank(byte)).count();
match rest.get(blanks..) {
Some([] | [b'\n', ..] | [b'\r', b'\n', ..]) => true,
Some([b'#', ..]) => blanks > 0,
Some([b',' | b'}', ..]) => flow,
_ => false,
}
}
const fn is_blank(byte: u8) -> bool {
matches!(byte, b' ' | b'\t')
}
pub async fn save_yaml<T: Serialize + Sync>(path: &Path, data: &T, prefix: Option<&str>) -> Result<()> { pub async fn save_yaml<T: Serialize + Sync>(path: &Path, data: &T, prefix: Option<&str>) -> Result<()> {
let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?; let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?;
@@ -465,140 +319,3 @@ pub fn snapshot_path(original_path: &Path) -> Result<PathBuf> {
Ok(temp_path) Ok(temp_path)
} }
#[cfg(test)]
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
mod tests {
use super::{has_numeric_short_id, parse_mapping, quote_plain_short_ids, short_id_tolerant};
use serde_yaml_ng::Value;
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
#[test]
fn quotes_plain_short_ids() {
let cases = [
("short-id: 1234e123", "short-id: \"1234e123\""),
("short-id: 0e5", "short-id: \"0e5\""),
("short-id: 12345678", "short-id: \"12345678\""),
("short-id: 6ba85179", "short-id: \"6ba85179\""),
("short-id: \"1234e123\"", "short-id: \"1234e123\""),
("short-id: '1234e123'", "short-id: '1234e123'"),
("short-id: \"\"", "short-id: \"\""),
("short-id:\npublic-key: k", "short-id:\npublic-key: k"),
("short-id: null", "short-id: null"),
("short-id: ~", "short-id: ~"),
("short-id: 1234e123 # note", "short-id: \"1234e123\" # note"),
("# short-id: 1234e123", "# short-id: 1234e123"),
(
"reality-opts: {public-key: k, short-id: 1234e123}",
"reality-opts: {public-key: k, short-id: \"1234e123\"}",
),
(
"reality-opts: {short-id: 1234e123, public-key: k}",
"reality-opts: {short-id: \"1234e123\", public-key: k}",
),
(
"{\n public-key: k,\n short-id: 1234e123\n}",
"{\n public-key: k,\n short-id: \"1234e123\"\n}",
),
("short-id: &sid 1234e123", "short-id: &sid 1234e123"),
("short-id: !!str 1234e123", "short-id: !!str 1234e123"),
("short-id: abc,def", "short-id: abc,def"),
("short-id: 1234e123\r\n", "short-id: \"1234e123\"\r\n"),
("my-short-id: 1234e123", "my-short-id: 1234e123"),
("xshort-id: 1", "xshort-id: 1"),
("short-id:1234e123", "short-id:1234e123"),
("short-id: 1.5e3", "short-id: 1.5e3"),
];
for (input, expected) in cases {
let output = quote_plain_short_ids(input);
assert_eq!(output, expected, "input: {input:?}");
assert_eq!(
matches!(output, Cow::Borrowed(_)),
input == expected,
"input: {input:?}"
);
assert_eq!(quote_plain_short_ids(&output), expected, "rerun: {input:?}");
}
}
#[test]
fn plain_short_id_survives_profile_parse() {
let profile = "\
proxies:
- name: r
type: vless
reality-opts:
public-key: SFv3pZQdZ1fpnNg9yI2RwXKqAOz4mZHRbT2Wq4kz8Ws
short-id: 1234e123
";
let mapping = parse_mapping(profile.into(), PathBuf::from("profile.yaml")).expect("profile should parse");
let short_id = mapping
.get("proxies")
.and_then(|proxies| proxies.get(0))
.and_then(|proxy| proxy.get("reality-opts"))
.and_then(|opts| opts.get("short-id"));
assert_eq!(short_id, Some(&Value::String("1234e123".into())));
let output = serde_yaml_ng::to_string(&mapping).expect("mapping should serialize");
assert!(output.contains("short-id: '1234e123'"), "{output}");
}
#[test]
fn short_id_rewrite_falls_back_to_original_parse() {
// (case, input, has a numeric short-id today)
let cases = [
("B2", "proxies:\n - name: \"node, short-id: 1234e123}\"\n", false),
(
"B3",
"proxies:\n - name: \"node, short-id: 1234e123}\"\n - name: r\n reality-opts:\n short-id: 1234e123\n",
true,
),
(
"B4",
"payload: |\n short-id: 1234e123\nreality-opts:\n short-id: 1234e123\n",
true,
),
("B5", "reality-opts:\n short-id: 1234e123\n more\n", false),
("B6", "reality-opts: {public-key: k, # c\n short-id: 1234e123}\n", true),
];
for (case, input, numeric) in cases {
let today = serde_yaml_ng::from_str::<Value>(input).expect("input should parse");
assert_eq!(has_numeric_short_id(&today), numeric, "{case}");
let (text, value) = short_id_tolerant(input, Path::new("b.yaml")).expect("input should parse");
assert!(matches!(text, Cow::Borrowed(_)), "{case}");
assert_eq!(value, today, "{case}");
}
}
#[test]
fn short_id_rewrite_keeps_every_numeric_short_id() {
let input = "\
proxies:
- name: a
reality-opts:
short-id: 1234e123
- {name: b, reality-opts: {short-id: 0e5}}
";
let (text, value) = short_id_tolerant(input, Path::new("b7.yaml")).expect("input should parse");
assert!(matches!(text, Cow::Owned(_)));
let short_ids: Vec<_> = value
.get("proxies")
.and_then(Value::as_sequence)
.expect("proxies should be a sequence")
.iter()
.map(|proxy| proxy.get("reality-opts").and_then(|opts| opts.get("short-id")).cloned())
.collect();
assert_eq!(
short_ids,
[
Some(Value::String("1234e123".into())),
Some(Value::String("0e5".into()))
]
);
}
}
+54 -24
View File
@@ -4,14 +4,16 @@ use serde_yaml_ng::{Mapping, Value};
use std::{ use std::{
io::Read as _, io::Read as _,
os::unix::process::CommandExt as _, os::unix::process::CommandExt as _,
path::PathBuf, path::{Path, PathBuf},
process::{Command, Stdio}, process::{Command, Stdio},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tokio::sync::Mutex; use tokio::sync::Mutex;
const DNS_STATE_FILE: &str = ".original_dns.txt";
const DNS_BACKUP_DIR: &str = ".original_dns";
// Any address routed into TUN works, since dns-hijack answers it; the value only matters if left behind. // Any address routed into TUN works, since dns-hijack answers it; the value only matters if left behind.
const PLACEHOLDER_DNS: &str = "114.114.114.114\n223.5.5.5\n1.1.1.1"; const PLACEHOLDER_DNS: &str = "114.114.114.114";
// Scripts run under lifecycle_lock, and networksetup waits for other preference writers without a limit. // Scripts run under lifecycle_lock, and networksetup waits for other preference writers without a limit.
pub(crate) const DNS_SCRIPT_TIMEOUT: Duration = Duration::from_secs(5); pub(crate) const DNS_SCRIPT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -24,6 +26,14 @@ fn dns_state_dir() -> anyhow::Result<PathBuf> {
Ok(dir) Ok(dir)
} }
fn restore_dns_state_dir(resource_dir: &Path, state_dir: PathBuf) -> PathBuf {
if resource_dir.join(DNS_STATE_FILE).exists() || resource_dir.join(DNS_BACKUP_DIR).exists() {
resource_dir.to_path_buf()
} else {
state_dir
}
}
// Without the placeholder, a LAN resolver is reached outside TUN and fake-ip is bypassed. // Without the placeholder, a LAN resolver is reached outside TUN and fake-ip is bypassed.
fn needs_public_dns(config: &Mapping) -> bool { fn needs_public_dns(config: &Mapping) -> bool {
let section = |name: &str| config.get(name).and_then(Value::as_mapping); let section = |name: &str| config.get(name).and_then(Value::as_mapping);
@@ -69,19 +79,25 @@ pub async fn restore_public_dns() -> anyhow::Result<()> {
async fn set_public_dns() { async fn set_public_dns() {
logging!(debug, Type::Config, "try to set system dns"); logging!(debug, Type::Config, "try to set system dns");
let result = run_dns_script("set_dns.sh", true).await; let result = async {
let script = crate::utils::dirs::app_resources_dir()?.join("set_dns.sh");
run_dns_script(&script, dns_state_dir()?).await
}
.await;
match result { match result {
Ok(DnsScriptOutcome::Complete) => logging!(info, Type::Config, "set system dns successfully"), Ok(()) => logging!(info, Type::Config, "set system dns successfully"),
Ok(DnsScriptOutcome::Deferred) => {
logging!(debug, Type::Config, "system DNS waits for a primary network service")
}
Err(err) => logging!(error, Type::Config, "set system dns failed: {err:#}"), Err(err) => logging!(error, Type::Config, "set system dns failed: {err:#}"),
} }
} }
async fn unset_public_dns() -> anyhow::Result<()> { async fn unset_public_dns() -> anyhow::Result<()> {
logging!(debug, Type::Config, "try to unset system dns"); logging!(debug, Type::Config, "try to unset system dns");
let result = run_dns_script("unset_dns.sh", false).await.map(|_| ()); let result = async {
let resource_dir = crate::utils::dirs::app_resources_dir()?;
let state_dir = restore_dns_state_dir(&resource_dir, dns_state_dir()?);
run_dns_script(&resource_dir.join("unset_dns.sh"), state_dir).await
}
.await;
match &result { match &result {
Ok(()) => logging!(info, Type::Config, "unset system dns successfully"), Ok(()) => logging!(info, Type::Config, "unset system dns successfully"),
Err(err) => logging!(error, Type::Config, "unset system dns failed: {err:#}"), Err(err) => logging!(error, Type::Config, "unset system dns failed: {err:#}"),
@@ -89,21 +105,12 @@ async fn unset_public_dns() -> anyhow::Result<()> {
result result
} }
enum DnsScriptOutcome {
Complete,
Deferred,
}
/// On timeout the script's whole process group is killed, so no networksetup it started writes late. /// On timeout the script's whole process group is killed, so no networksetup it started writes late.
async fn run_dns_script(script_name: &str, allow_deferred: bool) -> anyhow::Result<DnsScriptOutcome> { async fn run_dns_script(script: &Path, dir: PathBuf) -> anyhow::Result<()> {
let resource_dir = crate::utils::dirs::app_resources_dir()?;
let script = resource_dir.join(script_name);
let dir = dns_state_dir()?;
ensure!(script.exists(), "DNS script not found: {}", script.display()); ensure!(script.exists(), "DNS script not found: {}", script.display());
let mut child = Command::new("bash") let mut child = Command::new("bash")
.arg(&script) .arg(script)
.arg(PLACEHOLDER_DNS) .arg(PLACEHOLDER_DNS)
.arg(resource_dir)
.current_dir(dir) .current_dir(dir)
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@@ -127,18 +134,41 @@ async fn run_dns_script(script_name: &str, allow_deferred: bool) -> anyhow::Resu
if let Some(mut pipe) = child.stderr.take() { if let Some(mut pipe) = child.stderr.take() {
pipe.read_to_string(&mut stderr)?; pipe.read_to_string(&mut stderr)?;
} }
// EX_TEMPFAIL is reserved by set_dns.sh for a network service that is not ready.
if allow_deferred && status.code() == Some(75) {
return Ok(DnsScriptOutcome::Deferred);
}
ensure!( ensure!(
status.success(), status.success(),
"exit code {}: {}", "exit code {}: {}",
status.code().unwrap_or(-1), status.code().unwrap_or(-1),
stderr.trim() stderr.trim()
); );
Ok(DnsScriptOutcome::Complete) Ok(())
}) })
.await .await
.context("DNS script task failed")? .context("DNS script task failed")?
} }
#[cfg(test)]
mod tests {
#[test]
#[allow(clippy::expect_used)]
fn restore_dns_state_dir_defaults_to_app_data_but_honors_legacy_file() {
use super::restore_dns_state_dir;
let root = std::env::temp_dir().join(format!("clash-verge-dns-{}", nanoid::nanoid!()));
let resource_dir = root.join("resources");
let state_dir = root.join("app-data");
std::fs::create_dir_all(&resource_dir).expect("create test resource directory");
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir.clone()), state_dir);
std::fs::write(resource_dir.join(".original_dns.txt"), "empty").expect("create legacy DNS state");
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir.clone()), resource_dir);
// A migrated legacy backup whose restore failed must still be retried there.
std::fs::remove_file(resource_dir.join(".original_dns.txt")).expect("remove legacy DNS state");
std::fs::create_dir(resource_dir.join(".original_dns")).expect("create migrated DNS state");
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir), resource_dir);
std::fs::remove_dir_all(root).expect("remove test directory");
}
}
+2 -2
View File
@@ -33,9 +33,9 @@
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json" "https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
], ],
"windows": { "windows": {
"installMode": "passive" "installMode": "passive"
+1 -2
View File
@@ -26,9 +26,8 @@
"active": true, "active": true,
"dialog": false, "dialog": false,
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json" "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
} }
+1 -2
View File
@@ -26,9 +26,8 @@
"active": true, "active": true,
"dialog": false, "dialog": false,
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json" "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
} }
+1 -2
View File
@@ -26,9 +26,8 @@
"active": true, "active": true,
"dialog": false, "dialog": false,
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json" "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
} }
@@ -464,6 +464,21 @@ export const ConnectionTable = (props: Props) => {
] ]
}, [t]) }, [t])
useEffect(() => {
setColumnOrder((prevValue) => {
const baseFields = baseColumns.map((col) => col.field)
const prev = Array.isArray(prevValue) ? prevValue : []
const reconciled = reconcileColumnOrder(prev, baseFields)
if (
reconciled.length === prev.length &&
reconciled.every((field, i) => field === prev[i])
) {
return prevValue
}
return reconciled
})
}, [baseColumns, setColumnOrder])
const orderedColumns = useMemo(() => { const orderedColumns = useMemo(() => {
const baseFields = baseColumns.map((column) => column.field) const baseFields = baseColumns.map((column) => column.field)
const reconciledOrder = reconcileColumnOrder(columnOrder, baseFields) const reconciledOrder = reconcileColumnOrder(columnOrder, baseFields)
+4 -37
View File
@@ -1,7 +1,6 @@
import { DeveloperBoardOutlined, RestartAltRounded } from '@mui/icons-material' import { DeveloperBoardOutlined } from '@mui/icons-material'
import { Divider, IconButton, Stack, Tooltip, Typography } from '@mui/material' import { Divider, Stack, Typography } from '@mui/material'
import { useLockFn } from 'ahooks' import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useClash } from '@/hooks/use-clash' import { useClash } from '@/hooks/use-clash'
@@ -12,8 +11,6 @@ import {
useRulesData, useRulesData,
useSystemData, useSystemData,
} from '@/providers/app-data-context' } from '@/providers/app-data-context'
import { restartCore } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { EnhancedCard } from './enhanced-card' import { EnhancedCard } from './enhanced-card'
@@ -33,23 +30,6 @@ export const ClashInfoCard = () => {
const { rules } = useRulesData() const { rules } = useRulesData()
const uptime = useAppUptime() const uptime = useAppUptime()
const { systemProxyAddress } = useSystemData() const { systemProxyAddress } = useSystemData()
const [restarting, setRestarting] = useState(false)
const onRestart = useLockFn(async () => {
setRestarting(true)
try {
await mutate(() => restartCore(), {
id: 'restart-core',
successNotice: t(
'settings.feedback.notifications.clash.restartSuccess',
),
})
} catch {
// the funnel already notified
} finally {
setRestarting(false)
}
})
// 使用useMemo缓存格式化后的uptime,避免频繁计算 // 使用useMemo缓存格式化后的uptime,避免频繁计算
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]) const formattedUptime = useMemo(() => formatUptime(uptime), [uptime])
@@ -121,20 +101,7 @@ export const ClashInfoCard = () => {
title={t('home.components.clashInfo.title')} title={t('home.components.clashInfo.title')}
icon={<DeveloperBoardOutlined />} icon={<DeveloperBoardOutlined />}
iconColor="warning" iconColor="warning"
action={ action={null}
<Tooltip title={t('shared.actions.restart')}>
<span>
<IconButton
size="small"
aria-label={t('shared.actions.restart')}
loading={restarting}
onClick={onRestart}
>
<RestartAltRounded />
</IconButton>
</span>
</Tooltip>
}
> >
{cardContent} {cardContent}
</EnhancedCard> </EnhancedCard>
+78 -65
View File
@@ -1,3 +1,4 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import { import {
AccessTimeRounded, AccessTimeRounded,
ArrowDropDown, ArrowDropDown,
@@ -30,15 +31,7 @@ import {
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
useCallback,
useEffect,
useEffectEvent,
useId,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
@@ -577,22 +570,20 @@ export const CurrentProxyCard = () => {
return profileValue return profileValue
} }
return profileKey !== baseKey ? localStorage.getItem(baseKey) : null if (profileKey !== baseKey) {
const legacyValue = localStorage.getItem(baseKey)
if (legacyValue != null) {
localStorage.removeItem(baseKey)
localStorage.setItem(profileKey, legacyValue)
return legacyValue
}
}
return null
}, },
[getProfileStorageKey], [getProfileStorageKey],
) )
// Hand the pre-profile-scoping group key to the first profile that shows the card.
useEffect(() => {
const profileKey = getProfileStorageKey(STORAGE_KEY_GROUP)
if (profileKey === STORAGE_KEY_GROUP) return
if (localStorage.getItem(profileKey) != null) return
const legacyValue = localStorage.getItem(STORAGE_KEY_GROUP)
if (legacyValue == null) return
localStorage.removeItem(STORAGE_KEY_GROUP)
localStorage.setItem(profileKey, legacyValue)
}, [getProfileStorageKey])
const writeProfileScopedItem = useCallback( const writeProfileScopedItem = useCallback(
(baseKey: string, value: string) => { (baseKey: string, value: string) => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
@@ -606,6 +597,9 @@ export const CurrentProxyCard = () => {
) )
const { select } = useProxySelection({ const { select } = useProxySelection({
onSuccess: () => {
refreshProxy()
},
onError: (error) => { onError: (error) => {
console.error('代理切换失败', error) console.error('代理切换失败', error)
refreshProxy() refreshProxy()
@@ -621,17 +615,25 @@ export const CurrentProxyCard = () => {
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0 return savedSortType ? (Number(savedSortType) as ProxySortType) : 0
}) })
const [lastGroupName, setLastGroupName] = useState('') const [selectedGroupName, setSelectedGroupName] = useState('')
const [openSelect, setOpenSelect] = useState<OpenSelect>(null) const [openSelect, setOpenSelect] = useState<OpenSelect>(null)
const [openSelectMode, setOpenSelectMode] = useState(mode)
if (openSelectMode !== mode) { useEffect(() => {
setOpenSelectMode(mode)
setOpenSelect(null) setOpenSelect(null)
} }, [mode])
const delayButtonRef = useRef<HTMLButtonElement>(null) const delayButtonRef = useRef<HTMLButtonElement>(null)
const delays = useGroupDelays(selectedGroupName || null)
const autoCheckInProgressRef = useRef(false) const autoCheckInProgressRef = useRef(false)
const latestTimeoutRef = useRef<number>(
verge?.default_latency_timeout || 10000,
)
const latestProxyMemberRef = useRef<ResolvedProxyMember | null>(null)
useEffect(() => {
latestTimeoutRef.current = verge?.default_latency_timeout || 10000
}, [verge?.default_latency_timeout])
const selectableGroups = useMemo(() => { const selectableGroups = useMemo(() => {
if (!proxyView) return [] if (!proxyView) return []
@@ -642,39 +644,6 @@ export const CurrentProxyCard = () => {
) )
}, [proxyView]) }, [proxyView])
const selectedGroupName = useMemo(() => {
if (isDirectMode) return 'DIRECT'
if (!proxyView) return lastGroupName
if (isGlobalMode) return proxyView.global?.name ?? 'GLOBAL'
const hasGroup = (name: string | null) =>
!!name && selectableGroups.some((group) => group.name === name)
if (hasGroup(lastGroupName)) return lastGroupName
const savedGroup = readProfileScopedItem(STORAGE_KEY_GROUP)
if (hasGroup(savedGroup)) return savedGroup!
const primaryKeywords = ['auto', 'select', 'proxy', '节点选择', '自动选择']
const primaryGroup =
selectableGroups.find((group) =>
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) ?? selectableGroups[0]
return primaryGroup?.name ?? ''
}, [
isDirectMode,
isGlobalMode,
lastGroupName,
proxyView,
readProfileScopedItem,
selectableGroups,
])
// Keep the shown group, auto picks included, until it leaves the profile.
if (proxyView && lastGroupName !== selectedGroupName) {
setLastGroupName(selectedGroupName)
}
const delays = useGroupDelays(selectedGroupName || null)
const selectedGroup = useMemo<ProxyGroupView | null>(() => { const selectedGroup = useMemo<ProxyGroupView | null>(() => {
if (!proxyView || isDirectMode) return null if (!proxyView || isDirectMode) return null
if (isGlobalMode) return proxyView.global if (isGlobalMode) return proxyView.global
@@ -711,6 +680,46 @@ export const CurrentProxyCard = () => {
[optionsForGroup, selectedGroup], [optionsForGroup, selectedGroup],
) )
useEffect(() => {
if (!proxyView) return
if (isDirectMode) {
setSelectedGroupName('DIRECT')
return
}
if (isGlobalMode) {
setSelectedGroupName(proxyView.global?.name ?? 'GLOBAL')
return
}
const savedGroup = readProfileScopedItem(STORAGE_KEY_GROUP)
const primaryKeywords = ['auto', 'select', 'proxy', '节点选择', '自动选择']
const primaryGroup =
selectableGroups.find((group) =>
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) ?? selectableGroups[0]
const nextGroup = selectableGroups.some(
({ name }) => name === selectedGroupName,
)
? selectedGroupName
: selectableGroups.some(({ name }) => name === savedGroup)
? savedGroup!
: (primaryGroup?.name ?? '')
if (nextGroup !== selectedGroupName) {
setSelectedGroupName(nextGroup)
if (nextGroup) writeProfileScopedItem(STORAGE_KEY_GROUP, nextGroup)
}
}, [
isDirectMode,
isGlobalMode,
proxyView,
readProfileScopedItem,
selectableGroups,
selectedGroupName,
writeProfileScopedItem,
])
const currentOption = useMemo(() => { const currentOption = useMemo(() => {
if (!proxyView) return undefined if (!proxyView) return undefined
if (isDirectMode) { if (isDirectMode) {
@@ -734,10 +743,12 @@ export const CurrentProxyCard = () => {
: undefined : undefined
}, [isDirectMode, proxyView, selectedGroup]) }, [isDirectMode, proxyView, selectedGroup])
latestProxyMemberRef.current = currentOption?.member ?? null
const handleGroupChange = useCallback( const handleGroupChange = useCallback(
(newGroupName: string) => { (newGroupName: string) => {
if (isGlobalMode || isDirectMode) return if (isGlobalMode || isDirectMode) return
setLastGroupName(newGroupName) setSelectedGroupName(newGroupName)
writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName) writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName)
}, },
[isDirectMode, isGlobalMode, writeProfileScopedItem], [isDirectMode, isGlobalMode, writeProfileScopedItem],
@@ -752,9 +763,10 @@ export const CurrentProxyCard = () => {
if (!selectedGroup || !option || !isInteractableMember(option.member)) { if (!selectedGroup || !option || !isInteractableMember(option.member)) {
return return
} }
const previousProxy = selectedGroup.now
const nextName = option.member.ref.name const nextName = option.member.ref.name
// The profile selection is the durable source across core restarts and run modes. // The profile selection is the durable source across core restarts and run modes.
select(selectedGroup.name, nextName, selectedGroup.fixed!) select(selectedGroup.name, nextName, previousProxy!, selectedGroup.fixed!)
}, },
[isDirectMode, select, selectedGroup, unsortedProxyOptions], [isDirectMode, select, selectedGroup, unsortedProxyOptions],
) )
@@ -781,7 +793,7 @@ export const CurrentProxyCard = () => {
color: 'text.secondary', color: 'text.secondary',
} }
const checkCurrentProxyDelay = useEffectEvent(async () => { const checkCurrentProxyDelay = useCallback(async () => {
if (autoCheckInProgressRef.current) return if (autoCheckInProgressRef.current) return
if (isDirectMode) return if (isDirectMode) return
@@ -790,7 +802,7 @@ export const CurrentProxyCard = () => {
if (!groupName || !proxyName) return if (!groupName || !proxyName) return
const proxyMember = currentOption?.member const proxyMember = latestProxyMemberRef.current
if (!proxyMember || !isInteractableMember(proxyMember)) { if (!proxyMember || !isInteractableMember(proxyMember)) {
debugLog( debugLog(
`[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`, `[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`,
@@ -800,7 +812,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = true autoCheckInProgressRef.current = true
const timeout = verge?.default_latency_timeout || 10000 const timeout = latestTimeoutRef.current || 10000
try { try {
debugLog( debugLog(
@@ -816,7 +828,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = false autoCheckInProgressRef.current = false
refreshProxy() refreshProxy()
} }
}) }, [isDirectMode, refreshProxy, selectedGroupName, selectedProxyName])
useEffect(() => { useEffect(() => {
if (isDirectMode) return if (isDirectMode) return
@@ -846,6 +858,7 @@ export const CurrentProxyCard = () => {
if (intervalTimer) clearTimeout(intervalTimer) if (intervalTimer) clearTimeout(intervalTimer)
} }
}, [ }, [
checkCurrentProxyDelay,
autoDelayIntervalMs, autoDelayIntervalMs,
isDirectMode, isDirectMode,
selectedGroupName, selectedGroupName,
@@ -4,7 +4,6 @@ import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useEffectEvent,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useReducer, useReducer,
@@ -121,6 +120,7 @@ export const EnhancedCanvasTrafficGraph = memo(
const drawFrameRef = useRef<number | undefined>(undefined) const drawFrameRef = useRef<number | undefined>(undefined)
const hoverFrameRef = useRef<number | undefined>(undefined) const hoverFrameRef = useRef<number | undefined>(undefined)
const mouseMoveFrameRef = useRef<number | undefined>(undefined) const mouseMoveFrameRef = useRef<number | undefined>(undefined)
const scheduleDrawGraphRef = useRef<() => void>(() => {})
const pendingMousePositionRef = useRef<{ const pendingMousePositionRef = useRef<{
clientX: number clientX: number
clientY: number clientY: number
@@ -189,11 +189,11 @@ export const EnhancedCanvasTrafficGraph = memo(
} }
}, [displayData]) }, [displayData])
const handleFocusStateChange = useEffectEvent((focused: boolean) => { const handleFocusStateChange = useCallback((focused: boolean) => {
isWindowFocusedRef.current = focused isWindowFocusedRef.current = focused
scheduleDrawGraph() scheduleDrawGraphRef.current()
}) }, [])
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined' || typeof document === 'undefined') { if (typeof window === 'undefined' || typeof document === 'undefined') {
@@ -217,7 +217,7 @@ export const EnhancedCanvasTrafficGraph = memo(
window.removeEventListener('blur', handleBlur) window.removeEventListener('blur', handleBlur)
document.removeEventListener('visibilitychange', handleVisibilityChange) document.removeEventListener('visibilitychange', handleVisibilityChange)
} }
}, []) }, [handleFocusStateChange])
const calculateY = useCallback( const calculateY = useCallback(
( (
@@ -906,21 +906,25 @@ export const EnhancedCanvasTrafficGraph = memo(
scheduleDrawGraph() scheduleDrawGraph()
}, [scheduleDrawGraph]) }, [scheduleDrawGraph])
const redrawGraph = useEffectEvent(() => scheduleDrawGraph()) useEffect(() => {
scheduleDrawGraphRef.current = scheduleDrawGraph
}, [scheduleDrawGraph])
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current const canvas = canvasRef.current
if (!canvas || typeof window === 'undefined') return if (!canvas || typeof window === 'undefined') return
if (typeof ResizeObserver === 'undefined') { if (typeof ResizeObserver === 'undefined') {
const handleResize = () => redrawGraph() const handleResize = () => scheduleDrawGraphRef.current()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
return () => { return () => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
} }
} }
const resizeObserver = new ResizeObserver(() => redrawGraph()) const resizeObserver = new ResizeObserver(() =>
scheduleDrawGraphRef.current(),
)
resizeObserver.observe(canvas) resizeObserver.observe(canvas)
return () => { return () => {
+20 -18
View File
@@ -8,7 +8,7 @@ import {
} from '@mui/icons-material' } from '@mui/icons-material'
import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material' import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
@@ -22,23 +22,10 @@ import {
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { getSystemInfo } from '@/services/cmds' import { getSystemInfo } from '@/services/cmds'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { version as appVersion } from '@root/package.json' import { version as appVersion } from '@root/package.json'
import { EnhancedCard } from './enhanced-card' import { EnhancedCard } from './enhanced-card'
const getOsInfo = async () => {
const info = await getSystemInfo()
const sysName = info.system_name
let sysVersion = info.system_version
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) {
sysVersion = sysVersion.substring(sysName.length).trim()
}
return `${sysName} ${sysVersion}`
}
export const SystemInfoCard = () => { export const SystemInfoCard = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
@@ -49,16 +36,31 @@ export const SystemInfoCard = () => {
const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true) const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true)
const { data: osInfo = '' } = useQuery({ const [osInfo, setOsInfo] = useState('')
queryKey: ['getSystemInfo'],
queryFn: getOsInfo,
})
const lastCheckUpdateText = useMemo( const lastCheckUpdateText = useMemo(
() => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'), () => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'),
[lastCheckUpdate], [lastCheckUpdate],
) )
useEffect(() => {
getSystemInfo()
.then((info) => {
const sysName = info.system_name
let sysVersion = info.system_version
if (
sysName &&
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
) {
sysVersion = sysVersion.substring(sysName.length).trim()
}
setOsInfo(`${sysName} ${sysVersion}`)
})
.catch(console.error)
}, [])
useEffect(() => { useEffect(() => {
if (!verge?.auto_check_update) return if (!verge?.auto_check_update) return
if (readLastCheckTime() !== null) return if (readLastCheckTime() !== null) return
+7 -5
View File
@@ -21,7 +21,6 @@ import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useEffectEvent,
useReducer, useReducer,
useRef, useRef,
useState, useState,
@@ -114,6 +113,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const setProfileLoading = useSetProfileLoading() const setProfileLoading = useSetProfileLoading()
const [showNextUpdate, setShowNextUpdate] = useState(false) const [showNextUpdate, setShowNextUpdate] = useState(false)
const showNextUpdateRef = useRef(false)
const [nextUpdateTime, setNextUpdateTime] = useState('') const [nextUpdateTime, setNextUpdateTime] = useState('')
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>( const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined, undefined,
@@ -203,6 +203,10 @@ const ProfileItemBase = (props: ProfileItemProps) => {
setShowNextUpdate(!showNextUpdate) setShowNextUpdate(!showNextUpdate)
} }
useEffect(() => {
showNextUpdateRef.current = showNextUpdate
}, [showNextUpdate])
useEffect(() => { useEffect(() => {
if (showNextUpdate) { if (showNextUpdate) {
fetchNextUpdateTime() fetchNextUpdateTime()
@@ -214,10 +218,8 @@ const ProfileItemBase = (props: ProfileItemProps) => {
updated, updated,
]) ])
const isNextUpdateShown = useEffectEvent(() => showNextUpdate)
useEffect(() => { useEffect(() => {
if (timerUpdateRevision === 0 || !isNextUpdateShown()) return if (timerUpdateRevision === 0 || !showNextUpdateRef.current) return
if (refreshTimeoutRef.current !== undefined) { if (refreshTimeoutRef.current !== undefined) {
clearTimeout(refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current)
@@ -234,7 +236,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
}, [fetchNextUpdateTime, timerUpdateRevision]) }, [fetchNextUpdateTime, timerUpdateRevision])
useEffect(() => { useEffect(() => {
if (completedUpdateRevision === 0 || !isNextUpdateShown()) return if (completedUpdateRevision === 0 || !showNextUpdateRef.current) return
fetchNextUpdateTime() fetchNextUpdateTime()
}, [completedUpdateRevision, fetchNextUpdateTime]) }, [completedUpdateRevision, fetchNextUpdateTime])
+14 -19
View File
@@ -10,7 +10,7 @@ import {
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import type { Ref } from 'react' import type { Ref } from 'react'
import { useImperativeHandle, useRef, useState } from 'react' import { useEffect, useImperativeHandle, useRef, useState } from 'react'
import { Controller, useForm } from 'react-hook-form' import { Controller, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -77,6 +77,17 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
}, },
})) }))
const selfProxy = watch('option.self_proxy')
const withProxy = watch('option.with_proxy')
useEffect(() => {
if (selfProxy) setValue('option.with_proxy', false)
}, [selfProxy, setValue])
useEffect(() => {
if (withProxy) setValue('option.self_proxy', false)
}, [setValue, withProxy])
const handleOk = useLockFn( const handleOk = useLockFn(
handleSubmit(async (form) => { handleSubmit(async (form) => {
setLoading(true) setLoading(true)
@@ -408,15 +419,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel> <InputLabel>
{t('profiles.modals.profileForm.fields.useSystemProxy')} {t('profiles.modals.profileForm.fields.useSystemProxy')}
</InputLabel> </InputLabel>
<Switch <Switch checked={field.value!} {...field} color="primary" />
checked={field.value!}
{...field}
onChange={(_, checked) => {
field.onChange(checked)
if (checked) setValue('option.self_proxy', false)
}}
color="primary"
/>
</StyledBox> </StyledBox>
)} )}
/> />
@@ -429,15 +432,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel> <InputLabel>
{t('profiles.modals.profileForm.fields.useClashProxy')} {t('profiles.modals.profileForm.fields.useClashProxy')}
</InputLabel> </InputLabel>
<Switch <Switch checked={field.value!} {...field} color="primary" />
checked={field.value!}
{...field}
onChange={(_, checked) => {
field.onChange(checked)
if (checked) setValue('option.with_proxy', false)
}}
color="primary"
/>
</StyledBox> </StyledBox>
)} )}
/> />
-30
View File
@@ -1,5 +1,3 @@
import * as yaml from 'js-yaml'
import { import {
getRecord, getRecord,
rebindNode, rebindNode,
@@ -36,31 +34,3 @@ export const rebindProxyChainItems = (
delay: record?.history.at(-1)?.delay, delay: record?.history.at(-1)?.delay,
} }
}) })
interface ParsedChainConfig {
proxies?: Array<{
name: string
type: string
[key: string]: any
}>
}
export const parseChainConfigItems = (configData: string): ProxyChainItem[] => {
try {
// JSON is valid YAML, so one parser covers both persisted formats.
const parsedConfig = yaml.load(configData) as ParsedChainConfig | null
const timestamp = Date.now()
return (
parsedConfig?.proxies?.map((proxy, index) => ({
id: `${proxy.name}_${timestamp}_${index}`,
name: proxy.name,
type: proxy.type,
delay: undefined,
})) || []
)
} catch (error) {
console.error('Failed to process chain config data:', error)
return []
}
}
+43
View File
@@ -25,6 +25,7 @@ import {
keyframes, keyframes,
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
import * as yaml from 'js-yaml'
import { import {
type Ref, type Ref,
useCallback, useCallback,
@@ -59,9 +60,18 @@ const chainPointerSensor = PointerSensor.configure({
type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown } type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown }
interface ParsedChainConfig {
proxies?: Array<{
name: string
type: string
[key: string]: any
}>
}
interface ProxyChainProps { interface ProxyChainProps {
proxyChain: ProxyChainItem[] proxyChain: ProxyChainItem[]
onUpdateChain: (chain: ProxyChainItem[]) => void onUpdateChain: (chain: ProxyChainItem[]) => void
chainConfigData?: string | null
onMarkUnsavedChanges?: () => void onMarkUnsavedChanges?: () => void
mode?: string mode?: string
selectedGroup?: string | null selectedGroup?: string | null
@@ -88,6 +98,21 @@ const arrowFadeIn = keyframes`
} }
` `
const toChainItems = (
parsedConfig: ParsedChainConfig | null | undefined,
): ProxyChainItem[] => {
const timestamp = Date.now()
return (
parsedConfig?.proxies?.map((proxy, index) => ({
id: `${proxy.name}_${timestamp}_${index}`,
name: proxy.name,
type: proxy.type,
delay: undefined,
})) || []
)
}
interface ChainCardProps { interface ChainCardProps {
proxy: ProxyChainItem proxy: ProxyChainItem
index: number index: number
@@ -303,6 +328,7 @@ const SortableProxyChainItem = ({
export const ProxyChain = ({ export const ProxyChain = ({
proxyChain, proxyChain,
onUpdateChain, onUpdateChain,
chainConfigData,
onMarkUnsavedChanges, onMarkUnsavedChanges,
mode, mode,
selectedGroup, selectedGroup,
@@ -554,6 +580,23 @@ export const ProxyChain = ({
recordSelection, recordSelection,
]) ])
// 处理链式代理配置数据
useEffect(() => {
if (chainConfigData) {
try {
// JSON is valid YAML, so one parser covers both persisted formats.
const parsedConfig = yaml.load(chainConfigData) as ParsedChainConfig
const chainItems = toChainItems(parsedConfig)
if (chainItems.length > 0) {
onUpdateChain(chainItems)
}
} catch (error) {
console.error('Failed to process chain config data:', error)
}
}
}, [chainConfigData, onUpdateChain])
return ( return (
<Paper <Paper
elevation={1} elevation={1}
+3 -12
View File
@@ -76,7 +76,7 @@ interface GroupSelectMenuProps {
interface ProxyGroupsChainProps { interface ProxyGroupsChainProps {
mode: string mode: string
chainConfigItems?: ProxyChainItem[] | null chainConfigData?: string | null
onExitChainMode: () => void onExitChainMode: () => void
availableGroups: any[] availableGroups: any[]
activeSelectedGroup: string | null activeSelectedGroup: string | null
@@ -305,7 +305,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
mode, mode,
chainConfigItems, chainConfigData,
onExitChainMode, onExitChainMode,
availableGroups, availableGroups,
activeSelectedGroup, activeSelectedGroup,
@@ -338,16 +338,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
return [] return []
}) })
// Each fetched runtime chain replaces the stored one once.
const configItems = chainConfigItems ?? null
const [appliedConfigItems, setAppliedConfigItems] = useState<
ProxyChainItem[] | null
>(null)
if (configItems !== appliedConfigItems) {
setAppliedConfigItems(configItems)
if (configItems?.length) setProxyChain(configItems)
}
const candidateNodes = useMemo( const candidateNodes = useMemo(
() => () =>
renderList.flatMap((item) => { renderList.flatMap((item) => {
@@ -530,6 +520,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
<ProxyChain <ProxyChain
proxyChain={currentProxyChain} proxyChain={currentProxyChain}
onUpdateChain={setProxyChain} onUpdateChain={setProxyChain}
chainConfigData={chainConfigData}
mode={mode} mode={mode}
selectedGroup={activeSelectedGroup} selectedGroup={activeSelectedGroup}
onExitChainMode={onExitChainMode} onExitChainMode={onExitChainMode}
+12 -35
View File
@@ -32,7 +32,6 @@ import {
} from '@/types/proxy-view' } from '@/types/proxy-view'
import { debugLog } from '@/utils/debug' import { debugLog } from '@/utils/debug'
import type { ProxyChainItem } from './proxy-chain-model'
import { ProxyEmptyState } from './proxy-empty-state' import { ProxyEmptyState } from './proxy-empty-state'
import { import {
resolveEmptyListReason, resolveEmptyListReason,
@@ -69,7 +68,7 @@ function useStableCallback<T extends (...args: any[]) => any>(fn: T): T {
interface Props { interface Props {
mode: string mode: string
isChainMode?: boolean isChainMode?: boolean
chainConfigItems?: ProxyChainItem[] | null chainConfigData?: string | null
onExitChainMode: () => void onExitChainMode: () => void
} }
@@ -184,7 +183,6 @@ function useProxyRenderState(
renderList, renderList,
onProxies, onProxies,
onHeadState, onHeadState,
setHeadState: setListHeadState,
handleCheckAll, handleCheckAll,
saveScrollPosition, saveScrollPosition,
getScrollPosition, getScrollPosition,
@@ -193,10 +191,10 @@ function useProxyRenderState(
function ChainProxyGroups(props: { function ChainProxyGroups(props: {
mode: string mode: string
chainConfigItems?: ProxyChainItem[] | null chainConfigData?: string | null
onExitChainMode: () => void onExitChainMode: () => void
}) { }) {
const { mode, chainConfigItems, onExitChainMode } = props const { mode, chainConfigData, onExitChainMode } = props
const { proxyView } = useProxiesData() const { proxyView } = useProxiesData()
const [selectedGroup, setSelectedGroup] = useState<string | null>(null) const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
@@ -350,7 +348,7 @@ function ChainProxyGroups(props: {
<Suspense fallback={<BaseLoading />}> <Suspense fallback={<BaseLoading />}>
<ProxyGroupsChain <ProxyGroupsChain
mode={mode} mode={mode}
chainConfigItems={chainConfigItems} chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode} onExitChainMode={onExitChainMode}
availableGroups={availableGroups} availableGroups={availableGroups}
activeSelectedGroup={activeSelectedGroup} activeSelectedGroup={activeSelectedGroup}
@@ -379,17 +377,12 @@ function NormalProxyGroups(props: { mode: string }) {
renderList, renderList,
onProxies, onProxies,
onHeadState, onHeadState,
setHeadState,
handleCheckAll, handleCheckAll,
getScrollPosition, getScrollPosition,
saveScrollPosition, saveScrollPosition,
} = useProxyRenderState(mode, false, null) } = useProxyRenderState(mode, false, null)
const emptyList = useEmptyRenderList() const emptyList = useEmptyRenderList()
const { onDragEnd: onHeaderDragEnd } = useProxyGroupHeaderLayout() const { onDragEnd: onHeaderDragEnd } = useProxyGroupHeaderLayout()
const { proxyView } = useProxiesData()
const globalGroupName = proxyView?.global?.name
// The revealed group sits below GLOBAL's rows, so a click must bring it into view.
const pendingRevealRef = useRef<string | null>(null)
const renderFirstRef = useRef(true) const renderFirstRef = useRef(true)
// Do not persist intermediate positions produced while restoring virtual scroll. // Do not persist intermediate positions produced while restoring virtual scroll.
const isRestoringRef = useRef(false) const isRestoringRef = useRef(false)
@@ -470,6 +463,9 @@ function NormalProxyGroups(props: { mode: string }) {
}, [handleScroll]) }, [handleScroll])
const { select } = useProxySelection({ const { select } = useProxySelection({
onSuccess: () => {
onProxies()
},
onError: (error) => { onError: (error) => {
console.error('代理切换失败', error) console.error('代理切换失败', error)
onProxies() onProxies()
@@ -481,17 +477,9 @@ function NormalProxyGroups(props: { mode: string }) {
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
if (!isInteractableMember(member)) return if (!isInteractableMember(member)) return
if (group.name === globalGroupName) { select(group.name, member.ref.name, group.now!, group.fixed!)
pendingRevealRef.current =
member.kind === 'group' ? member.ref.name : null
// The backend selection event refreshes the view.
if (member.kind === 'group') {
setHeadState(member.ref.name, { open: true })
}
}
select(group.name, member.ref.name, group.fixed!)
}, },
[globalGroupName, select, setHeadState], [select],
) )
const handleLocation = useStableCallback((group: ProxyGroupView) => { const handleLocation = useStableCallback((group: ProxyGroupView) => {
@@ -537,17 +525,6 @@ function NormalProxyGroups(props: { mode: string }) {
return Array.from(new Set(names)) return Array.from(new Set(names))
}, [renderList]) }, [renderList])
useEffect(() => {
if (mode !== 'global') {
pendingRevealRef.current = null
return
}
const groupName = pendingRevealRef.current
if (!groupName || !proxyGroupNames.includes(groupName)) return
pendingRevealRef.current = null
handleGroupLocationByName(groupName)
}, [handleGroupLocationByName, mode, proxyGroupNames])
const handleGroupToggle = useCallback( const handleGroupToggle = useCallback(
async (group: ProxyGroupView) => { async (group: ProxyGroupView) => {
const index = renderList.findIndex( const index = renderList.findIndex(
@@ -631,7 +608,7 @@ function NormalProxyGroups(props: { mode: string }) {
/> />
</DragDropProvider> </DragDropProvider>
{mode === 'rule' && ( {(mode === 'rule' || mode === 'global') && (
<ProxyGroupNavigator <ProxyGroupNavigator
proxyGroupNames={proxyGroupNames} proxyGroupNames={proxyGroupNames}
onGroupLocation={handleGroupLocationByName} onGroupLocation={handleGroupLocationByName}
@@ -644,7 +621,7 @@ function NormalProxyGroups(props: { mode: string }) {
} }
export const ProxyGroups = (props: Props) => { export const ProxyGroups = (props: Props) => {
const { mode, isChainMode = false, chainConfigItems, onExitChainMode } = props const { mode, isChainMode = false, chainConfigData, onExitChainMode } = props
const { profiles, isLoading: isProfilesLoading } = useProfiles() const { profiles, isLoading: isProfilesLoading } = useProfiles()
const { isProxyViewPending } = useProxiesData() const { isProxyViewPending } = useProxiesData()
const { isRunningModePending } = useSystemData() const { isRunningModePending } = useSystemData()
@@ -668,7 +645,7 @@ export const ProxyGroups = (props: Props) => {
return isChainMode ? ( return isChainMode ? (
<ChainProxyGroups <ChainProxyGroups
mode={mode} mode={mode}
chainConfigItems={chainConfigItems} chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode} onExitChainMode={onExitChainMode}
/> />
) : ( ) : (
+18 -19
View File
@@ -6,7 +6,6 @@ import { useVerge } from '@/hooks/use-verge'
import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context' import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context'
import delayManager, { type DelaySnapshot } from '@/services/delay' import delayManager, { type DelaySnapshot } from '@/services/delay'
import { import {
findCurrentGroupMember,
isInteractableMember, isInteractableMember,
resolveMember, resolveMember,
selectGlobalChainNodes, selectGlobalChainNodes,
@@ -210,10 +209,8 @@ export const useRenderList = (
const renderGroups = useMemo(() => { const renderGroups = useMemo(() => {
if (!proxyView) return [] if (!proxyView) return []
if (mode === 'rule') return proxyView.groups if (mode === 'rule') return proxyView.groups
const { global } = proxyView const global = proxyView.global ? [proxyView.global] : []
if (!global) return [] return mode === 'global' ? [...global, ...proxyView.groups] : global
const current = findCurrentGroupMember(proxyView, global)?.member
return current?.kind === 'group' ? [global, current.group] : [global]
}, [mode, proxyView]) }, [mode, proxyView])
// Every group this list draws, so a test settling in any of them re-sorts that group. // Every group this list draws, so a test settling in any of them re-sorts that group.
@@ -263,10 +260,10 @@ export const useRenderList = (
})) }))
} }
const useRule = mode === 'rule' || mode === 'global'
const cache = groupCacheRef.current const cache = groupCacheRef.current
const retList = renderGroups.flatMap((group) => { const retList = renderGroups.flatMap((group) => {
const isFlat = group === proxyView.global
const headState = headStates[group.name] || DEFAULT_STATE const headState = headStates[group.name] || DEFAULT_STATE
const cached = cache.get(group.name) const cached = cache.get(group.name)
if ( if (
@@ -282,19 +279,17 @@ export const useRenderList = (
} }
const ret: IRenderItem[] = [ const ret: IRenderItem[] = [
isFlat {
? { type: 1, key: `head-${group.name}`, group, headState } type: 0,
: { key: group.name,
type: 0, group,
key: group.name, headState,
group, icon: group.icon!,
headState, testUrl: group.testUrl!,
icon: group.icon!, },
testUrl: group.testUrl!,
},
] ]
if (headState.open || isFlat) { if (headState.open || !useRule) {
const occurrences = filterSort( const occurrences = filterSort(
resolveOccurrences(proxyView, group), resolveOccurrences(proxyView, group),
group.name, group.name,
@@ -307,6 +302,9 @@ export const useRenderList = (
useRegularExpression: headState.filterUseRegularExpression, useRegularExpression: headState.filterUseRegularExpression,
}, },
) )
if (!useRule) {
ret.push({ type: 1, key: `head-${group.name}`, group, headState })
}
if (occurrences.length === 0) { if (occurrences.length === 0) {
ret.push({ type: 3, key: `empty-${group.name}`, group, headState }) ret.push({ type: 3, key: `empty-${group.name}`, group, headState })
} else if (col > 1) { } else if (col > 1) {
@@ -345,8 +343,9 @@ export const useRenderList = (
return ret return ret
}) })
const filtered = const filtered = !useRule
mode === 'rule' ? retList.filter((item) => !item.group.hidden) : retList ? retList.slice(1)
: retList.filter((item) => !item.group.hidden)
if ( if (
prevListRef.current.length === filtered.length && prevListRef.current.length === filtered.length &&
filtered.every((item, index) => item === prevListRef.current[index]) filtered.every((item, index) => item === prevListRef.current[index])
@@ -133,11 +133,7 @@ export function AutoBackupSettings() {
return ( return (
<Fragment> <Fragment>
<ListItem <ListItem divider disableGutters>
data-setting-target="settings.modals.backup.auto.scheduleLabel"
divider
disableGutters
>
<Stack <Stack
direction="row" direction="row"
spacing={1} spacing={1}
@@ -156,11 +152,7 @@ export function AutoBackupSettings() {
</Stack> </Stack>
</ListItem> </ListItem>
<ListItem <ListItem divider disableGutters>
data-setting-target="settings.modals.backup.auto.intervalLabel"
divider
disableGutters
>
<Stack <Stack
direction="row" direction="row"
spacing={2} spacing={2}
@@ -202,11 +194,7 @@ export function AutoBackupSettings() {
</Stack> </Stack>
</ListItem> </ListItem>
<ListItem <ListItem divider disableGutters>
data-setting-target="settings.modals.backup.auto.changeLabel"
divider
disableGutters
>
<Stack <Stack
direction="row" direction="row"
spacing={1} spacing={1}
@@ -173,7 +173,6 @@ export const BackupConfigViewer = memo(
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<TextField <TextField
data-setting-target="settings.modals.backup.fields.webdavUrl"
fullWidth fullWidth
label={t('settings.modals.backup.fields.webdavUrl')} label={t('settings.modals.backup.fields.webdavUrl')}
variant="outlined" variant="outlined"
@@ -188,7 +187,6 @@ export const BackupConfigViewer = memo(
</Grid> </Grid>
<Grid size={{ xs: 6 }}> <Grid size={{ xs: 6 }}>
<TextField <TextField
data-setting-target="settings.modals.backup.fields.username"
label={t('settings.modals.backup.fields.username')} label={t('settings.modals.backup.fields.username')}
variant="outlined" variant="outlined"
size="small" size="small"
@@ -201,7 +199,6 @@ export const BackupConfigViewer = memo(
</Grid> </Grid>
<Grid size={{ xs: 6 }}> <Grid size={{ xs: 6 }}>
<TextField <TextField
data-setting-target="webdav-password"
label={t('shared.labels.password')} label={t('shared.labels.password')}
type="password" type="password"
variant="outlined" variant="outlined"
@@ -233,7 +230,6 @@ export const BackupConfigViewer = memo(
color={'primary'} color={'primary'}
sx={{ height: '100%' }} sx={{ height: '100%' }}
type="button" type="button"
data-setting-target="webdav-save"
onClick={handleSubmit(save)} onClick={handleSubmit(save)}
> >
{t('shared.actions.save')} {t('shared.actions.save')}
@@ -243,7 +239,6 @@ export const BackupConfigViewer = memo(
<Button <Button
variant="contained" variant="contained"
color="success" color="success"
data-setting-target="webdav-backup"
onClick={handleBackup} onClick={handleBackup}
type="button" type="button"
size="large" size="large"
@@ -252,7 +247,6 @@ export const BackupConfigViewer = memo(
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
data-setting-target="webdav-refresh"
onClick={onRefresh} onClick={onRefresh}
type="button" type="button"
size="large" size="large"
@@ -146,12 +146,7 @@ export const BackupHistoryViewer = ({
setRows([]) setRows([])
return return
} }
// Read the status here: as a dependency, the write below re-runs the fetch. if (!isLocal && webdavStatus === 'failed' && !options?.force) {
if (
!isLocal &&
getWebdavStatus(webdavSignature) === 'failed' &&
!options?.force
) {
setRows([]) setRows([])
return return
} }
@@ -185,7 +180,7 @@ export const BackupHistoryViewer = ({
setLoading(false) setLoading(false)
} }
}, },
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature], [buildRow, isLocal, open, shouldSkipWebDav, webdavSignature, webdavStatus],
) )
useEffect(() => { useEffect(() => {
@@ -327,13 +322,7 @@ export const BackupHistoryViewer = ({
onCancel={onClose} onCancel={onClose}
onClose={onClose} onClose={onClose}
> >
<Box <Box sx={{ position: 'relative', minHeight: 320 }}>
data-settings-dialog="backup-history"
data-search-ready={!loading}
data-setting-target="backup-history"
tabIndex={-1}
sx={{ position: 'relative', minHeight: 320 }}
>
<BaseLoadingOverlay isLoading={isBusy} /> <BaseLoadingOverlay isLoading={isBusy} />
<Stack spacing={2}> <Stack spacing={2}>
<Stack <Stack
@@ -418,7 +407,6 @@ export const BackupHistoryViewer = ({
<IconButton <IconButton
size="small" size="small"
disabled={isBusy} disabled={isBusy}
data-setting-target="backup-history-export"
onClick={() => handleExport(row.filename)} onClick={() => handleExport(row.filename)}
> >
<DownloadRounded fontSize="small" /> <DownloadRounded fontSize="small" />
@@ -427,7 +415,6 @@ export const BackupHistoryViewer = ({
<IconButton <IconButton
size="small" size="small"
disabled={isBusy} disabled={isBusy}
data-setting-target="backup-history-delete"
onClick={() => handleDelete(row.filename)} onClick={() => handleDelete(row.filename)}
> >
<DeleteOutlined fontSize="small" /> <DeleteOutlined fontSize="small" />
@@ -435,7 +422,6 @@ export const BackupHistoryViewer = ({
<IconButton <IconButton
size="small" size="small"
disabled={isBusy} disabled={isBusy}
data-setting-target="backup-history-restore"
onClick={() => handleRestore(row.filename)} onClick={() => handleRestore(row.filename)}
> >
<RestoreRounded fontSize="small" /> <RestoreRounded fontSize="small" />
+4 -36
View File
@@ -9,7 +9,7 @@ import {
import { open as openDialog } from '@tauri-apps/plugin-dialog' import { open as openDialog } from '@tauri-apps/plugin-dialog'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import type { ReactNode, Ref } from 'react' import type { ReactNode, Ref } from 'react'
import { useCallback, useRef, useState } from 'react' import { useCallback, useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef } from '@/components/base' import { BaseDialog, DialogRef } from '@/components/base'
@@ -23,8 +23,6 @@ import { mutate } from '@/services/mutate'
import { errorDetail, showNotice } from '@/services/notice-service' import { errorDetail, showNotice } from '@/services/notice-service'
import { buildWebdavSignature, setWebdavStatus } from '@/services/webdav-status' import { buildWebdavSignature, setWebdavStatus } from '@/services/webdav-status'
import { useSettingDialog } from '../settings-search-context'
import { AutoBackupSettings } from './auto-backup-settings' import { AutoBackupSettings } from './auto-backup-settings'
import { BackupHistoryViewer } from './backup-history-viewer' import { BackupHistoryViewer } from './backup-history-viewer'
import { BackupWebdavDialog } from './backup-webdav-dialog' import { BackupWebdavDialog } from './backup-webdav-dialog'
@@ -41,18 +39,9 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [historySource, setHistorySource] = useState<BackupSource>('local') const [historySource, setHistorySource] = useState<BackupSource>('local')
const [historyPage, setHistoryPage] = useState(0) const [historyPage, setHistoryPage] = useState(0)
const [webdavDialogOpen, setWebdavDialogOpen] = useState(false) const [webdavDialogOpen, setWebdavDialogOpen] = useState(false)
const searchParentRef = useRef<'webdav' | 'history' | null>(null)
const closeSearchDetail = (detail: 'webdav' | 'history') => {
if (detail === 'webdav') setWebdavDialogOpen(false)
else setHistoryOpen(false)
if (searchParentRef.current === detail) {
searchParentRef.current = null
setOpen(false)
}
}
const webdavSignature = buildWebdavSignature(verge) const webdavSignature = buildWebdavSignature(verge)
useSettingDialog(ref, 'backup-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => setOpen(true), open: () => setOpen(true),
close: () => setOpen(false), close: () => setOpen(false),
})) }))
@@ -63,23 +52,6 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
setHistoryOpen(true) setHistoryOpen(true)
} }
useSettingDialog(undefined, 'backup-webdav', webdavDialogOpen, () => ({
open: () => {
if (!open) searchParentRef.current = 'webdav'
setOpen(true)
setWebdavDialogOpen(true)
},
close: () => setWebdavDialogOpen(false),
}))
useSettingDialog(undefined, 'backup-history', historyOpen, () => ({
open: () => {
if (!open) searchParentRef.current = 'history'
setOpen(true)
openHistory('local')
},
close: () => setHistoryOpen(false),
}))
const handleBackup = useLockFn(async (target: BackupSource) => { const handleBackup = useLockFn(async (target: BackupSource) => {
try { try {
setBusyAction(target) setBusyAction(target)
@@ -157,7 +129,6 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
> >
<span data-settings-dialog="backup-viewer" />
<Stack spacing={2}> <Stack spacing={2}>
<Stack <Stack
spacing={1} spacing={1}
@@ -195,7 +166,6 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
description: t('settings.modals.backup.manual.local'), description: t('settings.modals.backup.manual.local'),
actions: [ actions: [
<Button <Button
data-setting-target="settings.modals.backup.actions.backup"
key="backup" key="backup"
variant="contained" variant="contained"
size="small" size="small"
@@ -206,7 +176,6 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.backup.actions.backup')} {t('settings.modals.backup.actions.backup')}
</Button>, </Button>,
<Button <Button
data-setting-target="settings.modals.backup.actions.viewHistory"
key="history" key="history"
variant="outlined" variant="outlined"
size="small" size="small"
@@ -216,7 +185,6 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.backup.actions.viewHistory')} {t('settings.modals.backup.actions.viewHistory')}
</Button>, </Button>,
<Button <Button
data-setting-target="settings.modals.backup.actions.importBackup"
key="import" key="import"
variant="text" variant="text"
size="small" size="small"
@@ -295,11 +263,11 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
page={historyPage} page={historyPage}
onSourceChange={setHistorySource} onSourceChange={setHistorySource}
onPageChange={setHistoryPage} onPageChange={setHistoryPage}
onClose={() => closeSearchDetail('history')} onClose={() => setHistoryOpen(false)}
/> />
<BackupWebdavDialog <BackupWebdavDialog
open={webdavDialogOpen} open={webdavDialogOpen}
onClose={() => closeSearchDetail('webdav')} onClose={() => setWebdavDialogOpen(false)}
onBackupSuccess={() => openHistory('webdav')} onBackupSuccess={() => openHistory('webdav')}
setBusy={setWebdavBusy} setBusy={setWebdavBusy}
/> />
@@ -76,13 +76,7 @@ export const BackupWebdavDialog = ({
onCancel={onClose} onCancel={onClose}
onClose={onClose} onClose={onClose}
> >
<Box <Box sx={{ position: 'relative' }}>
data-settings-dialog="backup-webdav"
data-search-ready={!loading}
data-setting-target="webdav-state"
tabIndex={-1}
sx={{ position: 'relative' }}
>
<BaseLoadingOverlay isLoading={loading} /> <BaseLoadingOverlay isLoading={loading} />
<BackupConfigViewer <BackupConfigViewer
setLoading={handleLoading} setLoading={handleLoading}
@@ -13,7 +13,7 @@ import {
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import type { Ref } from 'react' import type { Ref } from 'react'
import { useState } from 'react' import { useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { closeAllConnections } from 'tauri-plugin-mihomo-api' import { closeAllConnections } from 'tauri-plugin-mihomo-api'
@@ -24,8 +24,6 @@ import { changeClashCore, restartCore, upgradeClashCore } from '@/services/cmds'
import { mutate } from '@/services/mutate' import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
const VALID_CORE = [ const VALID_CORE = [
{ {
name: 'Mihomo', name: 'Mihomo',
@@ -51,7 +49,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [restarting, setRestarting] = useState(false) const [restarting, setRestarting] = useState(false)
const [changingCore, setChangingCore] = useState<string | null>(null) const [changingCore, setChangingCore] = useState<string | null>(null)
useSettingDialog(ref, 'clash-core-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => setOpen(true), open: () => setOpen(true),
close: () => setOpen(false), close: () => setOpen(false),
})) }))
@@ -141,7 +139,6 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
loading={upgrading} loading={upgrading}
disabled={restarting || changingCore !== null} disabled={restarting || changingCore !== null}
sx={{ marginRight: '8px' }} sx={{ marginRight: '8px' }}
data-setting-target="core-upgrade"
onClick={onUpgrade} onClick={onUpgrade}
> >
{t('shared.actions.upgrade')} {t('shared.actions.upgrade')}
@@ -153,7 +150,6 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
loadingPosition="start" loadingPosition="start"
loading={restarting} loading={restarting}
disabled={upgrading} disabled={upgrading}
data-setting-target="core-restart"
onClick={onRestart} onClick={onRestart}
> >
{t('shared.actions.restart')} {t('shared.actions.restart')}
@@ -174,12 +170,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
> >
<List <List component="nav">
component="nav"
data-settings-dialog="clash-core-viewer"
data-setting-target="core-variants"
tabIndex={-1}
>
{VALID_CORE.map((each) => ( {VALID_CORE.map((each) => (
<ListItemButton <ListItemButton
key={each.core} key={each.core}
@@ -9,7 +9,7 @@ import {
TextField, TextField,
} from '@mui/material' } from '@mui/material'
import { useLockFn, useRequest } from 'ahooks' import { useLockFn, useRequest } from 'ahooks'
import { forwardRef, useState } from 'react' import { forwardRef, useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, Switch } from '@/components/base' import { BaseDialog, Switch } from '@/components/base'
@@ -22,8 +22,6 @@ import { showNotice } from '@/services/notice-service'
import type { TranslationKey } from '@/types/generated/i18n-keys' import type { TranslationKey } from '@/types/generated/i18n-keys'
import getSystem from '@/utils/get-system' import getSystem from '@/utils/get-system'
import { useSettingDialog } from '../settings-search-context'
const OS = getSystem() const OS = getSystem()
interface ClashPortViewerRef { interface ClashPortViewerRef {
@@ -89,7 +87,6 @@ const readPortSettings = (
}) })
interface PortRowProps { interface PortRowProps {
searchTarget: string
label: string label: string
secondary?: string secondary?: string
port: number port: number
@@ -100,7 +97,6 @@ interface PortRowProps {
} }
const PortRow = ({ const PortRow = ({
searchTarget,
label, label,
secondary, secondary,
port, port,
@@ -111,10 +107,7 @@ const PortRow = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<ListItem <ListItem sx={{ padding: '4px 0', minHeight: 36 }}>
data-setting-target={searchTarget}
sx={{ padding: '4px 0', minHeight: 36 }}
>
<ListItemText <ListItemText
primary={label} primary={label}
secondary={secondary} secondary={secondary}
@@ -136,7 +129,6 @@ const PortRow = ({
/> />
<IconButton <IconButton
size="small" size="small"
data-setting-target="port-random"
onClick={() => onPortChange(generateRandomPort())} onClick={() => onPortChange(generateRandomPort())}
title={t('settings.modals.clashPort.actions.random')} title={t('settings.modals.clashPort.actions.random')}
disabled={!enabled} disabled={!enabled}
@@ -200,7 +192,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
}, },
) )
useSettingDialog(ref, 'clash-port-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setPorts(readPortSettings(verge, configuredMixedPort)) setPorts(readPortSettings(verge, configuredMixedPort))
setOpen(true) setOpen(true)
@@ -253,11 +245,9 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<span data-settings-dialog="clash-port-viewer" />
<List sx={{ width: '100%' }}> <List sx={{ width: '100%' }}>
<PortRow <PortRow
label={t('settings.modals.clashPort.fields.mixed')} label={t('settings.modals.clashPort.fields.mixed')}
searchTarget="settings.modals.clashPort.fields.mixed"
secondary={ secondary={
displayedMixedPort !== configuredMixedPort displayedMixedPort !== configuredMixedPort
? t('settings.modals.clashPort.messages.runningPort', { ? t('settings.modals.clashPort.messages.runningPort', {
@@ -275,7 +265,6 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<PortRow <PortRow
key={key} key={key}
label={t(label)} label={t(label)}
searchTarget={label}
port={ports[key].port} port={ports[key].port}
enabled={ports[key].enabled} enabled={ports[key].enabled}
onPortChange={(port) => updateListener(key, { port })} onPortChange={(port) => updateListener(key, { port })}
@@ -12,7 +12,7 @@ import {
Tooltip, Tooltip,
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { useState, type Ref } from 'react' import { useImperativeHandle, useState, type Ref } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch } from '@/components/base' import { BaseDialog, DialogRef, Switch } from '@/components/base'
@@ -20,8 +20,6 @@ import { useClashInfo } from '@/hooks/use-clash'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) { export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@@ -37,7 +35,7 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
) )
// 对话框打开时初始化配置 // 对话框打开时初始化配置
useSettingDialog(ref, 'controller-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: async () => { open: async () => {
setOpen(true) setOpen(true)
setController(clashInfo?.server || '') setController(clashInfo?.server || '')
@@ -126,10 +124,8 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<span data-settings-dialog="controller-viewer" />
<List> <List>
<ListItem <ListItem
data-setting-target="settings.sections.externalController.fields.enable"
sx={{ sx={{
padding: '5px 2px', padding: '5px 2px',
display: 'flex', display: 'flex',
@@ -148,7 +144,6 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem> </ListItem>
<ListItem <ListItem
data-setting-target="settings.sections.externalController.fields.address"
sx={{ sx={{
padding: '5px 2px', padding: '5px 2px',
display: 'flex', display: 'flex',
@@ -189,7 +184,6 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem> </ListItem>
<ListItem <ListItem
data-setting-target="settings.sections.externalController.fields.secret"
sx={{ sx={{
padding: '5px 2px', padding: '5px 2px',
display: 'flex', display: 'flex',
+211 -201
View File
@@ -16,7 +16,14 @@ import {
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import * as yaml from 'js-yaml' import * as yaml from 'js-yaml'
import type { Ref } from 'react' import type { Ref } from 'react'
import { useCallback, useEffect, useReducer, useRef, useState } from 'react' import {
useCallback,
useEffect,
useImperativeHandle,
useReducer,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
@@ -40,8 +47,6 @@ import { useThemeMode } from '@/services/states'
import type { MonacoEditorInstance } from '@/types/monaco' import type { MonacoEditorInstance } from '@/types/monaco'
import { MONACO_FONT_FAMILY } from '@/utils/font-family' import { MONACO_FONT_FAMILY } from '@/utils/font-family'
import { useSettingDialog } from '../settings-search-context'
const Item = styled(ListItem)(() => ({ const Item = styled(ListItem)(() => ({
padding: '5px 2px', padding: '5px 2px',
'& textarea': { '& textarea': {
@@ -185,110 +190,6 @@ const DEFAULT_DNS_CONFIG = {
}, },
} }
type DnsFormValues = {
enable: boolean
listen: string
enhancedMode: 'fake-ip' | 'redir-host'
fakeIpRange: string
fakeIpRange6: string
fakeIpFilterMode: 'blacklist' | 'whitelist'
preferH3: boolean
respectRules: boolean
useHosts: boolean
useSystemHosts: boolean
ipv6: boolean
fakeIpFilter: string
nameserver: string
fallback: string
defaultNameserver: string
proxyServerNameserver: string
directNameserver: string
directNameserverFollowPolicy: boolean
fallbackGeoip: boolean
fallbackGeoipCode: string
fallbackIpcidr: string
fallbackDomain: string
nameserverPolicy: string
hosts: string // hosts设置,独立于dns
}
const DEFAULT_FORM_VALUES: DnsFormValues = {
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver: DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
}
function buildDnsConfig(values: DnsFormValues) {
const dnsConfig: any = {
enable: values.enable,
listen: values.listen,
'enhanced-mode': values.enhancedMode,
'fake-ip-range': values.fakeIpRange,
'fake-ip-range6': values.fakeIpRange6,
'fake-ip-filter-mode': values.fakeIpFilterMode,
'prefer-h3': values.preferH3,
'respect-rules': values.respectRules,
'use-hosts': values.useHosts,
'use-system-hosts': values.useSystemHosts,
ipv6: values.ipv6,
'fake-ip-filter': parseList(values.fakeIpFilter),
'default-nameserver': parseList(values.defaultNameserver),
nameserver: parseList(values.nameserver),
'nameserver-policy': parseNameserverPolicy(values.nameserverPolicy),
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
'fallback-filter': {
geoip: values.fallbackGeoip,
'geoip-code': values.fallbackGeoipCode,
ipcidr: parseList(values.fallbackIpcidr),
domain: parseList(values.fallbackDomain),
},
fallback: parseList(values.fallback),
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
'direct-nameserver': parseList(values.directNameserver),
}
return dnsConfig
}
function buildDnsYaml(values: DnsFormValues) {
const config: Record<string, any> = {}
const dnsConfig = buildDnsConfig(values)
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
config.hosts = parseHosts(values.hosts)
return yaml.dump(config, { forceQuotes: true })
}
export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) { export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation() const { t } = useTranslation()
const { mutateClash } = useClash() const { mutateClash } = useClash()
@@ -302,10 +203,64 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const themeMode = useThemeMode() const themeMode = useThemeMode()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [initializing, setInitializing] = useState(false)
const [visualization, setVisualization] = useState(true) const [visualization, setVisualization] = useState(true)
const skipYamlSyncRef = useRef(false)
const editorRef = useRef<MonacoEditorInstance | null>(null) const editorRef = useRef<MonacoEditorInstance | null>(null)
const [values, setValues] = useState<DnsFormValues>(DEFAULT_FORM_VALUES) const [values, setValues] = useState<{
enable: boolean
listen: string
enhancedMode: 'fake-ip' | 'redir-host'
fakeIpRange: string
fakeIpRange6: string
fakeIpFilterMode: 'blacklist' | 'whitelist'
preferH3: boolean
respectRules: boolean
useHosts: boolean
useSystemHosts: boolean
ipv6: boolean
fakeIpFilter: string
nameserver: string
fallback: string
defaultNameserver: string
proxyServerNameserver: string
directNameserver: string
directNameserverFollowPolicy: boolean
fallbackGeoip: boolean
fallbackGeoipCode: string
fallbackIpcidr: string
fallbackDomain: string
nameserverPolicy: string
hosts: string // hosts设置,独立于dns
}>({
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver: DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
})
// Prevent YAML-to-form sync before configuration loads. // Prevent YAML-to-form sync before configuration loads.
const [yamlContent, setYamlContent] = useReducer( const [yamlContent, setYamlContent] = useReducer(
@@ -314,7 +269,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
) )
const updateValuesFromConfig = useCallback((config: any) => { const updateValuesFromConfig = useCallback((config: any) => {
if (!config) return undefined if (!config) return
const dnsConfig = config.dns || {} const dnsConfig = config.dns || {}
const hostsConfig = config.hosts || {} const hostsConfig = config.hosts || {}
@@ -334,7 +289,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? fakeIpFilterMode ? fakeIpFilterMode
: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'] : DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
const next: DnsFormValues = { setValues({
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable, enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen, listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
enhancedMode: validEnhancedMode, enhancedMode: validEnhancedMode,
@@ -386,27 +341,127 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
nameserverPolicy: nameserverPolicy:
formatNameserverPolicy(dnsConfig['nameserver-policy']) || '', formatNameserverPolicy(dnsConfig['nameserver-policy']) || '',
hosts: formatHosts(hostsConfig) || '', hosts: formatHosts(hostsConfig) || '',
} })
setValues(next)
return next
}, []) }, [])
const generateDnsConfig = useCallback(() => {
const dnsConfig: any = {
enable: values.enable,
listen: values.listen,
'enhanced-mode': values.enhancedMode,
'fake-ip-range': values.fakeIpRange,
'fake-ip-range6': values.fakeIpRange6,
'fake-ip-filter-mode': values.fakeIpFilterMode,
'prefer-h3': values.preferH3,
'respect-rules': values.respectRules,
'use-hosts': values.useHosts,
'use-system-hosts': values.useSystemHosts,
ipv6: values.ipv6,
'fake-ip-filter': parseList(values.fakeIpFilter),
'default-nameserver': parseList(values.defaultNameserver),
nameserver: parseList(values.nameserver),
'nameserver-policy': parseNameserverPolicy(values.nameserverPolicy),
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
'fallback-filter': {
geoip: values.fallbackGeoip,
'geoip-code': values.fallbackGeoipCode,
ipcidr: parseList(values.fallbackIpcidr),
domain: parseList(values.fallbackDomain),
},
fallback: parseList(values.fallback),
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
'direct-nameserver': parseList(values.directNameserver),
}
return dnsConfig
}, [values])
const updateYamlFromValues = useCallback(() => {
const config: Record<string, any> = {}
const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
config.hosts = parseHosts(values.hosts)
setYamlContent(yaml.dump(config, { forceQuotes: true }))
}, [generateDnsConfig, values.hosts])
const resetToDefaults = useCallback(() => { const resetToDefaults = useCallback(() => {
setValues(DEFAULT_FORM_VALUES) setValues({
setYamlContent(buildDnsYaml(DEFAULT_FORM_VALUES)) enable: DEFAULT_DNS_CONFIG.enable,
}, []) listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver:
DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
})
updateYamlFromValues()
}, [updateYamlFromValues])
const updateValuesFromYaml = useCallback(() => { const updateValuesFromYaml = useCallback(() => {
try { try {
const parsedYaml = yaml.load(yamlContent) as any const parsedYaml = yaml.load(yamlContent) as any
if (!parsedYaml || parsedYaml.isInitValue) return if (!parsedYaml || parsedYaml.isInitValue) return
skipYamlSyncRef.current = true
updateValuesFromConfig(parsedYaml) updateValuesFromConfig(parsedYaml)
} catch { } catch {
showNotice.error('settings.modals.dns.errors.invalidYaml') showNotice.error('settings.modals.dns.errors.invalidYaml')
} }
}, [yamlContent, updateValuesFromConfig]) }, [yamlContent, updateValuesFromConfig])
useEffect(() => {
if (skipYamlSyncRef.current) {
skipYamlSyncRef.current = false
return
}
updateYamlFromValues()
}, [updateYamlFromValues])
const latestUpdateValuesFromYamlRef = useRef(updateValuesFromYaml)
const latestUpdateYamlFromValuesRef = useRef(updateYamlFromValues)
useEffect(() => {
latestUpdateValuesFromYamlRef.current = updateValuesFromYaml
latestUpdateYamlFromValuesRef.current = updateYamlFromValues
}, [updateValuesFromYaml, updateYamlFromValues])
useEffect(() => {
if (visualization) {
latestUpdateValuesFromYamlRef.current()
} else {
latestUpdateYamlFromValuesRef.current()
}
}, [visualization])
useEffect(() => { useEffect(() => {
return () => { return () => {
editorRef.current?.dispose() editorRef.current?.dispose()
@@ -415,33 +470,34 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
}, []) }, [])
const initDnsConfig = useCallback(async () => { const initDnsConfig = useCallback(async () => {
setInitializing(true)
try { try {
const dnsConfig = await getDnsConfigContent() const dnsConfig = await getDnsConfigContent()
if (dnsConfig !== null) { if (dnsConfig !== null) {
const config = yaml.load(dnsConfig) as any const config = yaml.load(dnsConfig) as any
const loaded = updateValuesFromConfig(config) updateValuesFromConfig(config)
setYamlContent(loaded ? buildDnsYaml(loaded) : dnsConfig) setYamlContent(dnsConfig)
} else { } else {
resetToDefaults() resetToDefaults()
} }
} catch (err) { } catch (err) {
console.error('Failed to initialize DNS config', err) console.error('Failed to initialize DNS config', err)
resetToDefaults() resetToDefaults()
} finally {
setInitializing(false)
} }
}, [resetToDefaults, updateValuesFromConfig]) }, [resetToDefaults, updateValuesFromConfig])
useSettingDialog(ref, 'dns-viewer', open, () => ({ useImperativeHandle(
open: () => { ref,
setOpen(true) () => ({
void initDnsConfig() open: () => {
}, setOpen(true)
close: () => setOpen(false), void initDnsConfig()
})) },
close: () => setOpen(false),
}),
[initDnsConfig],
)
const onSave = useLockFn(async () => { const onSave = useLockFn(async () => {
try { try {
@@ -450,7 +506,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
if (visualization) { if (visualization) {
config = {} config = {}
const dnsConfig = buildDnsConfig(values) const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) { if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig config.dns = dnsConfig
} }
@@ -551,7 +607,6 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.dns.dialog.title')} {t('settings.modals.dns.dialog.title')}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button <Button
data-setting-target="dns-viewer-reset"
variant="outlined" variant="outlined"
size="small" size="small"
color="warning" color="warning"
@@ -563,14 +618,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Button <Button
variant="contained" variant="contained"
size="small" size="small"
data-setting-target="dns-mode"
onClick={() => { onClick={() => {
if (visualization) { setVisualization((prev) => !prev)
setYamlContent(buildDnsYaml(values))
} else {
updateValuesFromYaml()
}
setVisualization(!visualization)
}} }}
> >
{visualization {visualization
@@ -593,15 +642,6 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<span
data-settings-dialog="dns-viewer"
data-search-ready={!initializing}
/>
{initializing && (
<Typography role="status" data-settings-loading tabIndex={-1}>
{t('shared.statuses.loading')}
</Typography>
)}
<Alert severity="info" sx={{ mb: 2 }}> <Alert severity="info" sx={{ mb: 2 }}>
{t('settings.modals.dns.dialog.profileScope')} {t('settings.modals.dns.dialog.profileScope')}
</Alert> </Alert>
@@ -622,7 +662,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.dns.sections.general')} {t('settings.modals.dns.sections.general')}
</Typography> </Typography>
<Item data-setting-target="settings.modals.dns.fields.enable"> <Item>
<ListItemText primary={t('settings.modals.dns.fields.enable')} /> <ListItemText primary={t('settings.modals.dns.fields.enable')} />
<Switch <Switch
edge="end" edge="end"
@@ -631,7 +671,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.listen"> <Item>
<ListItemText primary={t('settings.modals.dns.fields.listen')} /> <ListItemText primary={t('settings.modals.dns.fields.listen')} />
<TextField <TextField
size="small" size="small"
@@ -644,7 +684,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.enhancedMode"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.enhancedMode')} primary={t('settings.modals.dns.fields.enhancedMode')}
/> />
@@ -659,7 +699,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</FormControl> </FormControl>
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.fakeIpRange"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.fakeIpRange')} primary={t('settings.modals.dns.fields.fakeIpRange')}
/> />
@@ -674,7 +714,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.fakeIpRange6"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.fakeIpRange6')} primary={t('settings.modals.dns.fields.fakeIpRange6')}
/> />
@@ -689,7 +729,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.fakeIpFilterMode"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.fakeIpFilterMode')} primary={t('settings.modals.dns.fields.fakeIpFilterMode')}
/> />
@@ -704,7 +744,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</FormControl> </FormControl>
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.ipv6.label"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.ipv6.label')} primary={t('settings.modals.dns.fields.ipv6.label')}
secondary={t('settings.modals.dns.fields.ipv6.description')} secondary={t('settings.modals.dns.fields.ipv6.description')}
@@ -716,7 +756,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.preferH3.label"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.preferH3.label')} primary={t('settings.modals.dns.fields.preferH3.label')}
secondary={t('settings.modals.dns.fields.preferH3.description')} secondary={t('settings.modals.dns.fields.preferH3.description')}
@@ -728,7 +768,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.respectRules.label"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.respectRules.label')} primary={t('settings.modals.dns.fields.respectRules.label')}
secondary={t( secondary={t(
@@ -742,7 +782,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.useHosts.label"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.useHosts.label')} primary={t('settings.modals.dns.fields.useHosts.label')}
secondary={t('settings.modals.dns.fields.useHosts.description')} secondary={t('settings.modals.dns.fields.useHosts.description')}
@@ -754,7 +794,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.useSystemHosts.label"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.useSystemHosts.label')} primary={t('settings.modals.dns.fields.useSystemHosts.label')}
secondary={t( secondary={t(
@@ -768,7 +808,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.directPolicy.label"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.directPolicy.label')} primary={t('settings.modals.dns.fields.directPolicy.label')}
secondary={t( secondary={t(
@@ -782,10 +822,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.defaultNameserver.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.defaultNameserver.label')} primary={t('settings.modals.dns.fields.defaultNameserver.label')}
secondary={t( secondary={t(
@@ -805,10 +842,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.nameserver.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.nameserver.label')} primary={t('settings.modals.dns.fields.nameserver.label')}
secondary={t('settings.modals.dns.fields.nameserver.description')} secondary={t('settings.modals.dns.fields.nameserver.description')}
@@ -826,10 +860,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.fallback.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.fallback.label')} primary={t('settings.modals.dns.fields.fallback.label')}
secondary={t('settings.modals.dns.fields.fallback.description')} secondary={t('settings.modals.dns.fields.fallback.description')}
@@ -847,10 +878,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.proxy.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.proxy.label')} primary={t('settings.modals.dns.fields.proxy.label')}
secondary={t('settings.modals.dns.fields.proxy.description')} secondary={t('settings.modals.dns.fields.proxy.description')}
@@ -868,10 +896,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.directNameserver.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.directNameserver.label')} primary={t('settings.modals.dns.fields.directNameserver.label')}
secondary={t( secondary={t(
@@ -891,10 +916,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.fakeIpFilter.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.fakeIpFilter.label')} primary={t('settings.modals.dns.fields.fakeIpFilter.label')}
secondary={t( secondary={t(
@@ -914,10 +936,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.nameserverPolicy.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.nameserverPolicy.label')} primary={t('settings.modals.dns.fields.nameserverPolicy.label')}
secondary={t( secondary={t(
@@ -944,7 +963,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.dns.sections.fallbackFilter')} {t('settings.modals.dns.sections.fallbackFilter')}
</Typography> </Typography>
<Item data-setting-target="settings.modals.dns.fields.geoipFiltering.label"> <Item>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.geoipFiltering.label')} primary={t('settings.modals.dns.fields.geoipFiltering.label')}
secondary={t( secondary={t(
@@ -958,7 +977,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item data-setting-target="settings.modals.dns.fields.geoipCode"> <Item>
<ListItemText primary={t('settings.modals.dns.fields.geoipCode')} /> <ListItemText primary={t('settings.modals.dns.fields.geoipCode')} />
<TextField <TextField
size="small" size="small"
@@ -971,10 +990,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.fallbackIpCidr.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.fallbackIpCidr.label')} primary={t('settings.modals.dns.fields.fallbackIpCidr.label')}
secondary={t( secondary={t(
@@ -994,10 +1010,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</Item> </Item>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.fallbackDomain.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.fallbackDomain.label')} primary={t('settings.modals.dns.fields.fallbackDomain.label')}
secondary={t( secondary={t(
@@ -1024,10 +1037,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.dns.sections.hosts')} {t('settings.modals.dns.sections.hosts')}
</Typography> </Typography>
<Item <Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
data-setting-target="settings.modals.dns.fields.hosts.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.dns.fields.hosts.label')} primary={t('settings.modals.dns.fields.hosts.label')}
secondary={t('settings.modals.dns.fields.hosts.description')} secondary={t('settings.modals.dns.fields.hosts.description')}
@@ -1,7 +1,7 @@
import { Delete as DeleteIcon } from '@mui/icons-material' import { Delete as DeleteIcon } from '@mui/icons-material'
import { Box, Button, Divider, List, ListItem, TextField } from '@mui/material' import { Box, Button, Divider, List, ListItem, TextField } from '@mui/material'
import { useLockFn, useRequest } from 'ahooks' import { useLockFn, useRequest } from 'ahooks'
import { forwardRef, useRef, useState } from 'react' import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, Switch } from '@/components/base' import { BaseDialog, Switch } from '@/components/base'
@@ -10,8 +10,6 @@ import { restartCore } from '@/services/cmds'
import { mutate } from '@/services/mutate' import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
// Development origins must never be persisted into production configuration. // Development origins must never be persisted into production configuration.
const DEV_URLS = [ const DEV_URLS = [
'tauri://localhost', 'tauri://localhost',
@@ -156,7 +154,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
}, },
) )
useSettingDialog(ref, 'external-controller-cors', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
const cors = clash?.['external-controller-cors'] const cors = clash?.['external-controller-cors']
const origins = cors?.['allow-origins'] ?? [] const origins = cors?.['allow-origins'] ?? []
@@ -188,12 +186,8 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={handleSave} onOk={handleSave}
> >
<span data-settings-dialog="external-controller-cors" />
<List sx={{ width: '90%', padding: 2 }}> <List sx={{ width: '90%', padding: 2 }}>
<ListItem <ListItem sx={{ padding: '8px 0' }}>
data-setting-target="settings.sections.externalCors.fields.allowPrivateNetwork"
sx={{ padding: '8px 0' }}
>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -220,10 +214,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
<ListItem <ListItem sx={{ padding: '8px 0' }}>
data-setting-target="settings.sections.externalCors.fields.allowedOrigins"
sx={{ padding: '8px 0' }}
>
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<div style={{ marginBottom: 8, fontWeight: 'bold' }}> <div style={{ marginBottom: 8, fontWeight: 'bold' }}>
{t('settings.sections.externalCors.fields.allowedOrigins')} {t('settings.sections.externalCors.fields.allowedOrigins')}
@@ -261,7 +252,6 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
</div> </div>
))} ))}
<Button <Button
data-setting-target="settings.sections.externalCors.actions.add"
variant="contained" variant="contained"
size="small" size="small"
onClick={handleAddOrigin} onClick={handleAddOrigin}
+4 -10
View File
@@ -1,14 +1,12 @@
import { styled, Typography } from '@mui/material' import { styled, Typography } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { forwardRef, useState } from 'react' import { forwardRef, useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch } from '@/components/base' import { BaseDialog, DialogRef, Switch } from '@/components/base'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
import { HotkeyInput } from './hotkey-input' import { HotkeyInput } from './hotkey-input'
const ItemWrapper = styled('div')` const ItemWrapper = styled('div')`
@@ -53,7 +51,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
verge?.enable_global_hotkey ?? true, verge?.enable_global_hotkey ?? true,
) )
useSettingDialog(ref, 'hotkey-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true) setOpen(true)
@@ -113,11 +111,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<span data-settings-dialog="hotkey-viewer" /> <ItemWrapper style={{ marginBottom: 16 }}>
<ItemWrapper
data-setting-target="settings.modals.hotkey.toggles.enableGlobal"
style={{ marginBottom: 16 }}
>
<Typography> <Typography>
{t('settings.modals.hotkey.toggles.enableGlobal')} {t('settings.modals.hotkey.toggles.enableGlobal')}
</Typography> </Typography>
@@ -129,7 +123,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
</ItemWrapper> </ItemWrapper>
{HOTKEY_FUNC.map((func) => ( {HOTKEY_FUNC.map((func) => (
<ItemWrapper key={func} data-setting-target={HOTKEY_FUNC_LABELS[func]}> <ItemWrapper key={func}>
<Typography>{t(HOTKEY_FUNC_LABELS[func])}</Typography> <Typography>{t(HOTKEY_FUNC_LABELS[func])}</Typography>
<HotkeyInput <HotkeyInput
value={hotkeyMap[func] ?? []} value={hotkeyMap[func] ?? []}
+19 -23
View File
@@ -14,7 +14,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
import { join } from '@tauri-apps/api/path' import { join } from '@tauri-apps/api/path'
import { open as openDialog } from '@tauri-apps/plugin-dialog' import { open as openDialog } from '@tauri-apps/plugin-dialog'
import { exists } from '@tauri-apps/plugin-fs' import { exists } from '@tauri-apps/plugin-fs'
import { forwardRef, useEffect, useState } from 'react' import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base' import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
@@ -26,8 +26,6 @@ import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import getSystem from '@/utils/get-system' import getSystem from '@/utils/get-system'
import { useSettingDialog } from '../settings-search-context'
import { GuardState } from './guard-state' import { GuardState } from './guard-state'
const OS = getSystem() const OS = getSystem()
@@ -99,7 +97,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
} }
} }
useSettingDialog(ref, 'layout-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => setOpen(true), open: () => setOpen(true),
close: () => setOpen(false), close: () => setOpen(false),
})) }))
@@ -122,9 +120,8 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
> >
<span data-settings-dialog="layout-viewer" />
<List> <List>
<Item data-setting-target="settings.components.verge.layout.fields.preferSystemTitlebar"> <Item>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.components.verge.layout.fields.preferSystemTitlebar', 'settings.components.verge.layout.fields.preferSystemTitlebar',
@@ -143,7 +140,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.trafficGraph"> <Item>
<ListItemText <ListItemText
primary={t('settings.components.verge.layout.fields.trafficGraph')} primary={t('settings.components.verge.layout.fields.trafficGraph')}
/> />
@@ -159,7 +156,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.memoryUsage"> <Item>
<ListItemText <ListItemText
primary={t('settings.components.verge.layout.fields.memoryUsage')} primary={t('settings.components.verge.layout.fields.memoryUsage')}
/> />
@@ -175,7 +172,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.proxyGroupIcon"> <Item>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.components.verge.layout.fields.proxyGroupIcon', 'settings.components.verge.layout.fields.proxyGroupIcon',
@@ -193,7 +190,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur"> <Item>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur', 'settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur',
@@ -215,7 +212,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.toastPosition"> <Item>
<ListItemText <ListItemText
primary={t('settings.components.verge.layout.fields.toastPosition')} primary={t('settings.components.verge.layout.fields.toastPosition')}
/> />
@@ -251,7 +248,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.hoverNavigator"> <Item>
<ListItemText <ListItemText
primary={ primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
@@ -279,7 +276,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.hoverNavigatorDelay"> <Item>
<ListItemText <ListItemText
primary={ primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
@@ -338,7 +335,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.navIcon"> <Item>
<ListItemText <ListItemText
primary={t('settings.components.verge.layout.fields.navIcon')} primary={t('settings.components.verge.layout.fields.navIcon')}
/> />
@@ -363,7 +360,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.collapseNavBar"> <Item>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.components.verge.layout.fields.collapseNavBar', 'settings.components.verge.layout.fields.collapseNavBar',
@@ -382,7 +379,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item> </Item>
{OS === 'macos' && ( {OS === 'macos' && (
<Item data-setting-target="settings.components.verge.layout.fields.trayIcon"> <Item>
<ListItemText <ListItemText
primary={t('settings.components.verge.layout.fields.trayIcon')} primary={t('settings.components.verge.layout.fields.trayIcon')}
/> />
@@ -410,7 +407,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item> </Item>
)} )}
{OS === 'macos' && ( {OS === 'macos' && (
<Item data-setting-target="settings.components.verge.layout.fields.enableTraySpeed"> <Item>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.components.verge.layout.fields.enableTraySpeed', 'settings.components.verge.layout.fields.enableTraySpeed',
@@ -448,7 +445,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
)} */} )} */}
<Item data-setting-target="settings.components.verge.layout.fields.proxyGroupsDisplayMode"> <Item>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.components.verge.layout.fields.proxyGroupsDisplayMode', 'settings.components.verge.layout.fields.proxyGroupsDisplayMode',
@@ -484,7 +481,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Select> </Select>
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.showOutboundModesInline"> <Item>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.components.verge.layout.fields.showOutboundModesInline', 'settings.components.verge.layout.fields.showOutboundModesInline',
@@ -502,7 +499,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.commonTrayIcon"> <Item>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.components.verge.layout.fields.commonTrayIcon', 'settings.components.verge.layout.fields.commonTrayIcon',
@@ -515,7 +512,6 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
onGuard={(e) => patchVerge({ common_tray_icon: e })} onGuard={(e) => patchVerge({ common_tray_icon: e })}
> >
<Button <Button
data-setting-target="settings.components.verge.basic.actions.browse"
variant="outlined" variant="outlined"
size="small" size="small"
startIcon={ startIcon={
@@ -558,7 +554,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.systemProxyTrayIcon"> <Item>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.components.verge.layout.fields.systemProxyTrayIcon', 'settings.components.verge.layout.fields.systemProxyTrayIcon',
@@ -613,7 +609,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item data-setting-target="settings.components.verge.layout.fields.tunTrayIcon"> <Item>
<ListItemText <ListItemText
primary={t('settings.components.verge.layout.fields.tunTrayIcon')} primary={t('settings.components.verge.layout.fields.tunTrayIcon')}
/> />
@@ -8,7 +8,7 @@ import {
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import type { Ref } from 'react' import type { Ref } from 'react'
import { useState } from 'react' import { useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base' import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
@@ -17,8 +17,6 @@ import { entry_lightweight_mode } from '@/services/cmds'
import { mutate } from '@/services/mutate' import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) { export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation() const { t } = useTranslation()
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
@@ -29,7 +27,7 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
autoEnterLiteModeDelay: 10, // 默认10分钟 autoEnterLiteModeDelay: 10, // 默认10分钟
}) })
useSettingDialog(ref, 'lite-mode-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true) setOpen(true)
setValues({ setValues({
@@ -63,12 +61,8 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<span data-settings-dialog="lite-mode-viewer" />
<List> <List>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.liteMode.actions.enterNow"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.liteMode.actions.enterNow')} primary={t('settings.modals.liteMode.actions.enterNow')}
/> />
@@ -90,10 +84,7 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Typography> </Typography>
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.liteMode.toggles.autoEnter"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.liteMode.toggles.autoEnter')} primary={t('settings.modals.liteMode.toggles.autoEnter')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -114,10 +105,7 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
{values.autoEnterLiteMode && ( {values.autoEnterLiteMode && (
<> <>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.liteMode.fields.delay"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.liteMode.fields.delay')} primary={t('settings.modals.liteMode.fields.delay')}
/> />
+14 -53
View File
@@ -8,15 +8,13 @@ import {
TextField, TextField,
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { forwardRef, useState } from 'react' import { forwardRef, useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base' import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
const buildInitialValues = (verge?: IVergeConfig | null) => ({ const buildInitialValues = (verge?: IVergeConfig | null) => ({
appLogLevel: verge?.app_log_level ?? 'warn', appLogLevel: verge?.app_log_level ?? 'warn',
appLogMaxSize: verge?.app_log_max_size ?? 128, appLogMaxSize: verge?.app_log_max_size ?? 128,
@@ -40,7 +38,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [values, setValues] = useState(() => buildInitialValues(null)) const [values, setValues] = useState(() => buildInitialValues(null))
useSettingDialog(ref, 'misc-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true) setOpen(true)
setValues(buildInitialValues(verge)) setValues(buildInitialValues(verge))
@@ -82,12 +80,8 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<span data-settings-dialog="misc-viewer" />
<List> <List>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.appLogLevel"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.appLogLevel')} primary={t('settings.modals.misc.fields.appLogLevel')}
/> />
@@ -110,10 +104,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</Select> </Select>
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.appLogMaxSize"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.appLogMaxSize')} primary={t('settings.modals.misc.fields.appLogMaxSize')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -145,10 +136,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.appLogMaxCount"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.appLogMaxCount')} primary={t('settings.modals.misc.fields.appLogMaxCount')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -180,10 +168,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.autoCloseConnections"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.autoCloseConnections')} primary={t('settings.modals.misc.fields.autoCloseConnections')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -202,10 +187,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.autoCheckUpdate"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.autoCheckUpdate')} primary={t('settings.modals.misc.fields.autoCheckUpdate')}
/> />
@@ -218,10 +200,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.enableBuiltinEnhanced"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.enableBuiltinEnhanced')} primary={t('settings.modals.misc.fields.enableBuiltinEnhanced')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -240,10 +219,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.proxyLayoutColumns"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.proxyLayoutColumns')} primary={t('settings.modals.misc.fields.proxyLayoutColumns')}
/> />
@@ -269,10 +245,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</Select> </Select>
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.autoLogClean"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.autoLogClean')} primary={t('settings.modals.misc.fields.autoLogClean')}
/> />
@@ -325,10 +298,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</Select> </Select>
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.autoDelayDetection"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.autoDelayDetection')} primary={t('settings.modals.misc.fields.autoDelayDetection')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -347,10 +317,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.autoDelayDetectionInterval"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.modals.misc.fields.autoDelayDetectionInterval', 'settings.modals.misc.fields.autoDelayDetectionInterval',
@@ -388,10 +355,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.defaultLatencyTest"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.defaultLatencyTest')} primary={t('settings.modals.misc.fields.defaultLatencyTest')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -415,10 +379,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.misc.fields.defaultLatencyTimeout"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.misc.fields.defaultLatencyTimeout')} primary={t('settings.modals.misc.fields.defaultLatencyTimeout')}
/> />
@@ -2,21 +2,19 @@ import { ContentCopyRounded } from '@mui/icons-material'
import { alpha, Box, Button, CircularProgress, IconButton } from '@mui/material' import { alpha, Box, Button, CircularProgress, IconButton } from '@mui/material'
import { writeText } from '@tauri-apps/plugin-clipboard-manager' import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import type { Ref } from 'react' import type { Ref } from 'react'
import { useState } from 'react' import { useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, BaseEmpty, DialogRef } from '@/components/base' import { BaseDialog, BaseEmpty, DialogRef } from '@/components/base'
import { useNetworkInterfaces } from '@/hooks/use-network' import { useNetworkInterfaces } from '@/hooks/use-network'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) { export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [isV4, setIsV4] = useState(true) const [isV4, setIsV4] = useState(true)
useSettingDialog(ref, 'network-interface-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true) setOpen(true)
}, },
@@ -53,49 +51,41 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
> >
<div {loading && isEmpty ? (
data-settings-dialog="network-interface-viewer" <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
data-setting-target="network-interface-viewer" <CircularProgress size={24} />
tabIndex={-1} </Box>
> ) : isEmpty ? (
{loading && isEmpty ? ( <Box sx={{ minHeight: 160 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> <BaseEmpty />
<CircularProgress size={24} /> </Box>
</Box> ) : (
) : isEmpty ? ( networkInterfaces.map((item) => (
<Box sx={{ minHeight: 160 }}> <Box key={item.name}>
<BaseEmpty /> <h4>{item.name}</h4>
</Box> <Box>
) : ( {item.addr.map((address) => {
networkInterfaces.map((item) => ( const ip = getAddressIp(address)
<Box key={item.name}> return (
<h4>{item.name}</h4> ip && (
<Box> <AddressDisplay
{item.addr.map((address) => { key={ip}
const ip = getAddressIp(address) label={t(
return ( 'settings.modals.networkInterface.fields.ipAddress',
ip && ( )}
<AddressDisplay content={ip}
key={ip} />
label={t(
'settings.modals.networkInterface.fields.ipAddress',
)}
content={ip}
/>
)
) )
})} )
<AddressDisplay })}
label={t( <AddressDisplay
'settings.modals.networkInterface.fields.macAddress', label={t('settings.modals.networkInterface.fields.macAddress')}
)} content={item.mac_addr ?? ''}
content={item.mac_addr ?? ''} />
/>
</Box>
</Box> </Box>
)) </Box>
)} ))
</div> )}
</BaseDialog> </BaseDialog>
) )
} }
+2 -4
View File
@@ -13,7 +13,6 @@ import React, { ReactNode, useState } from 'react'
import isAsyncFunction from '@/utils/is-async-function' import isAsyncFunction from '@/utils/is-async-function'
interface ItemProps { interface ItemProps {
'data-setting-target'?: string
label: ReactNode label: ReactNode
extra?: ReactNode extra?: ReactNode
children?: ReactNode children?: ReactNode
@@ -22,7 +21,6 @@ interface ItemProps {
} }
export const SettingItem: React.FC<ItemProps> = ({ export const SettingItem: React.FC<ItemProps> = ({
'data-setting-target': searchTarget,
label, label,
extra, extra,
children, children,
@@ -51,7 +49,7 @@ export const SettingItem: React.FC<ItemProps> = ({
} }
return clickable ? ( return clickable ? (
<ListItem data-setting-target={searchTarget} disablePadding> <ListItem disablePadding>
<ListItemButton onClick={handleClick} disabled={isLoading}> <ListItemButton onClick={handleClick} disabled={isLoading}>
<ListItemText primary={primary} secondary={secondary} /> <ListItemText primary={primary} secondary={secondary} />
{isLoading ? ( {isLoading ? (
@@ -62,7 +60,7 @@ export const SettingItem: React.FC<ItemProps> = ({
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
) : ( ) : (
<ListItem data-setting-target={searchTarget} sx={{ pt: '5px', pb: '5px' }}> <ListItem sx={{ pt: '5px', pb: '5px' }}>
<ListItemText primary={primary} secondary={secondary} /> <ListItemText primary={primary} secondary={secondary} />
{children} {children}
</ListItem> </ListItem>
+50 -74
View File
@@ -13,7 +13,13 @@ import {
Typography, Typography,
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { forwardRef, useMemo, useState } from 'react' import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
@@ -35,12 +41,9 @@ import {
getSystemHostname, getSystemHostname,
} from '@/services/cmds' } from '@/services/cmds'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { debugLog } from '@/utils/debug' import { debugLog } from '@/utils/debug'
import getSystem from '@/utils/get-system' import getSystem from '@/utils/get-system'
import { useSettingDialog } from '../settings-search-context'
const DEFAULT_PAC = `function FindProxyForURL(url, host) { const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;"; return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;";
}` }`
@@ -125,10 +128,13 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
} = verge ?? {} } = verge ?? {}
const [value, setValue] = useState(() => buildInitialValues(null)) const [value, setValue] = useState(() => buildInitialValues(null))
const { data: embeddedServerPort } = useQuery({ const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
queryKey: ['getEmbeddedServerPort'], null,
queryFn: getEmbeddedServerPort, )
})
useEffect(() => {
getEmbeddedServerPort().then(setEmbeddedServerPort).catch(console.error)
}, [])
const separator = useMemo(() => (isWindows ? ';' : ','), [isWindows]) const separator = useMemo(() => (isWindows ? ';' : ','), [isWindows])
@@ -185,7 +191,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
setPacEditorSavedValue(nextPac) setPacEditorSavedValue(nextPac)
}) })
useSettingDialog(ref, 'sysproxy-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true) setOpen(true)
setValue(buildInitialValues(verge)) setValue(buildInitialValues(verge))
@@ -357,49 +363,41 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
loading={saving} loading={saving}
disableOk={saving} disableOk={saving}
> >
<span data-settings-dialog="sysproxy-viewer" />
<List> <List>
<Box data-setting-target="sysproxy-status" tabIndex={-1}> <BaseFieldset
<BaseFieldset label={t('settings.modals.sysproxy.fieldsets.currentStatus')}
label={t('settings.modals.sysproxy.fieldsets.currentStatus')} padding="15px 10px"
padding="15px 10px" >
> <FlexBox>
<Typography className="label">
{t('settings.modals.sysproxy.fields.enableStatus')}
</Typography>
<Typography className="value">
{isProxyReallyEnabled
? t('shared.statuses.enabled')
: t('shared.statuses.disabled')}
</Typography>
</FlexBox>
{!value.pac && (
<FlexBox> <FlexBox>
<Typography className="label"> <Typography className="label">
{t('settings.modals.sysproxy.fields.enableStatus')} {t('settings.modals.sysproxy.fields.serverAddr')}
</Typography>
<Typography className="value">{getSystemProxyAddress}</Typography>
</FlexBox>
)}
{value.pac && (
<FlexBox>
<Typography className="label">
{t('settings.modals.sysproxy.fields.pacUrl')}
</Typography> </Typography>
<Typography className="value"> <Typography className="value">
{isProxyReallyEnabled {getCurrentPacUrl || '-'}
? t('shared.statuses.enabled')
: t('shared.statuses.disabled')}
</Typography> </Typography>
</FlexBox> </FlexBox>
{!value.pac && ( )}
<FlexBox> </BaseFieldset>
<Typography className="label"> <ListItem sx={{ padding: '5px 2px' }}>
{t('settings.modals.sysproxy.fields.serverAddr')}
</Typography>
<Typography className="value">
{getSystemProxyAddress}
</Typography>
</FlexBox>
)}
{value.pac && (
<FlexBox>
<Typography className="label">
{t('settings.modals.sysproxy.fields.pacUrl')}
</Typography>
<Typography className="value">
{getCurrentPacUrl || '-'}
</Typography>
</FlexBox>
)}
</BaseFieldset>
</Box>
<ListItem
data-setting-target="settings.modals.sysproxy.fields.proxyHost"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.sysproxy.fields.proxyHost')} primary={t('settings.modals.sysproxy.fields.proxyHost')}
/> />
@@ -426,10 +424,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
}} }}
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.sysproxy.fields.usePacMode"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.sysproxy.fields.usePacMode')} primary={t('settings.modals.sysproxy.fields.usePacMode')}
/> />
@@ -441,10 +436,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.sysproxy.fields.proxyGuard"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.sysproxy.fields.proxyGuard')} primary={t('settings.modals.sysproxy.fields.proxyGuard')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -462,10 +454,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.sysproxy.fields.guardDuration"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.sysproxy.fields.guardDuration')} primary={t('settings.modals.sysproxy.fields.guardDuration')}
/> />
@@ -488,10 +477,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
{!value.pac && ( {!value.pac && (
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.sysproxy.fields.alwaysUseDefaultBypass"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.modals.sysproxy.fields.alwaysUseDefaultBypass', 'settings.modals.sysproxy.fields.alwaysUseDefaultBypass',
@@ -518,10 +504,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
)} )}
{!value.pac && ( {!value.pac && (
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.sysproxy.fields.enableBypassCheck"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.sysproxy.fields.enableBypassCheck')} primary={t('settings.modals.sysproxy.fields.enableBypassCheck')}
/> />
@@ -553,10 +536,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
setValue((v) => ({ ...v, bypass: nextValue })) setValue((v) => ({ ...v, bypass: nextValue }))
}} }}
renderHeader={(modeToggle) => ( renderHeader={(modeToggle) => (
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.sysproxy.fields.proxyBypass"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.sysproxy.fields.proxyBypass')} primary={t('settings.modals.sysproxy.fields.proxyBypass')}
/> />
@@ -584,16 +564,12 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
)} )}
{value.pac && ( {value.pac && (
<ListItem <ListItem sx={{ padding: '5px 2px', alignItems: 'start' }}>
data-setting-target="settings.modals.sysproxy.fields.pacScriptContent"
sx={{ padding: '5px 2px', alignItems: 'start' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.sysproxy.fields.pacScriptContent')} primary={t('settings.modals.sysproxy.fields.pacScriptContent')}
sx={{ padding: '3px 0' }} sx={{ padding: '3px 0' }}
/> />
<Button <Button
data-setting-target="settings.modals.sysproxy.actions.editPac"
startIcon={<EditRounded />} startIcon={<EditRounded />}
variant="outlined" variant="outlined"
onClick={openPacEditor} onClick={openPacEditor}
+11 -9
View File
@@ -9,7 +9,13 @@ import {
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { useEffect, useMemo, useRef, useState } from 'react' import {
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef } from '@/components/base' import { BaseDialog, DialogRef } from '@/components/base'
@@ -18,8 +24,6 @@ import { useVerge } from '@/hooks/use-verge'
import { defaultDarkTheme, defaultTheme } from '@/pages/_theme' import { defaultDarkTheme, defaultTheme } from '@/pages/_theme'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) { export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
const { ref } = props const { ref } = props
const { t } = useTranslation() const { t } = useTranslation()
@@ -37,7 +41,7 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
themeRef.current = theme themeRef.current = theme
}, [theme]) }, [theme])
useSettingDialog(ref, 'theme-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true) setOpen(true)
setTheme({ ...theme_setting }) setTheme({ ...theme_setting })
@@ -124,7 +128,7 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
const renderItem = (labelKey: string, key: ThemeKey) => { const renderItem = (labelKey: string, key: ThemeKey) => {
const label = t(labelKey) const label = t(labelKey)
return ( return (
<Item key={key} data-setting-target={labelKey}> <Item key={key}>
<ListItemText primary={label} /> <ListItemText primary={label} />
<Round sx={{ background: theme[key] || dt[key] }} /> <Round sx={{ background: theme[key] || dt[key] }} />
<TextField <TextField
@@ -149,11 +153,10 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<span data-settings-dialog="theme-viewer" />
<List sx={{ pt: 0 }}> <List sx={{ pt: 0 }}>
{fieldDefinitions.map((field) => renderItem(field.labelKey, field.key))} {fieldDefinitions.map((field) => renderItem(field.labelKey, field.key))}
<Item data-setting-target="settings.components.verge.theme.fields.fontFamily"> <Item>
<ListItemText <ListItemText
primary={t('settings.components.verge.theme.fields.fontFamily')} primary={t('settings.components.verge.theme.fields.fontFamily')}
/> />
@@ -164,12 +167,11 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
onKeyDown={(e) => e.key === 'Enter' && onSave()} onKeyDown={(e) => e.key === 'Enter' && onSave()}
/> />
</Item> </Item>
<Item data-setting-target="settings.components.verge.theme.fields.cssInjection"> <Item>
<ListItemText <ListItemText
primary={t('settings.components.verge.theme.fields.cssInjection')} primary={t('settings.components.verge.theme.fields.cssInjection')}
/> />
<Button <Button
data-setting-target="settings.components.verge.theme.actions.editCss"
startIcon={<EditRounded />} startIcon={<EditRounded />}
variant="outlined" variant="outlined"
onClick={openCssEditor} onClick={openCssEditor}
+12 -46
View File
@@ -9,7 +9,7 @@ import {
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import type { Ref } from 'react' import type { Ref } from 'react'
import { useState } from 'react' import { useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
@@ -26,8 +26,6 @@ import { showNotice } from '@/services/notice-service'
import getSystem from '@/utils/get-system' import getSystem from '@/utils/get-system'
import { areValidIpCidrs } from '@/utils/network' import { areValidIpCidrs } from '@/utils/network'
import { useSettingDialog } from '../settings-search-context'
import { DnsModeSwitch } from './dns-mode-switch' import { DnsModeSwitch } from './dns-mode-switch'
import { StackModeSwitch } from './stack-mode-switch' import { StackModeSwitch } from './stack-mode-switch'
@@ -69,7 +67,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
? t('settings.modals.tun.messages.invalidRouteExcludeAddress') ? t('settings.modals.tun.messages.invalidRouteExcludeAddress')
: t('settings.modals.tun.messages.routeExcludeAddressHint') : t('settings.modals.tun.messages.routeExcludeAddressHint')
useSettingDialog(ref, 'tun-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true) setOpen(true)
const nextAutoRoute = clash?.tun['auto-route'] ?? true const nextAutoRoute = clash?.tun['auto-route'] ?? true
@@ -154,7 +152,6 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1 }}>
<Typography variant="h6">{t('settings.modals.tun.title')}</Typography> <Typography variant="h6">{t('settings.modals.tun.title')}</Typography>
<Button <Button
data-setting-target="tun-viewer-reset"
variant="outlined" variant="outlined"
size="small" size="small"
onClick={async () => { onClick={async () => {
@@ -207,12 +204,8 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<span data-settings-dialog="tun-viewer" />
<List> <List>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.stack"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.stack')} /> <ListItemText primary={t('settings.modals.tun.fields.stack')} />
<StackModeSwitch <StackModeSwitch
value={values.stack} value={values.stack}
@@ -225,10 +218,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.dnsMode"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.tun.fields.dnsMode')} primary={t('settings.modals.tun.fields.dnsMode')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -247,10 +237,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Box> </Box>
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.device"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.device')} /> <ListItemText primary={t('settings.modals.tun.fields.device')} />
<TextField <TextField
autoComplete="new-password" autoComplete="new-password"
@@ -267,10 +254,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.autoRoute"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.autoRoute')} /> <ListItemText primary={t('settings.modals.tun.fields.autoRoute')} />
<Switch <Switch
edge="end" edge="end"
@@ -286,10 +270,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem> </ListItem>
{OS === 'linux' && ( {OS === 'linux' && (
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.autoRedirect"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.tun.fields.autoRedirect')} primary={t('settings.modals.tun.fields.autoRedirect')}
sx={{ maxWidth: 'fit-content' }} sx={{ maxWidth: 'fit-content' }}
@@ -313,10 +294,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem> </ListItem>
)} )}
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.strictRoute"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.strictRoute')} /> <ListItemText primary={t('settings.modals.tun.fields.strictRoute')} />
<Switch <Switch
edge="end" edge="end"
@@ -325,10 +303,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.autoDetectInterface"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.tun.fields.autoDetectInterface')} primary={t('settings.modals.tun.fields.autoDetectInterface')}
/> />
@@ -341,10 +316,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.dnsHijack"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.dnsHijack')} /> <ListItemText primary={t('settings.modals.tun.fields.dnsHijack')} />
<TextField <TextField
autoComplete="new-password" autoComplete="new-password"
@@ -361,10 +333,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/> />
</ListItem> </ListItem>
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.mtu"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.mtu')} /> <ListItemText primary={t('settings.modals.tun.fields.mtu')} />
<TextField <TextField
autoComplete="new-password" autoComplete="new-password"
@@ -396,10 +365,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
setValues((v) => ({ ...v, routeExcludeAddress: nextValue })) setValues((v) => ({ ...v, routeExcludeAddress: nextValue }))
} }
renderHeader={(modeToggle) => ( renderHeader={(modeToggle) => (
<ListItem <ListItem sx={{ padding: '5px 2px' }}>
data-setting-target="settings.modals.tun.fields.routeExcludeAddress"
sx={{ padding: '5px 2px' }}
>
<ListItemText <ListItemText
primary={t('settings.modals.tun.fields.routeExcludeAddress')} primary={t('settings.modals.tun.fields.routeExcludeAddress')}
/> />
+36 -49
View File
@@ -1,3 +1,4 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import { Delete, ExpandLess, ExpandMore } from '@mui/icons-material' import { Delete, ExpandLess, ExpandMore } from '@mui/icons-material'
import { import {
Button, Button,
@@ -11,7 +12,14 @@ import {
Select, Select,
MenuItem, MenuItem,
} from '@mui/material' } from '@mui/material'
import { forwardRef, useMemo, useRef, useState } from 'react' import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog } from '@/components/base' import { BaseDialog } from '@/components/base'
@@ -34,8 +42,6 @@ import {
normalizeListenHost, normalizeListenHost,
} from '@/utils/network' } from '@/utils/network'
import { useSettingDialog } from '../settings-search-context'
interface TunnelsViewerRef { interface TunnelsViewerRef {
open: () => void open: () => void
close: () => void close: () => void
@@ -89,7 +95,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
const [values, setValues] = useState<TunnelFormValues>(buildInitialValues) const [values, setValues] = useState<TunnelFormValues>(buildInitialValues)
const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([]) const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([])
useSettingDialog(ref, 'tunnels-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setValues(buildInitialValues()) setValues(buildInitialValues())
setDraftTunnels(() => clash?.tunnels ?? []) setDraftTunnels(() => clash?.tunnels ?? [])
@@ -151,20 +157,29 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
const selectedProxyToken = selectedProxyOption const selectedProxyToken = selectedProxyOption
? proxyOptionToken(selectedProxyOption) ? proxyOptionToken(selectedProxyOption)
: '' : ''
// A group or member missing from the proxy view reads as the default. const selectedGroupExists =
const effectiveGroup = proxyGroups.some(({ name }) => name === values.group) !values.group || proxyGroups.some(({ name }) => name === values.group)
? values.group
: ''
const effectiveProxy = selectedProxyOption ? values.proxy : null
const latestProxyStateRef = useRef({ const latestProxyStateRef = useRef({
binding: effectiveProxy, binding: values.proxy,
options: proxyOptions, options: proxyOptions,
}) })
latestProxyStateRef.current = { latestProxyStateRef.current = {
binding: effectiveProxy, binding: values.proxy,
options: proxyOptions, options: proxyOptions,
} }
useEffect(() => {
if (selectedGroupExists && (!values.proxy || selectedProxyOption)) return
setValues((current) => {
if (current.group !== values.group || current.proxy !== values.proxy) {
return current
}
return selectedGroupExists
? { ...current, proxy: null }
: { ...current, group: '', proxy: null }
})
}, [selectedGroupExists, selectedProxyOption, values.group, values.proxy])
const handleSave = async () => { const handleSave = async () => {
try { try {
await patchClash({ tunnels: draftTunnels }) await patchClash({ tunnels: draftTunnels })
@@ -297,14 +312,10 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
}} }}
onOk={handleSave} onOk={handleSave}
> >
<span data-settings-dialog="tunnels-viewer" />
<List> <List>
{draftTunnels.length > 0 && ( {draftTunnels.length > 0 && (
<> <>
<ListItem <ListItem sx={{ padding: '4px 0', opacity: 0.6 }}>
data-setting-target="settings.sections.clash.form.fields.tunnels.existing"
sx={{ padding: '4px 0', opacity: 0.6 }}
>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.sections.clash.form.fields.tunnels.existing', 'settings.sections.clash.form.fields.tunnels.existing',
@@ -314,7 +325,6 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
<List component="nav"> <List component="nav">
{tunnelEntries.map((item) => ( {tunnelEntries.map((item) => (
<ListItem <ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.default"
key={`${item.key}`} key={`${item.key}`}
sx={{ padding: '4px 0' }} sx={{ padding: '4px 0' }}
secondaryAction={ secondaryAction={
@@ -343,7 +353,6 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
)} )}
<ListItemButton <ListItemButton
sx={{ padding: '4px 0', opacity: 0.8 }} sx={{ padding: '4px 0', opacity: 0.8 }}
data-setting-target="tunnels-expand"
onClick={() => setExpanded((v) => !v)} onClick={() => setExpanded((v) => !v)}
> >
<ListItemText <ListItemText
@@ -354,10 +363,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
{expanded ? <ExpandLess /> : <ExpandMore />} {expanded ? <ExpandLess /> : <ExpandMore />}
</ListItemButton> </ListItemButton>
{expanded && ( {expanded && (
<ListItem <ListItem sx={{ padding: '8px 0' }}>
data-setting-target="settings.sections.clash.form.fields.tunnels.protocols"
sx={{ padding: '8px 0' }}
>
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
{/* 输入框区域 */} {/* 输入框区域 */}
{/* 协议 */} {/* 协议 */}
@@ -385,10 +391,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem> </ListItem>
{/* 本地监听地址 */} {/* 本地监听地址 */}
<ListItem <ListItem sx={{ padding: '6px 2px' }}>
data-setting-target="settings.sections.clash.form.fields.tunnels.localAddr"
sx={{ padding: '6px 2px' }}
>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.sections.clash.form.fields.tunnels.localAddr', 'settings.sections.clash.form.fields.tunnels.localAddr',
@@ -407,10 +410,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem> </ListItem>
{/* 本地监听端口 */} {/* 本地监听端口 */}
<ListItem <ListItem sx={{ padding: '6px 2px' }}>
data-setting-target="settings.sections.clash.form.fields.tunnels.localPort"
sx={{ padding: '6px 2px' }}
>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.sections.clash.form.fields.tunnels.localPort', 'settings.sections.clash.form.fields.tunnels.localPort',
@@ -430,10 +430,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem> </ListItem>
{/* 目标服务器地址 */} {/* 目标服务器地址 */}
<ListItem <ListItem sx={{ padding: '6px 2px' }}>
data-setting-target="settings.sections.clash.form.fields.tunnels.targetAddr"
sx={{ padding: '6px 2px' }}
>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.sections.clash.form.fields.tunnels.targetAddr', 'settings.sections.clash.form.fields.tunnels.targetAddr',
@@ -452,10 +449,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem> </ListItem>
{/* 目标服务器端口 */} {/* 目标服务器端口 */}
<ListItem <ListItem sx={{ padding: '6px 2px' }}>
data-setting-target="settings.sections.clash.form.fields.tunnels.targetPort"
sx={{ padding: '6px 2px' }}
>
<ListItemText <ListItemText
primary={t( primary={t(
'settings.sections.clash.form.fields.tunnels.targetPort', 'settings.sections.clash.form.fields.tunnels.targetPort',
@@ -475,10 +469,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem> </ListItem>
{/* 代理组 */} {/* 代理组 */}
<ListItem <ListItem sx={{ padding: '6px 2px' }}>
data-setting-target="settings.sections.clash.form.fields.tunnels.proxyGroup"
sx={{ padding: '6px 2px' }}
>
<ListItemText <ListItemText
primary={ primary={
<> <>
@@ -499,7 +490,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
<Select <Select
size="small" size="small"
sx={{ width: 200, '> div': { py: '7.5px' } }} sx={{ width: 200, '> div': { py: '7.5px' } }}
value={effectiveGroup} value={values.group}
displayEmpty displayEmpty
onChange={(e) => { onChange={(e) => {
const nextGroup = e.target.value as string const nextGroup = e.target.value as string
@@ -540,10 +531,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem> </ListItem>
{/* 代理节点 */} {/* 代理节点 */}
<ListItem <ListItem sx={{ padding: '6px 2px' }}>
data-setting-target="settings.sections.clash.form.fields.tunnels.proxyNode"
sx={{ padding: '6px 2px' }}
>
<ListItemText <ListItemText
primary={ primary={
<> <>
@@ -581,7 +569,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
: null, : null,
})) }))
}} }}
disabled={!effectiveGroup} // 没选组就禁用 disabled={!values.group} // 没选组就禁用
> >
<MenuItem value=""> <MenuItem value="">
{t('settings.sections.clash.form.fields.tunnels.default')} {t('settings.sections.clash.form.fields.tunnels.default')}
@@ -604,7 +592,6 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
{/* 添加按钮 */} {/* 添加按钮 */}
<Button <Button
data-setting-target="settings.sections.clash.form.fields.tunnels.actions.add"
variant="contained" variant="contained"
size="small" size="small"
sx={{ sx={{
@@ -131,7 +131,6 @@ export const WebUIItem = (props: Props) => {
</Typography> </Typography>
<IconButton <IconButton
size="small" size="small"
data-setting-target="web-ui-open"
title={t('settings.modals.webUI.actions.openUrl')} title={t('settings.modals.webUI.actions.openUrl')}
color="inherit" color="inherit"
onClick={() => onOpenUrl?.(value)} onClick={() => onOpenUrl?.(value)}
@@ -140,7 +139,6 @@ export const WebUIItem = (props: Props) => {
</IconButton> </IconButton>
<IconButton <IconButton
size="small" size="small"
data-setting-target="web-ui-edit"
title={t('shared.actions.edit')} title={t('shared.actions.edit')}
color="inherit" color="inherit"
onClick={() => { onClick={() => {
@@ -152,7 +150,6 @@ export const WebUIItem = (props: Props) => {
</IconButton> </IconButton>
<IconButton <IconButton
size="small" size="small"
data-setting-target="web-ui-delete"
title={t('shared.actions.delete')} title={t('shared.actions.delete')}
color="inherit" color="inherit"
onClick={onDelete} onClick={onDelete}
+31 -40
View File
@@ -1,7 +1,7 @@
import { Box, Button, Typography } from '@mui/material' import { Box, Button, Typography } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import type { Ref } from 'react' import type { Ref } from 'react'
import { useMemo, useState } from 'react' import { useImperativeHandle, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, BaseEmpty, DialogRef } from '@/components/base' import { BaseDialog, BaseEmpty, DialogRef } from '@/components/base'
@@ -10,8 +10,6 @@ import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { openExternalUrl } from '@/utils/open-external-url' import { openExternalUrl } from '@/utils/open-external-url'
import { useSettingDialog } from '../settings-search-context'
import { WebUIItem } from './web-ui-item' import { WebUIItem } from './web-ui-item'
const DEFAULT_WEB_UI_LIST = [ const DEFAULT_WEB_UI_LIST = [
@@ -29,7 +27,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
useSettingDialog(ref, 'web-ui-viewer', open, () => ({ useImperativeHandle(ref, () => ({
open: () => setOpen(true), open: () => setOpen(true),
close: () => setOpen(false), close: () => setOpen(false),
})) }))
@@ -116,7 +114,6 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="contained" variant="contained"
size="small" size="small"
disabled={editing} disabled={editing}
data-setting-target="web-ui-new"
onClick={() => setEditing(true)} onClick={() => setEditing(true)}
> >
{t('shared.actions.new')} {t('shared.actions.new')}
@@ -135,42 +132,36 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
> >
<div {!editing && webUIList.length === 0 && (
data-settings-dialog="web-ui-viewer" <BaseEmpty
data-setting-target="web-ui-viewer" extra={
tabIndex={-1} <Typography sx={{ mt: 2, fontSize: '12px' }}>
> {t('settings.modals.webUI.messages.placeholderInstruction')}
{!editing && webUIList.length === 0 && ( </Typography>
<BaseEmpty }
extra={ />
<Typography sx={{ mt: 2, fontSize: '12px' }}> )}
{t('settings.modals.webUI.messages.placeholderInstruction')}
</Typography>
}
/>
)}
{webUIEntries.map(({ item, index, key }) => ( {webUIEntries.map(({ item, index, key }) => (
<WebUIItem <WebUIItem
key={key} key={key}
value={item} value={item}
onChange={(v) => handleChange(index, v)} onChange={(v) => handleChange(index, v)}
onDelete={() => handleDelete(index)} onDelete={() => handleDelete(index)}
onOpenUrl={handleOpenUrl} onOpenUrl={handleOpenUrl}
/> />
))} ))}
{editing && ( {editing && (
<WebUIItem <WebUIItem
value="" value=""
onlyEdit onlyEdit
onChange={(v) => { onChange={(v) => {
setEditing(false) setEditing(false)
handleAdd(v || '') handleAdd(v || '')
}} }}
onCancel={() => setEditing(false)} onCancel={() => setEditing(false)}
/> />
)} )}
</div>
</BaseDialog> </BaseDialog>
) )
} }
+2 -18
View File
@@ -156,7 +156,6 @@ const SettingClash = ({ onError }: Props) => {
<HeaderConfiguration ref={corsRef} /> <HeaderConfiguration ref={corsRef} />
<TunnelsViewer ref={tunnelRef} /> <TunnelsViewer ref={tunnelRef} />
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.allowLan"
label={t('settings.sections.clash.form.fields.allowLan')} label={t('settings.sections.clash.form.fields.allowLan')}
extra={ extra={
<TooltipIcon <TooltipIcon
@@ -182,7 +181,6 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.dnsOverwrite"
label={t('settings.sections.clash.form.fields.dnsOverwrite')} label={t('settings.sections.clash.form.fields.dnsOverwrite')}
extra={ extra={
<> <>
@@ -205,10 +203,7 @@ const SettingClash = ({ onError }: Props) => {
/> />
</SettingItem> </SettingItem>
<SettingItem <SettingItem label={t('settings.sections.clash.form.fields.ipv6')}>
data-setting-target="settings.sections.clash.form.fields.ipv6"
label={t('settings.sections.clash.form.fields.ipv6')}
>
<GuardState <GuardState
value={ipv6 ?? false} value={ipv6 ?? false}
valueProps="checked" valueProps="checked"
@@ -222,7 +217,6 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.unifiedDelay"
label={t('settings.sections.clash.form.fields.unifiedDelay')} label={t('settings.sections.clash.form.fields.unifiedDelay')}
extra={ extra={
<TooltipIcon <TooltipIcon
@@ -244,7 +238,6 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.logLevel"
label={t('settings.sections.clash.form.fields.logLevel')} label={t('settings.sections.clash.form.fields.logLevel')}
extra={ extra={
<TooltipIcon <TooltipIcon
@@ -286,10 +279,7 @@ const SettingClash = ({ onError }: Props) => {
</GuardState> </GuardState>
</SettingItem> </SettingItem>
<SettingItem <SettingItem label={t('settings.sections.clash.form.fields.portConfig')}>
data-setting-target="settings.sections.clash.form.fields.portConfig"
label={t('settings.sections.clash.form.fields.portConfig')}
>
<TextField <TextField
autoComplete="new-password" autoComplete="new-password"
disabled={false} disabled={false}
@@ -304,7 +294,6 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.external"
label={t('settings.sections.clash.form.fields.external')} label={t('settings.sections.clash.form.fields.external')}
extra={ extra={
<TooltipIcon <TooltipIcon
@@ -322,13 +311,11 @@ const SettingClash = ({ onError }: Props) => {
/> />
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.webUI"
onClick={() => webRef.current?.open()} onClick={() => webRef.current?.open()}
label={t('settings.sections.clash.form.fields.webUI')} label={t('settings.sections.clash.form.fields.webUI')}
/> />
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.clashCore"
label={t('settings.sections.clash.form.fields.clashCore')} label={t('settings.sections.clash.form.fields.clashCore')}
extra={ extra={
<TooltipIcon <TooltipIcon
@@ -342,7 +329,6 @@ const SettingClash = ({ onError }: Props) => {
{isWIN && ( {isWIN && (
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.openUwpTool"
onClick={invoke_uwp_tool} onClick={invoke_uwp_tool}
label={t('settings.sections.clash.form.fields.openUwpTool')} label={t('settings.sections.clash.form.fields.openUwpTool')}
extra={ extra={
@@ -355,13 +341,11 @@ const SettingClash = ({ onError }: Props) => {
)} )}
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.updateGeoData"
onClick={onUpdateGeo} onClick={onUpdateGeo}
label={t('settings.sections.clash.form.fields.updateGeoData')} label={t('settings.sections.clash.form.fields.updateGeoData')}
/> />
<SettingItem <SettingItem
data-setting-target="settings.sections.clash.form.fields.tunnels.title"
label={t('settings.sections.clash.form.fields.tunnels.title')} label={t('settings.sections.clash.form.fields.tunnels.title')}
onClick={() => tunnelRef.current?.open()} onClick={() => tunnelRef.current?.open()}
/> />
+9 -18
View File
@@ -1,4 +1,3 @@
import { Box } from '@mui/material'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -30,24 +29,17 @@ const SettingSystem = ({ onError }: Props) => {
return ( return (
<SettingList title={t('settings.sections.system.title')}> <SettingList title={t('settings.sections.system.title')}>
<Box data-setting-target="settings.sections.system.toggles.tunMode"> <ProxyControlSwitches
<ProxyControlSwitches label={t('settings.sections.system.toggles.tunMode')}
label={t('settings.sections.system.toggles.tunMode')} onError={onError}
onError={onError} />
/>
</Box>
<Box data-setting-target="settings.sections.system.toggles.systemProxy"> <ProxyControlSwitches
<ProxyControlSwitches label={t('settings.sections.system.toggles.systemProxy')}
label={t('settings.sections.system.toggles.systemProxy')} onError={onError}
onError={onError} />
/>
</Box>
<SettingItem <SettingItem label={t('settings.sections.system.fields.autoLaunch')}>
data-setting-target="settings.sections.system.fields.autoLaunch"
label={t('settings.sections.system.fields.autoLaunch')}
>
<GuardState <GuardState
value={enable_auto_launch ?? false} value={enable_auto_launch ?? false}
valueProps="checked" valueProps="checked"
@@ -72,7 +64,6 @@ const SettingSystem = ({ onError }: Props) => {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.sections.system.fields.silentStart"
label={t('settings.sections.system.fields.silentStart')} label={t('settings.sections.system.fields.silentStart')}
extra={ extra={
<TooltipIcon <TooltipIcon
@@ -90,7 +90,6 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
<LiteModeViewer ref={liteModeRef} /> <LiteModeViewer ref={liteModeRef} />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.backupSetting"
onClick={() => backupRef.current?.open()} onClick={() => backupRef.current?.open()}
label={t('settings.components.verge.advanced.fields.backupSetting')} label={t('settings.components.verge.advanced.fields.backupSetting')}
extra={ extra={
@@ -102,13 +101,11 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.runtimeConfig"
onClick={() => configRef.current?.open()} onClick={() => configRef.current?.open()}
label={t('settings.components.verge.advanced.fields.runtimeConfig')} label={t('settings.components.verge.advanced.fields.runtimeConfig')}
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.openConfDir"
onClick={openAppDir} onClick={openAppDir}
label={t('settings.components.verge.advanced.fields.openConfDir')} label={t('settings.components.verge.advanced.fields.openConfDir')}
extra={ extra={
@@ -120,25 +117,21 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.openCoreDir"
onClick={openCoreDir} onClick={openCoreDir}
label={t('settings.components.verge.advanced.fields.openCoreDir')} label={t('settings.components.verge.advanced.fields.openCoreDir')}
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.openLogsDir"
onClick={openLogsDir} onClick={openLogsDir}
label={t('settings.components.verge.advanced.fields.openLogsDir')} label={t('settings.components.verge.advanced.fields.openLogsDir')}
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.checkUpdates"
onClick={onCheckUpdate} onClick={onCheckUpdate}
label={t('settings.components.verge.advanced.fields.checkUpdates')} label={t('settings.components.verge.advanced.fields.checkUpdates')}
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.openDevTools"
onClick={() => { onClick={() => {
void mutate(() => openDevTools(), { void mutate(() => openDevTools(), {
id: 'open-devtools', id: 'open-devtools',
@@ -149,7 +142,6 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.liteModeSettings"
label={t('settings.components.verge.advanced.fields.liteModeSettings')} label={t('settings.components.verge.advanced.fields.liteModeSettings')}
extra={ extra={
<TooltipIcon <TooltipIcon
@@ -161,7 +153,6 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.exit"
onClick={() => { onClick={() => {
void mutate(() => exitApp(), { void mutate(() => exitApp(), {
id: 'exit-app', id: 'exit-app',
@@ -172,7 +163,6 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.exportDiagnostics"
label={t('settings.components.verge.advanced.fields.exportDiagnostics')} label={t('settings.components.verge.advanced.fields.exportDiagnostics')}
extra={ extra={
<TooltipIcon <TooltipIcon
@@ -183,7 +173,6 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
></SettingItem> ></SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.components.verge.advanced.fields.vergeVersion"
label={t('settings.components.verge.advanced.fields.vergeVersion')} label={t('settings.components.verge.advanced.fields.vergeVersion')}
extra={ extra={
<TooltipIcon <TooltipIcon
+1 -14
View File
@@ -100,10 +100,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
<UpdateViewer ref={updateRef} /> <UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} /> <BackupViewer ref={backupRef} />
<SettingItem <SettingItem label={t('settings.components.verge.basic.fields.language')}>
data-setting-target="settings.components.verge.basic.fields.language"
label={t('settings.components.verge.basic.fields.language')}
>
<GuardState <GuardState
value={language ?? 'en'} value={language ?? 'en'}
onCatch={onError} onCatch={onError}
@@ -122,7 +119,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.components.verge.basic.fields.themeMode"
label={t('settings.components.verge.basic.fields.themeMode')} label={t('settings.components.verge.basic.fields.themeMode')}
> >
<GuardState <GuardState
@@ -137,7 +133,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
{OS !== 'linux' && ( {OS !== 'linux' && (
<SettingItem <SettingItem
data-setting-target="settings.components.verge.basic.fields.trayClickEvent"
label={t('settings.components.verge.basic.fields.trayClickEvent')} label={t('settings.components.verge.basic.fields.trayClickEvent')}
> >
<GuardState <GuardState
@@ -171,7 +166,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
)} )}
<SettingItem <SettingItem
data-setting-target="settings.components.verge.basic.fields.copyEnvType"
label={t('settings.components.verge.basic.fields.copyEnvType')} label={t('settings.components.verge.basic.fields.copyEnvType')}
extra={ extra={
<TooltipIcon icon={ContentCopyRounded} onClick={onCopyClashEnv} /> <TooltipIcon icon={ContentCopyRounded} onClick={onCopyClashEnv} />
@@ -195,7 +189,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.components.verge.basic.fields.startPage"
label={t('settings.components.verge.basic.fields.startPage')} label={t('settings.components.verge.basic.fields.startPage')}
> >
<GuardState <GuardState
@@ -218,7 +211,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.components.verge.basic.fields.startupScript"
label={t('settings.components.verge.basic.fields.startupScript')} label={t('settings.components.verge.basic.fields.startupScript')}
> >
<GuardState <GuardState
@@ -236,7 +228,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
endAdornment={ endAdornment={
<> <>
<Button <Button
data-setting-target="settings.components.verge.basic.actions.browse"
onClick={async () => { onClick={async () => {
const selected = await open({ const selected = await open({
directory: false, directory: false,
@@ -273,25 +264,21 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
data-setting-target="settings.components.verge.basic.fields.themeSetting"
onClick={() => themeRef.current?.open()} onClick={() => themeRef.current?.open()}
label={t('settings.components.verge.basic.fields.themeSetting')} label={t('settings.components.verge.basic.fields.themeSetting')}
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.basic.fields.layoutSetting"
onClick={() => layoutRef.current?.open()} onClick={() => layoutRef.current?.open()}
label={t('settings.components.verge.basic.fields.layoutSetting')} label={t('settings.components.verge.basic.fields.layoutSetting')}
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.basic.fields.misc"
onClick={() => miscRef.current?.open()} onClick={() => miscRef.current?.open()}
label={t('settings.components.verge.basic.fields.misc')} label={t('settings.components.verge.basic.fields.misc')}
/> />
<SettingItem <SettingItem
data-setting-target="settings.components.verge.basic.fields.hotkeySetting"
onClick={() => hotkeyRef.current?.open()} onClick={() => hotkeyRef.current?.open()}
label={t('settings.components.verge.basic.fields.hotkeySetting')} label={t('settings.components.verge.basic.fields.hotkeySetting')}
/> />
-645
View File
@@ -1,645 +0,0 @@
import type { TranslationKey } from '@/types/generated/i18n-keys'
export interface SettingEntry {
id: string
name: TranslationKey
aliases: string
section: TranslationKey
path: TranslationKey
prerequisite?: string
target?: string
fallback?: string
dialog?: string
platforms?: readonly string[]
}
type GroupDefaults = Pick<SettingEntry, 'section' | 'path'> &
Partial<
Pick<SettingEntry, 'dialog' | 'fallback' | 'prerequisite' | 'aliases'>
>
type EntryOptions = Partial<Omit<SettingEntry, 'name'>>
type GroupEntries = Partial<Record<TranslationKey, string | EntryOptions>>
function group(defaults: GroupDefaults, entries: GroupEntries): SettingEntry[] {
return (
Object.entries(entries) as [TranslationKey, string | EntryOptions][]
).map(([name, value]) => {
const options = typeof value === 'string' ? { aliases: value } : value
return {
...defaults,
...options,
id: options.id ?? name,
name,
aliases: [options.aliases, defaults.aliases].filter(Boolean).join(' '),
}
})
}
const system = {
section: 'settings.sections.system.title',
path: 'settings.sections.system.title',
} satisfies GroupDefaults
const tun = {
section: 'settings.sections.system.title',
path: 'settings.modals.tun.title',
dialog: 'tun-viewer',
} satisfies GroupDefaults
const sysproxy = {
section: 'settings.sections.system.title',
path: 'settings.modals.sysproxy.title',
dialog: 'sysproxy-viewer',
fallback: 'settings.modals.sysproxy.fields.usePacMode',
prerequisite: 'sysproxy-status',
aliases: '代理 PAC bypass 绕过',
} satisfies GroupDefaults
const clash = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.title',
} satisfies GroupDefaults
const networkInterface = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.title',
dialog: 'network-interface-viewer',
aliases: 'network interface viewer',
} satisfies GroupDefaults
const dns = {
section: 'settings.sections.clash.title',
path: 'settings.modals.dns.dialog.title',
dialog: 'dns-viewer',
fallback: 'dns-mode',
aliases: 'DNS 域名解析 DNS 域名解析',
} satisfies GroupDefaults
const dnsReset = {
section: 'settings.sections.clash.title',
path: 'settings.modals.dns.dialog.title',
dialog: 'dns-viewer',
aliases: 'dns viewer reset DNS 域名解析',
} satisfies GroupDefaults
const clashPort = {
section: 'settings.sections.clash.title',
path: 'settings.modals.clashPort.title',
dialog: 'clash-port-viewer',
} satisfies GroupDefaults
const controller = {
section: 'settings.sections.clash.title',
path: 'settings.sections.externalController.title',
dialog: 'controller-viewer',
} satisfies GroupDefaults
const externalControllerCors = {
section: 'settings.sections.clash.title',
path: 'settings.sections.externalCors.title',
dialog: 'external-controller-cors',
} satisfies GroupDefaults
const webUi = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.title',
dialog: 'web-ui-viewer',
aliases: 'web ui viewer',
} satisfies GroupDefaults
const webUiActions = {
section: 'settings.sections.clash.title',
path: 'settings.modals.webUI.title',
dialog: 'web-ui-viewer',
fallback: 'web-ui-viewer',
} satisfies GroupDefaults
const clashCore = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.form.fields.clashCore',
dialog: 'clash-core-viewer',
fallback: 'core-variants',
} satisfies GroupDefaults
const coreVariants = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.title',
dialog: 'clash-core-viewer',
aliases: 'core variants',
} satisfies GroupDefaults
const tunnels = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.form.fields.tunnels.title',
dialog: 'tunnels-viewer',
fallback: 'tunnels-expand',
} satisfies GroupDefaults
const basic = {
section: 'settings.components.verge.basic.title',
path: 'settings.components.verge.basic.title',
} satisfies GroupDefaults
const theme = {
section: 'settings.components.verge.basic.title',
path: 'settings.components.verge.theme.title',
dialog: 'theme-viewer',
} satisfies GroupDefaults
const layout = {
section: 'settings.components.verge.basic.title',
path: 'settings.components.verge.layout.title',
dialog: 'layout-viewer',
aliases: 'browse layout 布局 托盘',
} satisfies GroupDefaults
const layoutFields = {
section: 'settings.components.verge.basic.title',
path: 'settings.components.verge.basic.fields.layoutSetting',
dialog: 'layout-viewer',
aliases: 'layout 布局 托盘',
} satisfies GroupDefaults
const misc = {
section: 'settings.components.verge.basic.title',
path: 'settings.modals.misc.title',
dialog: 'misc-viewer',
aliases: 'misc 杂项',
} satisfies GroupDefaults
const hotkey = {
section: 'settings.components.verge.basic.title',
path: 'settings.modals.hotkey.title',
dialog: 'hotkey-viewer',
} satisfies GroupDefaults
const advanced = {
section: 'settings.components.verge.advanced.title',
path: 'settings.components.verge.advanced.title',
} satisfies GroupDefaults
const backup = {
section: 'settings.components.verge.advanced.title',
path: 'settings.modals.backup.title',
dialog: 'backup-viewer',
aliases: 'backup 备份',
} satisfies GroupDefaults
const backupHistory = {
section: 'settings.components.verge.advanced.title',
path: 'settings.modals.backup.title',
dialog: 'backup-history',
fallback: 'backup-history',
} satisfies GroupDefaults
const backupWebdav = {
section: 'settings.components.verge.advanced.title',
path: 'settings.modals.backup.webdav.title',
dialog: 'backup-webdav',
fallback: 'webdav-state',
} satisfies GroupDefaults
const liteMode = {
section: 'settings.components.verge.advanced.title',
path: 'settings.modals.liteMode.title',
dialog: 'lite-mode-viewer',
} satisfies GroupDefaults
const settingsCatalog: readonly SettingEntry[] = [
...group(system, {
'settings.sections.system.toggles.tunMode': 'TUN 虚拟网卡 vpn',
'settings.sections.proxyControl.actions.uninstallService': {
id: 'service-uninstall',
fallback: 'settings.sections.system.toggles.tunMode',
aliases: 'service uninstall',
},
}),
...group(tun, {
'settings.modals.tun.fields.stack': 'stack TUN 虚拟网卡',
'settings.modals.tun.fields.dnsMode': 'dns mode TUN 虚拟网卡',
'settings.modals.tun.fields.device': 'device TUN 虚拟网卡',
'settings.modals.tun.fields.autoRoute': 'auto route TUN 虚拟网卡',
'settings.modals.tun.fields.autoRedirect': {
aliases: 'auto redirect TUN 虚拟网卡',
prerequisite: 'settings.modals.tun.fields.autoRoute',
platforms: ['linux'],
},
'settings.modals.tun.fields.strictRoute': 'strict route TUN 虚拟网卡',
'settings.modals.tun.fields.autoDetectInterface':
'auto detect interface TUN 虚拟网卡',
'settings.modals.tun.fields.dnsHijack': 'dns hijack TUN 虚拟网卡',
'settings.modals.tun.fields.mtu': 'mtu TUN 虚拟网卡',
'settings.modals.tun.fields.routeExcludeAddress': {
aliases: 'route exclude address TUN 虚拟网卡',
prerequisite: 'settings.modals.tun.fields.autoRoute',
},
'shared.actions.resetToDefault': {
id: 'tun-viewer-reset',
aliases: 'tun viewer reset',
},
}),
...group(system, {
'settings.sections.system.toggles.systemProxy': 'system proxy 系统代理',
}),
...group(sysproxy, {
'settings.modals.sysproxy.fields.proxyHost': 'proxy host',
'settings.modals.sysproxy.fields.usePacMode': 'use pac mode',
'settings.modals.sysproxy.fields.proxyGuard': 'proxy guard',
'settings.modals.sysproxy.fields.guardDuration': 'guard duration',
'settings.modals.sysproxy.fields.alwaysUseDefaultBypass':
'always use default bypass',
'settings.modals.sysproxy.fields.enableBypassCheck': 'enable bypass check',
'settings.modals.sysproxy.fields.proxyBypass': 'proxy bypass',
'settings.modals.sysproxy.fields.pacScriptContent': 'pac script content',
'settings.modals.sysproxy.actions.editPac': 'edit pac',
}),
...group(system, {
'settings.sections.system.fields.autoLaunch': 'auto launch ',
'settings.sections.system.fields.silentStart': 'silent start ',
}),
...group(clash, {
'settings.sections.clash.form.fields.allowLan': 'allow lan ',
}),
...group(networkInterface, {
'settings.modals.networkInterface.title': {
id: 'network-interface-viewer',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.dnsOverwrite': 'dns overwrite ',
}),
...group(dns, {
'settings.modals.dns.fields.enable': 'enable',
'settings.modals.dns.fields.listen': 'listen',
'settings.modals.dns.fields.enhancedMode': 'enhancedMode',
'settings.modals.dns.fields.fakeIpRange': 'fakeIpRange',
'settings.modals.dns.fields.fakeIpRange6': 'fakeIpRange6',
'settings.modals.dns.fields.fakeIpFilterMode': 'fakeIpFilterMode',
'settings.modals.dns.fields.ipv6.label': 'label',
'settings.modals.dns.fields.preferH3.label': 'label',
'settings.modals.dns.fields.respectRules.label': 'label',
'settings.modals.dns.fields.useHosts.label': 'label',
'settings.modals.dns.fields.useSystemHosts.label': 'label',
'settings.modals.dns.fields.directPolicy.label': 'label',
'settings.modals.dns.fields.defaultNameserver.label': 'label',
'settings.modals.dns.fields.nameserver.label': 'label',
'settings.modals.dns.fields.fallback.label': 'label',
'settings.modals.dns.fields.proxy.label': 'label',
'settings.modals.dns.fields.directNameserver.label': 'label',
'settings.modals.dns.fields.fakeIpFilter.label': 'label',
'settings.modals.dns.fields.nameserverPolicy.label': 'label',
'settings.modals.dns.fields.geoipFiltering.label': 'label',
'settings.modals.dns.fields.geoipCode': 'geoipCode',
'settings.modals.dns.fields.fallbackIpCidr.label': 'label',
'settings.modals.dns.fields.fallbackDomain.label': 'label',
'settings.modals.dns.fields.hosts.label': 'label',
}),
...group(dnsReset, {
'shared.actions.resetToDefault': {
id: 'dns-viewer-reset',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.ipv6': 'ipv6 ',
'settings.sections.clash.form.fields.unifiedDelay': 'unified delay ',
'settings.sections.clash.form.fields.logLevel': 'log level ',
'settings.sections.clash.form.fields.portConfig': 'port config ',
}),
...group(clashPort, {
'settings.modals.clashPort.fields.socks': 'clash port viewer',
'settings.modals.clashPort.fields.http': 'clash port viewer',
'settings.modals.clashPort.fields.redir': {
aliases: 'clash port viewer',
platforms: ['macos', 'linux'],
},
'settings.modals.clashPort.fields.tproxy': {
aliases: 'clash port viewer',
platforms: ['linux'],
},
'settings.modals.clashPort.fields.mixed': 'clash port viewer',
'settings.modals.clashPort.actions.random': {
id: 'port-random',
aliases: 'port random',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.external': 'external ',
}),
...group(controller, {
'settings.sections.externalController.fields.enable': 'enable ',
'settings.sections.externalController.fields.address': {
aliases: 'address ',
prerequisite: 'settings.sections.externalController.fields.enable',
},
'settings.sections.externalController.fields.secret': {
aliases: 'secret ',
prerequisite: 'settings.sections.externalController.fields.enable',
},
}),
...group(externalControllerCors, {
'settings.sections.externalCors.fields.allowPrivateNetwork':
'allow private network ',
'settings.sections.externalCors.fields.allowedOrigins': 'allowed origins ',
'settings.sections.externalCors.actions.add': 'add ',
}),
...group(clash, {
'settings.sections.clash.form.fields.webUI': 'web ui ',
}),
...group(webUi, {
'settings.modals.webUI.title': {
id: 'web-ui-viewer',
},
}),
...group(webUiActions, {
'shared.actions.new': {
id: 'web-ui-new',
aliases: 'web ui new',
},
'shared.actions.edit': {
id: 'web-ui-edit',
aliases: 'web ui edit',
},
'shared.actions.delete': {
id: 'web-ui-delete',
aliases: 'web ui delete',
},
'settings.modals.webUI.actions.openUrl': {
id: 'web-ui-open',
aliases: 'web ui open',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.clashCore': 'clash core ',
}),
...group(clashCore, {
'shared.actions.upgrade': {
id: 'core-upgrade',
aliases: 'core upgrade',
},
'shared.actions.restart': {
id: 'core-restart',
aliases: 'core restart',
},
}),
...group(coreVariants, {
'settings.sections.clash.form.fields.clashCore': {
id: 'core-variants',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.openUwpTool': {
aliases: 'open uwp tool ',
platforms: ['windows'],
},
'settings.sections.clash.form.fields.updateGeoData': 'update geo data ',
'settings.sections.clash.form.fields.tunnels.title': 'title ',
}),
...group(tunnels, {
'settings.sections.clash.form.fields.tunnels.existing': 'existing ',
'settings.sections.clash.form.fields.tunnels.default': 'default ',
'settings.sections.clash.form.fields.tunnels.protocols': 'protocols ',
'settings.sections.clash.form.fields.tunnels.localAddr': 'local addr ',
'settings.sections.clash.form.fields.tunnels.localPort': 'local port ',
'settings.sections.clash.form.fields.tunnels.targetAddr': 'target addr ',
'settings.sections.clash.form.fields.tunnels.targetPort': 'target port ',
'settings.sections.clash.form.fields.tunnels.proxyGroup': 'proxy group ',
'settings.sections.clash.form.fields.tunnels.proxyNode': 'proxy node ',
'settings.sections.clash.form.fields.tunnels.actions.add': 'add ',
}),
...group(basic, {
'settings.components.verge.basic.fields.language': 'language ',
'settings.components.verge.basic.fields.themeMode': 'theme mode ',
'settings.components.verge.basic.fields.trayClickEvent': {
aliases: 'tray click event ',
platforms: ['windows', 'macos'],
},
'settings.components.verge.basic.fields.copyEnvType': 'copy env type ',
'settings.components.verge.basic.fields.startPage': 'start page ',
'settings.components.verge.basic.fields.startupScript': 'startup script ',
'settings.components.verge.basic.fields.themeSetting': 'theme setting ',
}),
...group(theme, {
'settings.components.verge.theme.actions.editCss':
'edit css theme 主题 外观',
'settings.components.verge.theme.fields.fontFamily':
'fontFamily theme 主题 外观',
'settings.components.verge.theme.fields.cssInjection':
'cssInjection theme 主题 外观',
'settings.components.verge.theme.fields.primaryColor': 'theme viewer',
'settings.components.verge.theme.fields.secondaryColor': 'theme viewer',
'settings.components.verge.theme.fields.primaryText': 'theme viewer',
'settings.components.verge.theme.fields.secondaryText': 'theme viewer',
'settings.components.verge.theme.fields.infoColor': 'theme viewer',
'settings.components.verge.theme.fields.warningColor': 'theme viewer',
'settings.components.verge.theme.fields.errorColor': 'theme viewer',
'settings.components.verge.theme.fields.successColor': 'theme viewer',
}),
...group(basic, {
'settings.components.verge.basic.fields.layoutSetting': 'layout setting ',
}),
...group(layout, {
'settings.components.verge.basic.actions.browse': '',
}),
...group(layoutFields, {
'settings.components.verge.layout.fields.preferSystemTitlebar':
'preferSystemTitlebar',
'settings.components.verge.layout.fields.trafficGraph': 'trafficGraph',
'settings.components.verge.layout.fields.memoryUsage': 'memoryUsage',
'settings.components.verge.layout.fields.proxyGroupIcon': 'proxyGroupIcon',
'settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur':
'pauseRenderTrafficStatsOnBlur',
'settings.components.verge.layout.fields.toastPosition': 'toastPosition',
'settings.components.verge.layout.fields.hoverNavigator': 'hoverNavigator',
'settings.components.verge.layout.fields.hoverNavigatorDelay':
'hoverNavigatorDelay',
'settings.components.verge.layout.fields.navIcon': 'navIcon',
'settings.components.verge.layout.fields.collapseNavBar': 'collapseNavBar',
'settings.components.verge.layout.fields.trayIcon': {
aliases: 'trayIcon',
platforms: ['macos'],
},
'settings.components.verge.layout.fields.enableTraySpeed': {
aliases: 'enableTraySpeed',
platforms: ['macos'],
},
'settings.components.verge.layout.fields.proxyGroupsDisplayMode':
'proxyGroupsDisplayMode',
'settings.components.verge.layout.fields.showOutboundModesInline':
'showOutboundModesInline',
'settings.components.verge.layout.fields.commonTrayIcon': 'commonTrayIcon',
'settings.components.verge.layout.fields.systemProxyTrayIcon':
'systemProxyTrayIcon',
'settings.components.verge.layout.fields.tunTrayIcon': 'tunTrayIcon',
}),
...group(basic, {
'settings.components.verge.basic.fields.misc': 'misc ',
}),
...group(misc, {
'settings.modals.misc.fields.appLogLevel': 'app log level',
'settings.modals.misc.fields.appLogMaxSize': 'app log max size',
'settings.modals.misc.fields.appLogMaxCount': 'app log max count',
'settings.modals.misc.fields.autoCloseConnections':
'auto close connections',
'settings.modals.misc.fields.autoCheckUpdate': 'auto check update',
'settings.modals.misc.fields.enableBuiltinEnhanced':
'enable builtin enhanced',
'settings.modals.misc.fields.proxyLayoutColumns': 'proxy layout columns',
'settings.modals.misc.fields.autoLogClean': 'auto log clean',
'settings.modals.misc.fields.autoDelayDetection': 'auto delay detection',
'settings.modals.misc.fields.autoDelayDetectionInterval': {
aliases: 'auto delay detection interval',
prerequisite: 'settings.modals.misc.fields.autoDelayDetection',
},
'settings.modals.misc.fields.defaultLatencyTest': 'default latency test',
'settings.modals.misc.fields.defaultLatencyTimeout':
'default latency timeout',
}),
...group(basic, {
'settings.components.verge.basic.fields.hotkeySetting': 'hotkey setting ',
}),
...group(hotkey, {
'settings.modals.hotkey.functions.openOrCloseDashboard': 'hotkey viewer',
'settings.modals.hotkey.functions.rule': 'hotkey viewer',
'settings.modals.hotkey.functions.global': 'hotkey viewer',
'settings.modals.hotkey.functions.direct': 'hotkey viewer',
'settings.modals.hotkey.functions.toggleSystemProxy': 'hotkey viewer',
'settings.modals.hotkey.functions.toggleTunMode': 'hotkey viewer',
'settings.modals.hotkey.functions.entryLightweightMode': 'hotkey viewer',
'settings.modals.hotkey.functions.reactivateProfiles': 'hotkey viewer',
'settings.modals.hotkey.toggles.enableGlobal':
'settings.modals.hotkey.toggles.enableGlobal',
}),
...group(advanced, {
'settings.components.verge.advanced.fields.backupSetting':
'backup setting ',
}),
...group(backup, {
'settings.modals.backup.actions.backup': 'backup',
}),
...group(backupHistory, {
'settings.modals.backup.actions.viewHistory': {
aliases: 'view history backup 备份',
target: 'backup-history',
},
}),
...group(backup, {
'settings.modals.backup.actions.importBackup': 'import backup',
}),
...group(backupWebdav, {
'settings.modals.backup.fields.webdavUrl': 'webdavUrl WebDAV 云备份',
'settings.modals.backup.fields.username': 'username WebDAV 云备份',
}),
...group(backup, {
'settings.modals.backup.auto.scheduleLabel': 'scheduleLabel',
'settings.modals.backup.auto.intervalLabel': {
aliases: 'intervalLabel',
prerequisite: 'settings.modals.backup.auto.scheduleLabel',
},
'settings.modals.backup.auto.changeLabel': 'changeLabel',
}),
...group(backupHistory, {
'settings.modals.backup.history.title': {
aliases: '备份 历史 恢复 删除 restore delete history',
target: 'backup-history',
},
}),
...group(backupWebdav, {
'shared.labels.password': {
id: 'webdav-password',
aliases: 'webdav password',
},
'shared.actions.save': {
id: 'webdav-save',
aliases: 'webdav save',
},
'settings.modals.backup.actions.backup': {
id: 'webdav-backup',
aliases: 'webdav backup',
},
'shared.actions.refresh': {
id: 'webdav-refresh',
aliases: 'webdav refresh',
},
}),
...group(backupHistory, {
'settings.modals.backup.actions.deleteBackup': {
id: 'backup-history-delete',
aliases: 'backup history delete',
path: 'settings.modals.backup.history.title',
},
'settings.modals.backup.actions.restoreBackup': {
id: 'backup-history-restore',
aliases: 'backup history restore',
path: 'settings.modals.backup.history.title',
},
'settings.modals.backup.actions.exportBackup': {
id: 'backup-history-export',
aliases: 'backup history export',
path: 'settings.modals.backup.history.title',
},
}),
...group(advanced, {
'settings.components.verge.advanced.fields.runtimeConfig':
'runtime config ',
'settings.components.verge.advanced.fields.openConfDir': 'open conf dir ',
'settings.components.verge.advanced.fields.openCoreDir': 'open core dir ',
'settings.components.verge.advanced.fields.openLogsDir': 'open logs dir ',
'settings.components.verge.advanced.fields.checkUpdates': 'check updates ',
'settings.components.verge.advanced.fields.openDevTools': 'open dev tools ',
'settings.components.verge.advanced.fields.liteModeSettings':
'lite mode settings ',
}),
...group(liteMode, {
'settings.modals.liteMode.actions.enterNow': 'enter now ',
'settings.modals.liteMode.toggles.autoEnter': 'auto enter ',
'settings.modals.liteMode.fields.delay': {
fallback: 'settings.modals.liteMode.toggles.autoEnter',
aliases: 'delay ',
},
}),
...group(advanced, {
'settings.components.verge.advanced.fields.exit': 'exit ',
'settings.components.verge.advanced.fields.exportDiagnostics':
'export diagnostics ',
'settings.components.verge.advanced.fields.vergeVersion': 'verge version ',
}),
]
export function searchSettings(
query: string,
platform: string,
translate: (key: TranslationKey) => string,
english: (key: TranslationKey) => string,
) {
const words = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)
if (!words.length) return []
return settingsCatalog
.filter((e) => !e.platforms || e.platforms.includes(platform))
.map((entry, order) => {
const names = (
translate(entry.name) +
' ' +
english(entry.name)
).toLocaleLowerCase()
const other = (
translate(entry.section) +
' ' +
english(entry.section) +
' ' +
entry.aliases +
' ' +
translate(entry.path) +
' ' +
english(entry.path)
).toLocaleLowerCase()
const score = words.every(
(word) => names.includes(word) || other.includes(word),
)
? words.reduce((sum, word) => sum + (names.includes(word) ? 1 : 0), 0)
: -1
return { entry, order, score }
})
.filter((e) => e.score >= 0)
.sort((a, b) => b.score - a.score || a.order - b.order)
.map((e) => e.entry)
}
@@ -1,42 +0,0 @@
import {
createContext,
use,
useEffect,
useImperativeHandle,
type Ref,
} from 'react'
import type { DialogRef } from '@/components/base'
export interface SearchDialog {
id: string
open: () => void
isOpen: boolean
}
export const SettingsSearchContext = createContext<Set<SearchDialog> | null>(
null,
)
export function useSettingDialog(
ref: Ref<DialogRef> | undefined,
id: string,
open: boolean,
create: () => DialogRef,
) {
useImperativeHandle(ref, create)
const registry = use(SettingsSearchContext)
useEffect(() => {
if (!registry) return
const handle = {
id,
isOpen: open,
open: () => {
if (!open) create().open()
},
}
registry.add(handle)
return () => {
registry.delete(handle)
}
})
}
-317
View File
@@ -1,317 +0,0 @@
import { CloseRounded } from '@mui/icons-material'
import {
GlobalStyles,
Box,
Button,
IconButton,
List,
ListItemButton,
ListItemText,
TextField,
Typography,
} from '@mui/material'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import getSystem from '@/utils/get-system'
import { searchSettings, type SettingEntry } from './settings-catalog'
import {
SettingsSearchContext,
type SearchDialog,
} from './settings-search-context'
export function SettingsSearch({
children,
}: {
children: (
header: ReactNode,
results: ReactNode,
hidden: boolean,
) => ReactNode
}) {
const { t } = useTranslation()
const [registry] = useState(() => new Set<SearchDialog>())
const [query, setQuery] = useState('')
const [selected, setSelected] = useState(0)
const [showPage, setShowPage] = useState(false)
const rootRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const originalScrollRef = useRef(0)
const resultScrollRef = useRef(0)
const cancelLocateRef = useRef<() => void>(() => {})
const results = searchSettings(
query,
getSystem(),
(key) => t(key),
(key) => t(key, { lng: 'en' }),
)
const searching = !!query.trim()
const scroller = () =>
rootRef.current?.querySelector('.base-container > section')
const restoreResults = () => {
cancelLocateRef.current()
setShowPage(false)
requestAnimationFrame(() => {
scroller()?.scrollTo({ top: resultScrollRef.current })
inputRef.current?.focus()
})
}
const changeQuery = (value: string) => {
cancelLocateRef.current()
if (!searching && value.trim())
originalScrollRef.current = scroller()?.scrollTop ?? 0
setQuery(value)
setSelected(0)
setShowPage(false)
resultScrollRef.current = 0
requestAnimationFrame(() =>
scroller()?.scrollTo({
top: value.trim() ? 0 : originalScrollRef.current,
}),
)
}
const locate = (entry: SettingEntry) => {
cancelLocateRef.current()
if (!showPage) resultScrollRef.current = scroller()?.scrollTop ?? 0
if (entry.dialog) {
const handles = [...registry].filter(
(handle) => handle.id === entry.dialog,
)
;(handles.find((handle) => handle.isOpen) ?? handles[0])?.open()
} else setShowPage(true)
let highlighted: HTMLElement | null = null
let stopped = false
let sawScope = false
let temporaryTabIndex = false
const cleanup = () => {
stopped = true
observer.disconnect()
clearTimeout(timeout)
highlighted?.removeAttribute('data-search-highlight')
if (temporaryTabIndex) highlighted?.removeAttribute('tabindex')
}
const find = (timedOut = false) => {
if (stopped) return
const marker = entry.dialog
? [
...document.querySelectorAll(
`[data-settings-dialog="${entry.dialog}"]`,
),
].find(
(marker) =>
marker.closest('[role="dialog"]')?.getClientRects().length,
)
: rootRef.current
const scope = entry.dialog ? marker?.closest('[role="dialog"]') : marker
if (!scope) {
if (sawScope) {
cleanup()
inputRef.current?.focus()
}
return
}
if (highlighted) return
sawScope = true
const pending = marker?.getAttribute('data-search-ready') === 'false'
if (pending && !timedOut) return
let target = pending
? (scope.querySelector<HTMLElement>('[data-settings-loading]') ??
(marker instanceof HTMLElement ? marker : null))
: scope.querySelector<HTMLElement>(
`[data-setting-target="${entry.target ?? entry.id}"]`,
)
const fields = target
? [
...target.querySelectorAll<HTMLElement>(
'input,textarea,select,[role="combobox"]',
),
]
: []
const disabled =
target?.matches(':disabled,[aria-disabled="true"]') ||
(fields.length > 0 &&
fields.every((field) =>
field.matches(':disabled,[aria-disabled="true"]'),
))
let fallback = pending
if (!target || !target.getClientRects().length || disabled) {
const fallbackId = disabled
? (entry.prerequisite ?? entry.fallback)
: entry.fallback
if (fallbackId) {
target = scope.querySelector<HTMLElement>(
`[data-setting-target="${fallbackId}"]`,
)
fallback = !disabled || !entry.prerequisite
}
}
if (!target || !target.getClientRects().length) return
highlighted = target
target.setAttribute('data-search-highlight', 'true')
target.scrollIntoView({ block: 'center' })
const targetFocusable = target.matches(
'button,input,textarea,select,a[href],[tabindex]',
)
const control =
!fallback && !targetFocusable
? [
...target.querySelectorAll<HTMLElement>(
'input:not(:disabled),textarea:not(:disabled),button:not(:disabled),[role="combobox"],a[href]',
),
].find(
(element) =>
element.getClientRects().length &&
element.getAttribute('aria-disabled') !== 'true',
)
: null
temporaryTabIndex = !control && !targetFocusable
if (temporaryTabIndex) target.tabIndex = -1
;(control ?? target).focus({ preventScroll: true })
}
const observer = new MutationObserver(() => find())
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class', 'data-search-ready', 'disabled'],
})
const timeout = setTimeout(() => {
if (!highlighted) {
find(true)
if (!highlighted) cleanup()
}
}, 5000)
cancelLocateRef.current = cleanup
requestAnimationFrame(() => find())
}
useEffect(() => () => cancelLocateRef.current(), [])
const moveSelection = (
from: number,
direction: number,
focusResult = false,
) => {
const next = (from + direction + results.length) % results.length
setSelected(next)
const target = rootRef.current?.querySelector<HTMLElement>(
`[data-search-result="${next}"]`,
)
if (focusResult) target?.focus()
target?.scrollIntoView({ block: 'nearest' })
}
const header = (
<Box
sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}
>
<TextField
inputRef={inputRef}
size="small"
label={t('settings.search.placeholder')}
value={query}
onChange={(e) => changeQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.preventDefault()
if (showPage) restoreResults()
else changeQuery('')
}
if (e.key === 'Enter' && results[selected]) {
e.preventDefault()
locate(results[selected])
}
if (
(e.key === 'ArrowDown' || e.key === 'ArrowUp') &&
results.length
) {
e.preventDefault()
moveSelection(selected, e.key === 'ArrowDown' ? 1 : -1)
}
}}
/>
{searching && (
<IconButton
aria-label={t('settings.search.clear')}
onClick={() => changeQuery('')}
>
<CloseRounded />
</IconButton>
)}
{showPage && (
<Button onClick={restoreResults}>{t('settings.search.back')}</Button>
)}
</Box>
)
const resultView =
searching && !showPage ? (
<List aria-label={t('settings.search.results')}>
{results.length ? (
results.map((entry, index) => (
<ListItemButton
key={entry.id}
data-search-result={index}
selected={selected === index}
onFocus={() => setSelected(index)}
onClick={() => {
setSelected(index)
locate(entry)
}}
onKeyDown={(e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault()
moveSelection(index, e.key === 'ArrowDown' ? 1 : -1, true)
}
if (e.key === 'Escape') {
e.preventDefault()
inputRef.current?.focus()
}
}}
>
<ListItemText
primary={t(entry.name)}
secondary={`${t(entry.section)} / ${t(entry.path)}`}
/>
</ListItemButton>
))
) : (
<Typography sx={{ p: 2 }}>{t('settings.search.empty')}</Typography>
)}
</List>
) : null
return (
<SettingsSearchContext value={registry}>
<GlobalStyles
styles={{
'[data-search-highlight]': {
outline: '2px solid #6b70e8',
outlineOffset: -2,
},
}}
/>
<Box
ref={rootRef}
onKeyDown={(e) => {
if (
e.key === 'Escape' &&
showPage &&
!(e.target as HTMLElement).closest('[role="dialog"]')
) {
e.stopPropagation()
restoreResults()
}
}}
sx={{
height: '100%',
'& .base-page > header': {
flex: '0 0 auto',
minHeight: 58,
flexWrap: 'wrap',
gap: 1,
py: 1,
},
}}
>
{children(header, resultView, searching && !showPage)}
</Box>
</SettingsSearchContext>
)
}
@@ -220,7 +220,6 @@ const ProxyControlSwitches = ({
)} )}
icon={DeleteForeverRounded} icon={DeleteForeverRounded}
color="secondary" color="secondary"
data-setting-target="service-uninstall"
onClick={onUninstallService} onClick={onUninstallService}
sx={{ ml: 1 }} sx={{ ml: 1 }}
/> />
+4 -18
View File
@@ -7,10 +7,6 @@ import { useTranslation } from 'react-i18next'
import { BaseDialog } from '@/components/base' import { BaseDialog } from '@/components/base'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import type {
IVergeTestItem_Deserialize,
IVergeTestItem_Serialize,
} from '@/services/bindings'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
interface Props { interface Props {
@@ -31,7 +27,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
const testList = verge?.test_list ?? [] const testList = verge?.test_list ?? []
const { control, ...formIns } = useForm<IVergeTestItem_Deserialize>({ const { control, ...formIns } = useForm<IVergeTestItem>({
defaultValues: { defaultValues: {
name: '', name: '',
icon: '', icon: '',
@@ -41,16 +37,11 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const patchTestList = async ( const patchTestList = async (
uid: string, uid: string,
patch: Partial<IVergeTestItem_Deserialize>, patch: Partial<IVergeTestItem>,
) => { ) => {
const newList = testList.map((x) => { const newList = testList.map((x) => {
if (x.uid === uid) { if (x.uid === uid) {
return { return { ...x, ...patch }
uid: x.uid,
name: patch.name === undefined ? x.name : patch.name,
icon: patch.icon === undefined ? x.icon : patch.icon,
url: patch.url === undefined ? x.url : patch.url,
}
} }
return x return x
}) })
@@ -86,12 +77,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
if (openType === 'new') { if (openType === 'new') {
const uid = nanoid() const uid = nanoid()
const item: IVergeTestItem_Serialize = { const item = { ...form, uid }
uid,
name: form.name ?? null,
icon: form.icon ?? null,
url: form.url ?? null,
}
const newList = [...testList, item] const newList = [...testList, item]
await patchVerge({ test_list: newList }) await patchVerge({ test_list: newList })
onChange(uid) onChange(uid)
+29 -13
View File
@@ -1,35 +1,51 @@
import { selectNodeForGroup } from 'tauri-plugin-mihomo-api'
import { expect, it, vi } from 'vitest' import { expect, it, vi } from 'vitest'
import { switchProxyNode } from '@/services/cmds'
import { useProxySelection } from './use-proxy-selection' import { useProxySelection } from './use-proxy-selection'
const record = vi.hoisted(() => vi.fn())
vi.mock('react', () => ({ vi.mock('react', () => ({
useCallback: (callback: unknown) => callback, useCallback: (callback: unknown) => callback,
useRef: (current: unknown) => ({ current }), useRef: (current: unknown) => ({ current }),
})) }))
vi.mock('@/services/cmds', () => ({ switchProxyNode: vi.fn() })) vi.mock('@/hooks/use-record-selection', () => ({
useRecordSelection: () => record,
useForgetSelection: () => vi.fn(),
}))
vi.mock('@/hooks/use-verge', () => ({ useVerge: () => ({ verge: {} }) }))
vi.mock('tauri-plugin-mihomo-api', () => ({
selectNodeForGroup: vi.fn(),
unfixedProxy: vi.fn(),
}))
vi.mock('@/services/cmds', () => ({ syncTrayProxySelection: vi.fn() }))
vi.mock('@/services/notice-service', () => ({ showNotice: { error: vi.fn() } })) vi.mock('@/services/notice-service', () => ({ showNotice: { error: vi.fn() } }))
vi.mock('@/services/query-client', () => ({ revalidateQueries: vi.fn() })) vi.mock('@/services/query-client', () => ({ revalidateQueries: vi.fn() }))
it('requests independent group selections while another group is pending', async () => { it('applies and records independent group selections while another group is pending', async () => {
let finishFirst!: () => void let finishFirst!: () => void
vi.mocked(switchProxyNode) vi.mocked(selectNodeForGroup)
.mockImplementationOnce( .mockImplementationOnce(
() => () =>
new Promise((resolve) => { new Promise((resolve) => {
finishFirst = () => resolve(null) finishFirst = resolve
}), }),
) )
.mockResolvedValueOnce(null) .mockResolvedValueOnce(undefined)
const first = useProxySelection() const firstSuccess = vi.fn()
const second = useProxySelection() const secondSuccess = vi.fn()
const first = useProxySelection({ onSuccess: firstSuccess })
const second = useProxySelection({ onSuccess: secondSuccess })
first.select('Group A', 'Node A') first.select('Group A', 'Node A')
second.select('Group B', 'Node B', 'Node B') second.select('Group B', 'Node B')
await vi.waitFor(() => await vi.waitFor(() =>
expect(switchProxyNode).toHaveBeenCalledWith('Group B', 'Node B', true), expect(record).toHaveBeenCalledWith('Group B', 'Node B'),
) )
expect(switchProxyNode).toHaveBeenCalledWith('Group A', 'Node A', false) expect(selectNodeForGroup).toHaveBeenCalledTimes(2)
expect(switchProxyNode).toHaveBeenCalledTimes(2) expect(firstSuccess).not.toHaveBeenCalled()
expect(secondSuccess).toHaveBeenCalledOnce()
finishFirst() finishFirst()
await vi.waitFor(() =>
expect(record).toHaveBeenCalledWith('Group A', 'Node A'),
)
expect(firstSuccess).toHaveBeenCalledOnce()
}) })
+96 -8
View File
@@ -1,39 +1,121 @@
import { useCallback, useRef } from 'react' import { useCallback, useRef } from 'react'
import {
closeConnection,
getConnections,
selectNodeForGroup,
unfixedProxy,
} from 'tauri-plugin-mihomo-api'
import { switchProxyNode } from '@/services/cmds' import {
useForgetSelection,
useRecordSelection,
} from '@/hooks/use-record-selection'
import { useVerge } from '@/hooks/use-verge'
import { syncTrayProxySelection } from '@/services/cmds'
import { mutate } from '@/services/mutate' import { mutate } from '@/services/mutate'
import { debugLog } from '@/utils/debug'
// 缓存连接清理
const cleanupConnections = async (previousProxy: string) => {
try {
const { connections } = await getConnections()
const cleanupPromises = (connections ?? [])
.filter((conn) => conn.chains.includes(previousProxy))
.map((conn) => closeConnection(conn.id))
if (cleanupPromises.length > 0) {
await Promise.allSettled(cleanupPromises)
debugLog(`[ProxySelection] 清理了 ${cleanupPromises.length} 个连接`)
}
} catch (error) {
console.warn('[ProxySelection] 连接清理失败:', error)
}
}
interface ProxySelectionOptions { interface ProxySelectionOptions {
onSuccess?: () => void
onError?: (error: any) => void onError?: (error: any) => void
} }
interface ProxyChangeRequest { interface ProxyChangeRequest {
groupName: string groupName: string
proxyName: string proxyName: string
previousProxy?: string
fixed?: string fixed?: string
} }
// 代理选择 Hook
export const useProxySelection = (options: ProxySelectionOptions = {}) => { export const useProxySelection = (options: ProxySelectionOptions = {}) => {
const recordSelection = useRecordSelection()
const forgetSelection = useForgetSelection()
const { verge } = useVerge()
const pendingRequestRef = useRef<ProxyChangeRequest | null>(null) const pendingRequestRef = useRef<ProxyChangeRequest | null>(null)
const isProcessingRef = useRef(false) const isProcessingRef = useRef(false)
const { onError } = options
const { onSuccess, onError } = options
const autoCloseConnection = verge?.auto_close_connection ?? false
// 切换节点
const syncTraySelection = useCallback(() => {
mutate(() => syncTrayProxySelection(), {
id: 'sync-tray-selection',
errorNotice: false,
}).catch((error) => {
console.error('[ProxySelection] 托盘状态同步失败:', error)
})
}, [])
const executeChange = useCallback( const executeChange = useCallback(
async ({ groupName, proxyName, fixed }: ProxyChangeRequest) => { async (request: ProxyChangeRequest) => {
const { groupName, proxyName, previousProxy, fixed } = request
const isFixedProxy = fixed === proxyName
if (isFixedProxy) {
debugLog(`[ProxySelection] 代理取消固定: ${groupName} -> ${proxyName}`)
} else {
debugLog(`[ProxySelection] 代理切换: ${groupName} -> ${proxyName}`)
}
try { try {
await mutate( const result = await mutate(
() => switchProxyNode(groupName, proxyName, fixed === proxyName), () =>
isFixedProxy
? unfixedProxy(groupName)
: selectNodeForGroup(groupName, proxyName),
{ id: `select-proxy-node:${groupName}`, errorNotice: false }, { id: `select-proxy-node:${groupName}`, errorNotice: false },
) )
if (!result.ok) return
onSuccess?.()
syncTraySelection()
if (isFixedProxy) {
await forgetSelection(groupName)
debugLog(`[ProxySelection] 代理和状态同步完成: ${groupName}`)
} else {
await recordSelection(groupName, proxyName)
debugLog(
`[ProxySelection] 代理和状态同步完成: ${groupName} -> ${proxyName}`,
)
}
if (autoCloseConnection && previousProxy) {
void cleanupConnections(previousProxy)
}
} catch (error) { } catch (error) {
console.error( console.error(
`[ProxySelection] Failed to switch proxy: ${groupName} -> ${proxyName}`, `[ProxySelection] 代理切换失败: ${groupName} -> ${proxyName}`,
error, error,
) )
onError?.(error) onError?.(error)
} }
}, },
[onError], [
autoCloseConnection,
forgetSelection,
onError,
onSuccess,
recordSelection,
syncTraySelection,
],
) )
const flushChangeQueue = useCallback(async () => { const flushChangeQueue = useCallback(async () => {
@@ -55,10 +137,16 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
}, [executeChange]) }, [executeChange])
const select = useCallback( const select = useCallback(
(groupName: string, proxyName: string, fixed?: string) => { (
groupName: string,
proxyName: string,
previousProxy?: string,
fixed?: string,
) => {
pendingRequestRef.current = { pendingRequestRef.current = {
groupName, groupName,
proxyName, proxyName,
previousProxy,
fixed, fixed,
} }
void flushChangeQueue() void flushChangeQueue()
+14 -1
View File
@@ -1,6 +1,6 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { recordSelectedNode } from '@/services/cmds' import { forgetSelectedNode, recordSelectedNode } from '@/services/cmds'
import { mutate } from '@/services/mutate' import { mutate } from '@/services/mutate'
/** /**
@@ -19,3 +19,16 @@ export const useRecordSelection = () => {
} }
}, []) }, [])
} }
export const useForgetSelection = () => {
return useCallback(async (groupName: string) => {
try {
await mutate(() => forgetSelectedNode(groupName), {
id: `persist-selected-node:${groupName}`,
errorNotice: false,
})
} catch (error) {
console.error('[Selection] 清除代理选择失败:', error)
}
}, [])
}
-7
View File
@@ -714,12 +714,5 @@
"installing": "جاري تثبيت الخدمة...", "installing": "جاري تثبيت الخدمة...",
"uninstalling": "Uninstalling Service..." "uninstalling": "Uninstalling Service..."
} }
},
"search": {
"placeholder": "البحث في الإعدادات",
"clear": "مسح البحث",
"back": "العودة إلى النتائج",
"results": "نتائج البحث",
"empty": "لم يتم العثور على إعدادات"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "Service wird installiert...", "installing": "Service wird installiert...",
"uninstalling": "Service wird deinstalliert..." "uninstalling": "Service wird deinstalliert..."
} }
},
"search": {
"placeholder": "Einstellungen suchen",
"clear": "Suche löschen",
"back": "Zurück zu Ergebnissen",
"results": "Suchergebnisse",
"empty": "Keine Einstellungen gefunden"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "Installing Service...", "installing": "Installing Service...",
"uninstalling": "Uninstalling Service..." "uninstalling": "Uninstalling Service..."
} }
},
"search": {
"placeholder": "Search settings",
"clear": "Clear search",
"back": "Back to results",
"results": "Search results",
"empty": "No settings found"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "Instalando servicio...", "installing": "Instalando servicio...",
"uninstalling": "Desinstalando servicio..." "uninstalling": "Desinstalando servicio..."
} }
},
"search": {
"placeholder": "Buscar ajustes",
"clear": "Borrar búsqueda",
"back": "Volver a resultados",
"results": "Resultados de búsqueda",
"empty": "No se encontraron ajustes"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "در حال نصب سرویس...", "installing": "در حال نصب سرویس...",
"uninstalling": "در حال حذف سرویس..." "uninstalling": "در حال حذف سرویس..."
} }
},
"search": {
"placeholder": "جستجوی تنظیمات",
"clear": "پاک کردن جستجو",
"back": "بازگشت به نتایج",
"results": "نتایج جستجو",
"empty": "تنظیماتی یافت نشد"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "Memasang Layanan...", "installing": "Memasang Layanan...",
"uninstalling": "Uninstalling Service..." "uninstalling": "Uninstalling Service..."
} }
},
"search": {
"placeholder": "Cari pengaturan",
"clear": "Hapus pencarian",
"back": "Kembali ke hasil",
"results": "Hasil pencarian",
"empty": "Pengaturan tidak ditemukan"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "サービスをインストール中...", "installing": "サービスをインストール中...",
"uninstalling": "サービスをアンインストール中..." "uninstalling": "サービスをアンインストール中..."
} }
},
"search": {
"placeholder": "設定を検索",
"clear": "検索をクリア",
"back": "結果に戻る",
"results": "検索結果",
"empty": "設定が見つかりません"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "서비스 설치 중...", "installing": "서비스 설치 중...",
"uninstalling": "서비스 제거 중..." "uninstalling": "서비스 제거 중..."
} }
},
"search": {
"placeholder": "설정 검색",
"clear": "검색 지우기",
"back": "결과로 돌아가기",
"results": "검색 결과",
"empty": "설정을 찾을 수 없습니다"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "Instalando o Serviço...", "installing": "Instalando o Serviço...",
"uninstalling": "Desinstalando o Serviço..." "uninstalling": "Desinstalando o Serviço..."
} }
},
"search": {
"placeholder": "Pesquisar configurações",
"clear": "Limpar pesquisa",
"back": "Voltar aos resultados",
"results": "Resultados da pesquisa",
"empty": "Nenhuma configuração encontrada"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "Установка службы...", "installing": "Установка службы...",
"uninstalling": "Удаление службы..." "uninstalling": "Удаление службы..."
} }
},
"search": {
"placeholder": "Поиск настроек",
"clear": "Очистить поиск",
"back": "Вернуться к результатам",
"results": "Результаты поиска",
"empty": "Настройки не найдены"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "Hizmet Kuruluyor...", "installing": "Hizmet Kuruluyor...",
"uninstalling": "Hizmet Kaldırılıyor..." "uninstalling": "Hizmet Kaldırılıyor..."
} }
},
"search": {
"placeholder": "Ayarları ara",
"clear": "Aramayı temizle",
"back": "Sonuçlara dön",
"results": "Arama sonuçları",
"empty": "Ayar bulunamadı"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "Хезмәт урнаштырыла...", "installing": "Хезмәт урнаштырыла...",
"uninstalling": "Uninstalling Service..." "uninstalling": "Uninstalling Service..."
} }
},
"search": {
"placeholder": "Көйләүләрне эзләү",
"clear": "Эзләүне чистарту",
"back": "Нәтиҗәләргә кайту",
"results": "Эзләү нәтиҗәләре",
"empty": "Көйләүләр табылмады"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "安装服务中...", "installing": "安装服务中...",
"uninstalling": "卸载服务中..." "uninstalling": "卸载服务中..."
} }
},
"search": {
"placeholder": "搜索设置",
"clear": "清空搜索",
"back": "返回结果",
"results": "搜索结果",
"empty": "未找到设置"
} }
} }
-7
View File
@@ -714,12 +714,5 @@
"installing": "安裝服務中...", "installing": "安裝服務中...",
"uninstalling": "服務解除安裝中..." "uninstalling": "服務解除安裝中..."
} }
},
"search": {
"placeholder": "搜尋設定",
"clear": "清除搜尋",
"back": "返回結果",
"results": "搜尋結果",
"empty": "找不到設定"
} }
} }
@@ -1,7 +1,7 @@
import { arrayMove } from '@dnd-kit/helpers' import { arrayMove } from '@dnd-kit/helpers'
import type { DragEndEvent } from '@dnd-kit/react' import type { DragEndEvent } from '@dnd-kit/react'
import { isSortable } from '@dnd-kit/react/sortable' import { isSortable } from '@dnd-kit/react/sortable'
import { useCallback, useMemo, useReducer, useState } from 'react' import { useCallback, useEffect, useMemo, useReducer } from 'react'
type MenuOrderAction = { type: 'sync'; payload: string[] } type MenuOrderAction = { type: 'sync'; payload: string[] }
@@ -79,11 +79,9 @@ export const useNavMenuOrder = <T extends { path: string }>({
configMenuOrder, configMenuOrder,
) )
const [syncedConfigOrder, setSyncedConfigOrder] = useState(configMenuOrder) useEffect(() => {
if (!areOrdersEqual(syncedConfigOrder, configMenuOrder)) {
setSyncedConfigOrder(configMenuOrder)
dispatchMenuOrder({ type: 'sync', payload: configMenuOrder }) dispatchMenuOrder({ type: 'sync', payload: configMenuOrder })
} }, [configMenuOrder])
const isDefaultOrder = useMemo( const isDefaultOrder = useMemo(
() => areOrdersEqual(menuOrder, defaultOrder), () => areOrdersEqual(menuOrder, defaultOrder),

Some files were not shown because too many files have changed in this diff Show More