mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-21 17:52:25 +08:00
Compare commits
22
Commits
22b51555ee
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6c7af82a7 | ||
|
|
5e6a696087 | ||
|
|
27d8a71d41 | ||
|
|
9dd5aa676b | ||
|
|
4e5ac8683b | ||
|
|
7965711945 | ||
|
|
396d343a3a | ||
|
|
b729e59650 | ||
|
|
a3cc58013b | ||
|
|
cef66cd745 | ||
|
|
3b7bfa10fd | ||
|
|
b238175f18 | ||
|
|
8f00096a9f | ||
|
|
9f498a85d0 | ||
|
|
e014b913d2 | ||
|
|
f38ceae3fb | ||
|
|
089635c8bf | ||
|
|
dc5a8a5ff4 | ||
|
|
476ddb63c3 | ||
|
|
c6b51171a1 | ||
|
|
d4287970bc | ||
|
|
b20fdc9b96 |
@@ -4,11 +4,11 @@
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "24.21.0",
|
||||
"pnpmVersion": "12.11.2"
|
||||
"version": "24.22.0",
|
||||
"pnpmVersion": "12.14.0"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.99.0",
|
||||
"version": "1.99.1",
|
||||
"profile": "default"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git:1": {},
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -347,7 +347,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -426,7 +426,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -524,7 +524,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6.1.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
|
||||
- name: Install Rust Stable
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: dtolnay/rust-toolchain@1.99.0
|
||||
uses: dtolnay/rust-toolchain@1.99.1
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Restore pnpm cache
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -311,7 +311,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -331,7 +331,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
@@ -389,7 +389,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -446,7 +446,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -466,7 +466,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -487,7 +487,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -547,7 +547,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -573,7 +573,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -638,7 +638,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -22,13 +22,13 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
nodejs 24.21.0
|
||||
pnpm 12.11.2
|
||||
rust 1.99.0
|
||||
nodejs 24.22.0
|
||||
pnpm 12.14.0
|
||||
rust 1.99.1
|
||||
|
||||
Generated
+107
-191
@@ -1119,7 +1119,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify 0.3.0",
|
||||
"minisign-verify",
|
||||
"nanoid",
|
||||
"network-interface",
|
||||
"objc2",
|
||||
@@ -1138,7 +1138,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
"smartstring",
|
||||
"socket2",
|
||||
"specta",
|
||||
@@ -1175,7 +1175,7 @@ dependencies = [
|
||||
"windows-service",
|
||||
"windows-sys 0.61.2",
|
||||
"winreg 0.56.0",
|
||||
"zip 9.0.1",
|
||||
"zip 9.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1247,7 +1247,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"sysproxy",
|
||||
@@ -1462,9 +1462,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
checksum = "9eb6feb175df65d0a3ccd3e73484e940d0ac664e8034f0519fd49a93db3071d9"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
@@ -3951,9 +3954,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
version = "0.2.191"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -4048,11 +4051,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.21.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde178a3caf126c440fa15628147772d8ee590a39477d711353fbe2e58a73a5b"
|
||||
checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb"
|
||||
dependencies = [
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4167,12 +4170,6 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.3.0"
|
||||
@@ -5343,9 +5340,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppmd-rust"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
|
||||
checksum = "196a7c80b9a7652aba7cc070827516c2abe4ccdf53d128e1944003cf5726cff1"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
@@ -5996,12 +5993,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||
checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ordered-multimap",
|
||||
"trim-in-place",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6067,9 +6065,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.45"
|
||||
version = "0.23.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
@@ -6410,9 +6408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -6597,9 +6595,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
@@ -6622,7 +6620,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"sigchld",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
@@ -6642,17 +6639,6 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "sigchld"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"os_pipe",
|
||||
"signal-hook 0.3.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.1.17"
|
||||
@@ -6663,16 +6649,6 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -6878,6 +6854,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "static_vcruntime"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.9.0"
|
||||
@@ -6937,9 +6919,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"
|
||||
checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"serde",
|
||||
@@ -7186,9 +7168,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.12.1"
|
||||
version = "2.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9"
|
||||
checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -7234,16 +7216,16 @@ dependencies = [
|
||||
"tray-icon",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.7.1"
|
||||
version = "2.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45"
|
||||
checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -7255,6 +7237,8 @@ dependencies = [
|
||||
"semver 1.0.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"static_vcruntime",
|
||||
"strsim",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"walkdir",
|
||||
@@ -7262,11 +7246,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.7.1"
|
||||
version = "2.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80"
|
||||
checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64 0.23.1",
|
||||
"brotli",
|
||||
"ico",
|
||||
"json-patch",
|
||||
@@ -7289,9 +7273,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.7.1"
|
||||
version = "2.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c"
|
||||
checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -7347,9 +7331,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-clipboard-manager"
|
||||
version = "2.4.1"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "333d3b6beeb1c2c7fc040233d4ea7b6c03a9f8f039592b7400833c43fb5b568a"
|
||||
checksum = "9be5ed8d94c33afe8377e885119069f6508780659271d0a25cfc54d441f2bc81"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"log",
|
||||
@@ -7362,9 +7346,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.6.1"
|
||||
version = "2.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ac7a92a46ab5c88f44532ca50906d6e448a948d4ddf8c5376ab125e3260f736"
|
||||
checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"plist",
|
||||
@@ -7410,9 +7394,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.8.1"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf9a5c92e39bdd84f22f6be9230e39d0c4e140345f71ffe6058dca4f71127cf"
|
||||
checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
@@ -7428,9 +7412,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.6.0"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb"
|
||||
checksum = "5d58eb1d4984e535f47a642ffec09a2da6f5c18a12b5902d12126cb7626d628b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
@@ -7452,9 +7436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-global-shortcut"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bd2e1f725891a1613af8f25bf918ba3357654b0fd3f2d794c0ce76cbdd92ed2"
|
||||
checksum = "93ff17919fe09852d269bd37b1d3d2e993b9dbb514afe7acbf3346c1d3627e2d"
|
||||
dependencies = [
|
||||
"global-hotkey",
|
||||
"log",
|
||||
@@ -7467,9 +7451,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.8.0"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e9d6b3319e101bc1be27cfa0fb4054d7784a12509c67ead0033b82180e5ed1"
|
||||
checksum = "2c57e3451553f95a1db3bfed6a358a77ce5080b6399a242c6eadac113f97abd2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cookie_store",
|
||||
@@ -7486,13 +7470,13 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlpattern 0.3.0",
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-mihomo"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
|
||||
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?rev=7a03d16f4d39b1bb885b7babe66b9e153e34b615#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"clashmap",
|
||||
@@ -7516,9 +7500,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.5.1"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533770889abb60a0407d5a32096182f0e6463f5a3dfaa675be94580aa14070e1"
|
||||
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
@@ -7531,13 +7515,14 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"time",
|
||||
"url",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.7.0"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d39404b2572927e0d3ed30a7ca226037f7508d22e79c96ee6ef0d42b6a4b3345"
|
||||
checksum = "53810be1b4fed22bb6ac3f4cab5a2b80d9391d1af436bda145607532bc0bb219"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
@@ -7567,11 +7552,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "094bdbc47e5efe36544ba21b3a53ab5c724150236cf55e6c9be46191deb6eab6"
|
||||
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"libc",
|
||||
"log",
|
||||
"open",
|
||||
"os_pipe",
|
||||
@@ -7584,13 +7570,14 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.13.1"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cb0b2ea3e85ca287990d3859a29cc5cb18024240fd78f62e027fb7963670f18"
|
||||
checksum = "7259989d4fe11b2bf51f7d671642249b1837c4ae2da1430dc713ae38f98ceeec"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs 7.0.0",
|
||||
@@ -7598,8 +7585,9 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify 0.2.5",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest 0.13.5",
|
||||
@@ -7621,9 +7609,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-wdio-webdriver"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c5e97428174a52b7f4357bbc4535fcce52c6a06c8ee7a379502f7cda1b6045"
|
||||
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -7647,9 +7635,9 @@ dependencies = [
|
||||
"tracing",
|
||||
"uuid",
|
||||
"webkit2gtk",
|
||||
"webview2-com 0.38.2",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7669,9 +7657,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.12.1"
|
||||
version = "2.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413"
|
||||
checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
@@ -7688,15 +7676,15 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.12.1"
|
||||
version = "2.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22"
|
||||
checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -7714,7 +7702,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
"wry",
|
||||
]
|
||||
@@ -7752,9 +7740,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.10.1"
|
||||
version = "2.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee"
|
||||
checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brotli",
|
||||
@@ -7783,7 +7771,7 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"toml 1.1.6+spec-1.1.0",
|
||||
"url",
|
||||
"urlpattern 0.6.0",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -7902,7 +7890,7 @@ dependencies = [
|
||||
"regex",
|
||||
"semver 0.11.0",
|
||||
"sha2 0.9.9",
|
||||
"signal-hook 0.1.17",
|
||||
"signal-hook",
|
||||
"terminfo",
|
||||
"termios",
|
||||
"thiserror 1.0.69",
|
||||
@@ -8073,9 +8061,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.2"
|
||||
version = "1.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
||||
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -8135,9 +8123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.19"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||
checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -8500,6 +8488,12 @@ dependencies = [
|
||||
"petgraph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trim-in-place"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daa60874b45b168f67f3f9ba1ee821ad77144d6fa19cef1cd1874a7450a948da"
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.16"
|
||||
@@ -8592,47 +8586,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
|
||||
dependencies = [
|
||||
"unic-char-range",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-range"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-common"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-ident"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
|
||||
dependencies = [
|
||||
"unic-char-property",
|
||||
"unic-char-range",
|
||||
"unic-ucd-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-version"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
|
||||
dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
@@ -8692,18 +8645,6 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"unic-ucd-ident",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.6.0"
|
||||
@@ -9095,20 +9036,6 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys 0.38.2",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.39.1"
|
||||
@@ -9116,7 +9043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys 0.39.1",
|
||||
"webview2-com-sys",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
@@ -9132,17 +9059,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com-sys"
|
||||
version = "0.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com-sys"
|
||||
version = "0.39.1"
|
||||
@@ -9882,7 +9798,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-version",
|
||||
@@ -10150,9 +10066,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.0.1"
|
||||
version = "9.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692"
|
||||
checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
@@ -10202,27 +10118,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
version = "2.1.2+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
|
||||
+4
-4
@@ -61,13 +61,13 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
|
||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||
tracing = "0.1.44"
|
||||
|
||||
tauri = { version = "2.12.1" }
|
||||
tauri-plugin-clipboard-manager = "2.4.1"
|
||||
tauri = { version = "2.12.4" }
|
||||
tauri-plugin-clipboard-manager = "2.5.0"
|
||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||
anyhow = "1.0.104"
|
||||
chrono = "0.4.45"
|
||||
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
||||
tokio = { version = "1.53.2", features = [
|
||||
tokio = { version = "1.53.3", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
@@ -88,7 +88,7 @@ rust_iso3166 = "0.2.0"
|
||||
smartstring = { version = "1.0.1" }
|
||||
|
||||
serde = { version = "1.0.229" }
|
||||
serde_json = { version = "1.0.151" }
|
||||
serde_json = { version = "1.0.154" }
|
||||
serde_yaml_ng = { version = "0.10.0" }
|
||||
bitflags = { version = "2.13.2" }
|
||||
socket2 = "0.6.5"
|
||||
|
||||
+5
-2
@@ -3,8 +3,10 @@
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
|
||||
- 修复「首页」网站测速失败后卡片一直显示测试中的问题
|
||||
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
||||
|
||||
**🐧 Linux**
|
||||
|
||||
@@ -21,6 +23,7 @@
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: عام
|
||||
profiles: الملفات التعريفية
|
||||
proxies: وكلاء
|
||||
testLatency: اختبار التأخير
|
||||
testingLatency: جارٍ اختبار التأخير…
|
||||
systemProxy: وكيل النظام
|
||||
tunMode: وضع TUN
|
||||
closeAllConnections: إغلاق كل الاتصالات
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Profile
|
||||
proxies: Proxy
|
||||
testLatency: Latenz testen
|
||||
testingLatency: Latenz wird getestet…
|
||||
systemProxy: Systemproxy
|
||||
tunMode: TUN-Modus
|
||||
closeAllConnections: Alle Verbindungen schließen
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
testLatency: Test Latency
|
||||
testingLatency: Testing latency…
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Perfiles
|
||||
proxies: Proxies
|
||||
testLatency: Probar latencia
|
||||
testingLatency: Probando latencia…
|
||||
systemProxy: Proxy del sistema
|
||||
tunMode: Modo TUN
|
||||
closeAllConnections: Cerrar todas las conexiones
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: سراسری
|
||||
profiles: پروفایلها
|
||||
proxies: پروکسیها
|
||||
testLatency: آزمایش تأخیر
|
||||
testingLatency: در حال آزمایش تأخیر…
|
||||
systemProxy: پروکسی سیستم
|
||||
tunMode: حالت TUN
|
||||
closeAllConnections: بستن همه اتصالها
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Profil
|
||||
proxies: Proksi
|
||||
testLatency: Uji Latensi
|
||||
testingLatency: Menguji latensi…
|
||||
systemProxy: Proksi Sistem
|
||||
tunMode: Mode TUN
|
||||
closeAllConnections: Tutup Semua Koneksi
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: グローバル
|
||||
profiles: プロファイル
|
||||
proxies: プロキシ
|
||||
testLatency: 遅延テスト
|
||||
testingLatency: 遅延をテスト中…
|
||||
systemProxy: システムプロキシ
|
||||
tunMode: TUN モード
|
||||
closeAllConnections: すべての接続を閉じる
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: 글로벌
|
||||
profiles: 프로필
|
||||
proxies: 프록시
|
||||
testLatency: 지연 테스트
|
||||
testingLatency: 지연 테스트 중…
|
||||
systemProxy: 시스템 프록시
|
||||
tunMode: TUN 모드
|
||||
closeAllConnections: 모든 연결 닫기
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Perfis
|
||||
proxies: Proxies
|
||||
testLatency: Test Latency
|
||||
testingLatency: Testing latency…
|
||||
systemProxy: Proxy do sistema
|
||||
tunMode: Modo TUN
|
||||
closeAllConnections: Fechar todas as conexões
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Глобальный
|
||||
profiles: Профили
|
||||
proxies: Прокси
|
||||
testLatency: Проверить задержку
|
||||
testingLatency: Проверка задержки…
|
||||
systemProxy: Системный прокси
|
||||
tunMode: Режим TUN
|
||||
closeAllConnections: Закрыть все соединения
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Küresel
|
||||
profiles: Profiller
|
||||
proxies: Vekil'ler
|
||||
testLatency: Gecikmeyi Test Et
|
||||
testingLatency: Gecikme test ediliyor…
|
||||
systemProxy: Sistem Vekil'i
|
||||
tunMode: TUN Modu
|
||||
closeAllConnections: Tüm Bağlantıları Kapat
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Глобаль
|
||||
profiles: Профильләр
|
||||
proxies: Проксилар
|
||||
testLatency: Кичектерүне тикшерү
|
||||
testingLatency: Кичектерү тикшерелә…
|
||||
systemProxy: Системалы прокси
|
||||
tunMode: TUN режимы
|
||||
closeAllConnections: Барлык тоташуларны ябу
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: 全局
|
||||
profiles: 订阅
|
||||
proxies: 代理
|
||||
testLatency: 测试延迟
|
||||
testingLatency: 正在测试延迟…
|
||||
systemProxy: 系统代理
|
||||
tunMode: TUN 模式
|
||||
closeAllConnections: 关闭所有连接
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: 全域
|
||||
profiles: 訂閱
|
||||
proxies: 代理
|
||||
testLatency: 測試延遲
|
||||
testingLatency: 正在測試延遲…
|
||||
systemProxy: 系統代理
|
||||
tunMode: 虛擬網路介面卡模式
|
||||
closeAllConnections: 關閉所有連線
|
||||
|
||||
@@ -15,7 +15,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
sysinfo = { version = "0.39.6", features = ["network", "system"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc = "0.2.190"
|
||||
libc = "0.2.191"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
deelevate = { workspace = true }
|
||||
|
||||
+15
-15
@@ -54,13 +54,13 @@
|
||||
"@mui/icons-material": "^9.4.0",
|
||||
"@mui/material": "^9.4.0",
|
||||
"@tanstack/react-virtual": "^3.14.13",
|
||||
"@tauri-apps/api": "2.12.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.4.0",
|
||||
"@tauri-apps/plugin-dialog": "2.8.0",
|
||||
"@tauri-apps/plugin-fs": "2.6.0",
|
||||
"@tauri-apps/plugin-http": "2.8.0",
|
||||
"@tauri-apps/plugin-opener": "2.7.0",
|
||||
"@tauri-apps/plugin-updater": "2.13.1",
|
||||
"@tauri-apps/api": "2.12.4",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "2.8.3",
|
||||
"@tauri-apps/plugin-fs": "2.7.1",
|
||||
"@tauri-apps/plugin-http": "2.8.3",
|
||||
"@tauri-apps/plugin-opener": "2.7.2",
|
||||
"@tauri-apps/plugin-updater": "2.14.0",
|
||||
"ahooks": "^3.10.0",
|
||||
"cidr-block": "^2.3.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
@@ -75,11 +75,11 @@
|
||||
"monaco-yaml": "^5.5.1",
|
||||
"nanoid": "^6.0.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0",
|
||||
"react": "19.3.1",
|
||||
"react-dom": "19.3.1",
|
||||
"react-error-boundary": "6.1.6",
|
||||
"react-hook-form": "^7.88.0",
|
||||
"react-i18next": "17.0.15",
|
||||
"react-i18next": "17.0.19",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "^8.4.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
@@ -95,7 +95,7 @@
|
||||
"@biomejs/biome": "^2.5.14",
|
||||
"@eslint-react/eslint-plugin": "^5.20.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tauri-apps/cli": "2.12.1",
|
||||
"@tauri-apps/cli": "2.12.3",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.13.6",
|
||||
"@types/react": "19.3.0",
|
||||
@@ -103,7 +103,7 @@
|
||||
"@types/validator": "^13.15.10",
|
||||
"@vitejs/plugin-legacy": "^8.2.3",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"@wdio/tauri-service": "1.4.0",
|
||||
"@wdio/tauri-service": "1.5.0",
|
||||
"adm-zip": "^0.6.1",
|
||||
"axios": "^1.20.0",
|
||||
"cli-color": "^2.0.4",
|
||||
@@ -123,7 +123,7 @@
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.7.0",
|
||||
"knip": "6.39.0",
|
||||
"knip": "6.41.0",
|
||||
"lint-staged": "^17.5.1",
|
||||
"sass": "^1.104.1",
|
||||
"tar": "^7.5.22",
|
||||
@@ -133,7 +133,7 @@
|
||||
"vite": "^8.3.0",
|
||||
"vite-plugin-svgr": "^5.2.0",
|
||||
"vitest": "^5.0.1",
|
||||
"webdriverio": "10.0.0",
|
||||
"webdriverio": "10.0.2",
|
||||
"ws": "8.22.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -146,5 +146,5 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.11.2"
|
||||
"packageManager": "pnpm@12.14.0"
|
||||
}
|
||||
|
||||
Generated
+451
-447
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,17 @@
|
||||
"matchManagers": ["npm"],
|
||||
"groupName": "npm dependencies"
|
||||
},
|
||||
{
|
||||
"description": "Update Tauri npm packages and Cargo crates together so frontend and backend stay in sync",
|
||||
"matchPackageNames": [
|
||||
"@tauri-apps/**",
|
||||
"@wdio/tauri-service",
|
||||
"tauri",
|
||||
"tauri-*"
|
||||
],
|
||||
"groupName": "tauri",
|
||||
"addLabels": ["tauri"]
|
||||
},
|
||||
{
|
||||
"description": "Group all GitHub Actions updates into a single PR",
|
||||
"matchManagers": ["github-actions"],
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.99.0"
|
||||
channel = "1.99.1"
|
||||
components = ["rustfmt", "clippy"]
|
||||
|
||||
+17
-17
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.7.1", features = [] }
|
||||
tauri-build = { version = "2.7.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
clash-verge-draft = { workspace = true }
|
||||
@@ -72,55 +72,55 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
|
||||
"guard",
|
||||
] }
|
||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.4.0"
|
||||
tauri-plugin-opener = "2.7.0"
|
||||
tauri-plugin-dialog = "2.8.1"
|
||||
tauri-plugin-fs = "2.6.0"
|
||||
tauri-plugin-shell = "2.5.0"
|
||||
tauri-plugin-opener = "2.7.2"
|
||||
tauri-plugin-dialog = "2.8.3"
|
||||
tauri-plugin-fs = "2.7.1"
|
||||
tauri-plugin-process = "2.4.0"
|
||||
tauri-plugin-deep-link = "2.6.1"
|
||||
tauri-plugin-deep-link = "2.6.2"
|
||||
tauri-plugin-window-state = "2.5.0"
|
||||
zip = "9.0.1"
|
||||
zip = "9.0.4"
|
||||
flate2 = "1.1.10"
|
||||
reqwest_dav = "0.3.3"
|
||||
aes-gcm = { version = "0.11.1" }
|
||||
base64 = "0.23.1"
|
||||
minisign-verify = "0.3.0"
|
||||
sha2 = "0.11"
|
||||
sha2 = "0.11.1"
|
||||
getrandom = "0.4.3"
|
||||
futures = "0.3.34"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.5.1"
|
||||
tauri-plugin-notification = "2.7.0"
|
||||
tokio-stream = "0.1.19"
|
||||
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||
tauri-plugin-http = "2.8.0"
|
||||
tauri-plugin-http = "2.8.3"
|
||||
tracing = { workspace = true }
|
||||
tauri-plugin-devtools = { version = "2.2.2", optional = true }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
||||
async-trait = "0.1.92"
|
||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [
|
||||
"client",
|
||||
] }
|
||||
arc-swap = "1.9.2"
|
||||
tokio-rustls = "0.26.6"
|
||||
rustls = { version = "0.23.45", features = ["ring"] }
|
||||
rustls = { version = "0.23.46", features = ["ring"] }
|
||||
webpki-roots = "1.0.9"
|
||||
dark-light = "3.0.0"
|
||||
bytes = "1.12.1"
|
||||
tokio-util = { version = "0.7.19", features = ["time"] }
|
||||
tokio-util = { version = "0.7.20", features = ["time"] }
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
|
||||
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
|
||||
specta-typescript = "=0.0.12"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.190"
|
||||
libc = "0.2.191"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
|
||||
clash-verge-signal = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
|
||||
tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true }
|
||||
objc2 = "0.6.5"
|
||||
objc2-foundation = { version = "0.3.2", features = [
|
||||
"NSString",
|
||||
@@ -153,8 +153,8 @@ windows-sys.workspace = true
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.7.0"
|
||||
tauri-plugin-global-shortcut = "2.4.0"
|
||||
tauri-plugin-updater = "2.13.1"
|
||||
tauri-plugin-global-shortcut = "2.4.1"
|
||||
tauri-plugin-updater = "2.14.0"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
|
||||
@@ -282,7 +282,9 @@ async fn handle_validation_failure(
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", outcome);
|
||||
discard_and_restore(current_profile).await?;
|
||||
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
|
||||
if !matches!(outcome, ValidationOutcome::Busy) {
|
||||
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
|
||||
}
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
@@ -311,9 +313,6 @@ async fn perform_config_update(
|
||||
current_value: Option<&String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let update_result = run_profile_config_update_transition(|| CoreManager::global().update_config_forced()).await;
|
||||
|
||||
match update_result {
|
||||
@@ -373,8 +372,6 @@ pub struct ProfilePatch {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub selected: Option<Vec<crate::config::PrfSelected>>,
|
||||
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||
pub extra: Option<crate::config::PrfExtra>,
|
||||
pub updated: Option<usize>,
|
||||
pub option: Option<PrfOption>,
|
||||
@@ -389,7 +386,6 @@ impl From<ProfilePatch> for PrfItem {
|
||||
file: patch.file,
|
||||
url: patch.url,
|
||||
selected: patch.selected,
|
||||
latency_test_urls: patch.latency_test_urls,
|
||||
extra: patch.extra,
|
||||
updated: patch.updated,
|
||||
option: patch.option,
|
||||
@@ -409,6 +405,9 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let profiles = IProfiles {
|
||||
|
||||
@@ -108,32 +108,20 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
|
||||
proxy_view(None).await
|
||||
}
|
||||
|
||||
pub(crate) async fn proxy_view(provider_timeout: Option<std::time::Duration>) -> CmdResult<ProxyViewV1> {
|
||||
let runtime = Config::runtime().await;
|
||||
let latest_runtime = runtime.latest_arc();
|
||||
let runtime_group_order = runtime_group_order(latest_runtime.config.as_ref());
|
||||
let group_scopes = runtime_group_scopes(latest_runtime.config.as_ref());
|
||||
|
||||
let mihomo = Handle::mihomo();
|
||||
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), async {
|
||||
match provider_timeout {
|
||||
Some(timeout) => tokio::time::timeout(timeout, mihomo.get_proxy_providers())
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok),
|
||||
None => mihomo.get_proxy_providers().await.ok(),
|
||||
}
|
||||
});
|
||||
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), mihomo.get_proxy_providers(),);
|
||||
let proxies = proxies.stringify_err()?;
|
||||
|
||||
Ok(ProxyViewBuilder::build(ProxyViewInput {
|
||||
runtime_group_order,
|
||||
group_scopes,
|
||||
proxies,
|
||||
providers,
|
||||
providers: providers.ok(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -53,10 +53,6 @@ pub struct PrfItem {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub selected: Option<Vec<PrfSelected>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra: Option<PrfExtra>,
|
||||
|
||||
@@ -233,7 +229,6 @@ impl PrfItem {
|
||||
desc: Some(desc),
|
||||
file: Some(file),
|
||||
url: None,
|
||||
latency_test_urls: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: Some(PrfOption {
|
||||
@@ -413,7 +408,6 @@ impl PrfItem {
|
||||
desc: desc.cloned(),
|
||||
file: Some(file),
|
||||
url: Some(url.as_str().into()),
|
||||
latency_test_urls: None,
|
||||
selected: None,
|
||||
extra,
|
||||
option: Some(PrfOption {
|
||||
|
||||
@@ -250,7 +250,6 @@ impl IProfiles {
|
||||
patch!(each, item, file);
|
||||
patch!(each, item, url);
|
||||
patch!(each, item, selected);
|
||||
patch!(each, item, latency_test_urls);
|
||||
patch!(each, item, extra);
|
||||
patch!(each, item, updated);
|
||||
patch!(each, item, option);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use serde::{Serialize, Serializer, ser::SerializeMap as _};
|
||||
use serde::Serialize;
|
||||
use tauri_plugin_mihomo::models::{
|
||||
DelayHistory, Extra, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
||||
DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
||||
};
|
||||
|
||||
pub struct ProxyViewInput {
|
||||
@@ -57,28 +57,6 @@ pub struct ProxyCapabilities {
|
||||
pub smux: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, specta::Type)]
|
||||
struct ProxyLatencyHistory<'a> {
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
history: &'a [DelayHistory],
|
||||
}
|
||||
|
||||
fn serialize_proxy_latency_history<S: Serializer>(
|
||||
extra: &HashMap<String, Extra>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let mut map = serializer.serialize_map(Some(extra.len()))?;
|
||||
for (url, extra) in extra {
|
||||
map.serialize_entry(
|
||||
url,
|
||||
&ProxyLatencyHistory {
|
||||
history: &extra.history,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxyGroupView {
|
||||
@@ -98,9 +76,6 @@ pub struct ProxyGroupView {
|
||||
pub test_url: Option<String>,
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
pub history: Vec<DelayHistory>,
|
||||
#[serde(serialize_with = "serialize_proxy_latency_history")]
|
||||
#[specta(type = HashMap<String, ProxyLatencyHistory<'static>>)]
|
||||
pub extra: HashMap<String, Extra>,
|
||||
#[serde(flatten)]
|
||||
pub capabilities: ProxyCapabilities,
|
||||
pub members: Vec<ProxyMemberRef>,
|
||||
@@ -116,9 +91,6 @@ pub struct ProxyNodeView {
|
||||
pub proxy_type: ProxyType,
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
pub history: Vec<DelayHistory>,
|
||||
#[serde(serialize_with = "serialize_proxy_latency_history")]
|
||||
#[specta(type = HashMap<String, ProxyLatencyHistory<'static>>)]
|
||||
pub extra: HashMap<String, Extra>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -284,7 +256,7 @@ impl ProxyViewBuilder {
|
||||
} else {
|
||||
ProxyViewProviderState::Unavailable
|
||||
};
|
||||
let (mut core_groups, core_nodes) = partition_core(proxies, providers.is_some());
|
||||
let (mut core_groups, core_nodes) = partition_core(proxies);
|
||||
let (mut records, core_node_ids) = build_core_records(core_nodes);
|
||||
let (providers, provider_candidates) = build_provider_records(providers, &mut records);
|
||||
let resolver = MemberResolver {
|
||||
@@ -317,14 +289,14 @@ impl ProxyViewBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn partition_core(proxies: Proxies, providers_available: bool) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
|
||||
fn partition_core(proxies: Proxies) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
|
||||
let mut groups = BTreeMap::new();
|
||||
let mut nodes = BTreeMap::new();
|
||||
|
||||
for (name, proxy) in proxies.proxies {
|
||||
if proxy.all.is_some() {
|
||||
groups.insert(name, proxy);
|
||||
} else if !providers_available || proxy.provider_name.is_empty() {
|
||||
} else {
|
||||
nodes.insert(name, proxy);
|
||||
}
|
||||
}
|
||||
@@ -431,7 +403,6 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
now,
|
||||
test_url,
|
||||
history,
|
||||
extra,
|
||||
udp,
|
||||
xudp,
|
||||
tfo,
|
||||
@@ -464,7 +435,6 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
icon,
|
||||
test_url,
|
||||
history,
|
||||
extra,
|
||||
capabilities: ProxyCapabilities {
|
||||
udp,
|
||||
xudp,
|
||||
@@ -483,7 +453,6 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
||||
icon,
|
||||
test_url,
|
||||
history,
|
||||
extra,
|
||||
udp,
|
||||
xudp,
|
||||
tfo,
|
||||
@@ -498,7 +467,6 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
||||
name,
|
||||
proxy_type,
|
||||
history,
|
||||
extra,
|
||||
id,
|
||||
hidden,
|
||||
icon,
|
||||
@@ -552,62 +520,3 @@ fn build_standalone(core_node_ids: &BTreeMap<String, String>) -> Vec<String> {
|
||||
);
|
||||
standalone
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn provider_echo_in_proxies_does_not_override_group_membership() -> anyhow::Result<()> {
|
||||
let view = ProxyViewBuilder::build(ProxyViewInput {
|
||||
runtime_group_order: vec!["A".into(), "B".into()],
|
||||
group_scopes: ["A", "B"]
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.into(),
|
||||
GroupScope {
|
||||
providers: vec![name.into()],
|
||||
exclude_types: vec![],
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
proxies: serde_json::from_value(json!({"proxies": {
|
||||
"A": {"all": ["node"], "type": "Selector"},
|
||||
"B": {"all": ["node"], "type": "Selector"},
|
||||
"node": {"name": "node", "provider-name": "A", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}
|
||||
}}))?,
|
||||
providers: Some(serde_json::from_value(json!({"providers": {
|
||||
"A": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}]},
|
||||
"B": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 123}]}]}
|
||||
}}))?),
|
||||
});
|
||||
for (group, expected) in view.groups.iter().zip([32, 123]) {
|
||||
let ProxyMemberRef::Node { record_id, .. } = &group.members[0] else {
|
||||
anyhow::bail!("Expected node")
|
||||
};
|
||||
let record = &view.records[record_id];
|
||||
assert_eq!(record.history.last().map(|entry| entry.delay), Some(expected));
|
||||
assert!(
|
||||
matches!(&record.source, ProxyNodeSource::Provider { provider_name, .. } if provider_name == &group.name)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn unavailable_provider_keeps_core_echo_interactable() -> anyhow::Result<()> {
|
||||
let view = ProxyViewBuilder::build(ProxyViewInput {
|
||||
runtime_group_order: vec!["A".into()],
|
||||
group_scopes: BTreeMap::new(),
|
||||
proxies: serde_json::from_value(json!({"proxies": {
|
||||
"A": {"all": ["node"], "type": "Selector"},
|
||||
"node": {"name": "node", "provider-name": "provider"}
|
||||
}}))?,
|
||||
providers: None,
|
||||
});
|
||||
assert!(matches!(&view.groups[0].members[0], ProxyMemberRef::Node { .. }));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
use super::Tray;
|
||||
use crate::{
|
||||
Type, cmd,
|
||||
config::Config,
|
||||
core::{
|
||||
handle,
|
||||
notify::{Refresh, announce},
|
||||
proxy_view::{ProxyMemberRef, ProxyNodeSource},
|
||||
},
|
||||
logging,
|
||||
};
|
||||
|
||||
const TRAY_DELAY_TEST_CONCURRENCY: usize = 10;
|
||||
const TRAY_DEFAULT_LATENCY_URL: &str = "http://cp.cloudflare.com/generate_204";
|
||||
|
||||
fn is_latency_test_url(raw: &str) -> bool {
|
||||
let raw = raw.trim();
|
||||
raw.split_once("://")
|
||||
.is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https"))
|
||||
&& tauri::Url::parse(raw).is_ok_and(|url| url.has_host() && matches!(url.scheme(), "http" | "https"))
|
||||
}
|
||||
|
||||
pub(super) fn test_url(
|
||||
group: &crate::core::proxy_view::ProxyGroupView,
|
||||
verge: &crate::config::IVerge,
|
||||
profiles: &crate::config::IProfiles,
|
||||
) -> String {
|
||||
let custom_url = profiles
|
||||
.current
|
||||
.as_ref()
|
||||
.and_then(|uid| profiles.get_item(uid).ok())
|
||||
.and_then(|profile| profile.latency_test_urls.as_ref())
|
||||
.and_then(|urls| urls.get(group.name.as_str()))
|
||||
.map(|url| url.as_str())
|
||||
.filter(|url| !url.trim().is_empty());
|
||||
let group_url = group.test_url.as_deref().filter(|url| !url.trim().is_empty());
|
||||
let default_url = verge
|
||||
.default_latency_test
|
||||
.as_deref()
|
||||
.filter(|url| !url.trim().is_empty());
|
||||
custom_url
|
||||
.or(group_url)
|
||||
.or(default_url)
|
||||
.filter(|&url| is_latency_test_url(url))
|
||||
.unwrap_or(TRAY_DEFAULT_LATENCY_URL)
|
||||
.trim()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
static RUNNING: parking_lot::Mutex<std::collections::BTreeSet<String>> =
|
||||
parking_lot::Mutex::new(std::collections::BTreeSet::new());
|
||||
|
||||
pub(super) fn is_running(group_name: &str) -> bool {
|
||||
RUNNING.lock().contains(group_name)
|
||||
}
|
||||
|
||||
pub(super) async fn test_proxy_group_delay(group_name: &str) {
|
||||
if !RUNNING.lock().insert(group_name.to_owned()) {
|
||||
return;
|
||||
}
|
||||
Tray::global().refresh_latency_item(group_name);
|
||||
let _running = scopeguard::guard(group_name, |name| {
|
||||
RUNNING.lock().remove(name);
|
||||
Tray::global().refresh_latency_item(name);
|
||||
});
|
||||
use futures::{StreamExt as _, stream};
|
||||
|
||||
let view = match cmd::proxy::get_proxy_view().await {
|
||||
Ok(view) => view,
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Tray, "Failed to read proxies for tray latency test: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
|
||||
let group = view
|
||||
.groups
|
||||
.iter()
|
||||
.chain(view.global.iter())
|
||||
.find(|group| group.name.as_str() == group_name);
|
||||
let Some(group) = group else {
|
||||
logging!(warn, Type::Tray, "Tray latency test group not found: {group_name}");
|
||||
return;
|
||||
};
|
||||
|
||||
let profiles = Config::profiles().await.latest_arc();
|
||||
let url = test_url(group, &verge, &profiles);
|
||||
let timeout = verge
|
||||
.default_latency_timeout
|
||||
.filter(|timeout| *timeout > 0)
|
||||
.unwrap_or(10000) as u32;
|
||||
|
||||
let mut targets: Vec<(std::string::String, Option<std::string::String>)> = Vec::with_capacity(group.members.len());
|
||||
for member in &group.members {
|
||||
match member {
|
||||
ProxyMemberRef::Node { name, record_id } => match view.records.get(record_id).map(|node| &node.source) {
|
||||
Some(ProxyNodeSource::Provider {
|
||||
provider_name,
|
||||
proxy_name,
|
||||
}) => targets.push((proxy_name.clone(), Some(provider_name.clone()))),
|
||||
_ => targets.push((name.clone(), None)),
|
||||
},
|
||||
ProxyMemberRef::Group { name } => targets.push((name.clone(), None)),
|
||||
ProxyMemberRef::Unresolved { .. } => {}
|
||||
}
|
||||
}
|
||||
let mihomo = handle::Handle::mihomo();
|
||||
|
||||
stream::iter(targets.into_iter().map(move |(member, provider)| {
|
||||
let url = url.clone();
|
||||
async move {
|
||||
let result = match provider {
|
||||
Some(provider_name) => {
|
||||
mihomo
|
||||
.healthcheck_node_in_provider(&provider_name, &member, &url, timeout)
|
||||
.await
|
||||
}
|
||||
None => mihomo.delay_proxy_by_name(&member, &url, timeout).await,
|
||||
};
|
||||
if let Err(err) = result {
|
||||
logging!(debug, Type::Tray, "Tray latency test failed: {member} - {err}");
|
||||
}
|
||||
}
|
||||
}))
|
||||
.buffer_unordered(TRAY_DELAY_TEST_CONCURRENCY)
|
||||
.for_each(|()| std::future::ready(()))
|
||||
.await;
|
||||
|
||||
if let Err(err) = cmd::proxy::sync_tray_proxy_selection().await {
|
||||
logging!(warn, Type::Tray, "Failed to refresh tray after latency test: {err}");
|
||||
}
|
||||
|
||||
announce(Refresh::Proxies);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn latency_urls_require_a_valid_http_host_and_port() {
|
||||
for invalid in [
|
||||
"http://?",
|
||||
"http://host:invalid",
|
||||
"http://bad host/",
|
||||
"ftp://localhost",
|
||||
"http:localhost",
|
||||
] {
|
||||
assert!(!is_latency_test_url(invalid), "{invalid}");
|
||||
}
|
||||
for valid in ["http://localhost/", " https://example.com/ ", "HTTP://localhost:8080/"] {
|
||||
assert!(is_latency_test_url(valid), "{valid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ define_menu! {
|
||||
outbound_modes => OUTBOUND_MODES, "tray_outbound_modes", "tray.outboundModes",
|
||||
profiles => PROFILES, "tray_profiles", "tray.profiles",
|
||||
proxies => PROXIES, "tray_proxies", "tray.proxies",
|
||||
test_latency => TEST_LATENCY, "tray_test_latency", "tray.testLatency",
|
||||
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
|
||||
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
|
||||
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
|
||||
|
||||
+135
-356
@@ -1,6 +1,5 @@
|
||||
use crate::config::{IProfilePreview, IVerge};
|
||||
use crate::core::lightweight;
|
||||
use crate::core::proxy_view::{ProxyGroupView, ProxyMemberRef, ProxyNodeSource, ProxyViewProviderState, ProxyViewV1};
|
||||
use crate::core::tray::menu_def::TrayAction;
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::singleton;
|
||||
@@ -15,6 +14,7 @@ use crate::{
|
||||
use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter};
|
||||
use clash_verge_logging::logging_error;
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri_plugin_mihomo::models::Proxies;
|
||||
use tokio::fs;
|
||||
|
||||
use super::handle;
|
||||
@@ -28,7 +28,6 @@ use tauri::{
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
};
|
||||
|
||||
mod latency;
|
||||
mod menu_def;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod speed_task;
|
||||
@@ -56,25 +55,8 @@ enum IconKind {
|
||||
Tun,
|
||||
}
|
||||
|
||||
struct LatencyMenuItem {
|
||||
item: MenuItem<Wry>,
|
||||
running: bool,
|
||||
proxies: Vec<ProxyLatencyItem>,
|
||||
provider_state: ProxyViewProviderState,
|
||||
}
|
||||
|
||||
struct ProxyLatencyItem {
|
||||
item: CheckMenuItem<Wry>,
|
||||
name: std::string::String,
|
||||
provider_name: Option<std::string::String>,
|
||||
text: std::string::String,
|
||||
}
|
||||
|
||||
pub struct Tray {
|
||||
limiter: SystemLimiter,
|
||||
proxy_hover_limiter: SystemLimiter,
|
||||
menu_update: tokio::sync::Mutex<()>,
|
||||
latency_items: parking_lot::Mutex<HashMap<std::string::String, LatencyMenuItem>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
speed_controller: speed_task::TraySpeedController,
|
||||
}
|
||||
@@ -148,9 +130,6 @@ impl Default for Tray {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock),
|
||||
proxy_hover_limiter: Limiter::new(Duration::from_secs(1), SystemClock),
|
||||
menu_update: tokio::sync::Mutex::new(()),
|
||||
latency_items: parking_lot::Mutex::new(HashMap::new()),
|
||||
#[cfg(target_os = "macos")]
|
||||
speed_controller: speed_task::TraySpeedController::new(),
|
||||
}
|
||||
@@ -216,7 +195,6 @@ impl Tray {
|
||||
}
|
||||
|
||||
async fn update_menu_internal(&self, app_handle: &AppHandle, include_proxy_groups: bool) -> Result<()> {
|
||||
let _update = self.menu_update.lock().await;
|
||||
let Some(tray) = app_handle.tray_by_id(TRAY_ID) else {
|
||||
logging!(warn, Type::Tray, "Failed to update tray menu: tray not found");
|
||||
return Ok(());
|
||||
@@ -241,133 +219,29 @@ impl Tray {
|
||||
let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default();
|
||||
let is_lightweight_mode = is_in_lightweight_mode();
|
||||
|
||||
let (menu, latency_items) = create_tray_menu(
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
tun_mode_available,
|
||||
profiles_preview,
|
||||
TrayMenuOptions {
|
||||
is_lightweight_mode,
|
||||
include_proxy_groups,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
tray.set_menu(Some(menu))?;
|
||||
let groups: Vec<_> = latency_items.keys().cloned().collect();
|
||||
*self.latency_items.lock() = latency_items;
|
||||
for group in groups {
|
||||
self.refresh_latency_item(&group);
|
||||
}
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
tray.set_menu(Some(
|
||||
create_tray_menu(
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
tun_mode_available,
|
||||
profiles_preview,
|
||||
TrayMenuOptions {
|
||||
is_lightweight_mode,
|
||||
include_proxy_groups,
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
);
|
||||
|
||||
logging!(debug, Type::Tray, "托盘菜单更新成功");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn refresh_proxy_items(&'static self) {
|
||||
let Ok(update) = self.menu_update.try_lock() else {
|
||||
return;
|
||||
};
|
||||
if self.latency_items.lock().is_empty() || handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
let Ok(Ok(view)) = tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if self
|
||||
.latency_items
|
||||
.lock()
|
||||
.values()
|
||||
.any(|entry| entry.provider_state != view.provider_state)
|
||||
{
|
||||
// Provider fallback changes member identities; rebuild through the existing queue.
|
||||
drop(update);
|
||||
logging_error!(Type::Tray, cmd::proxy::sync_tray_proxy_selection().await);
|
||||
return;
|
||||
}
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
let profiles = Config::profiles().await.latest_arc();
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
handle::Handle::app_handle().run_on_main_thread(move || {
|
||||
// Keep rebuilding serialized until these handles have received their snapshot.
|
||||
let _update = update;
|
||||
let mut items = self.latency_items.lock();
|
||||
for (group_name, entry) in items.iter_mut() {
|
||||
let Some(group) = find_group(&view, group_name) else {
|
||||
continue;
|
||||
};
|
||||
let url = latency::test_url(group, &verge, &profiles);
|
||||
let mut members = HashMap::with_capacity(group.members.len());
|
||||
for member in &group.members {
|
||||
members
|
||||
.entry((member_name(member), member_provider(&view, member)))
|
||||
.or_insert(member);
|
||||
}
|
||||
for proxy in &mut entry.proxies {
|
||||
let Some(member) = members.get(&(proxy.name.as_str(), proxy.provider_name.as_deref())) else {
|
||||
continue;
|
||||
};
|
||||
let text = proxy_item_text(&view, member, &url, &verge);
|
||||
let checked = group.now.as_ref() == Some(&proxy.name);
|
||||
if text != proxy.text {
|
||||
match proxy.item.set_text(&text) {
|
||||
Ok(()) => proxy.text = text,
|
||||
Err(err) => logging!(warn, Type::Tray, "Failed to refresh proxy latency: {err}"),
|
||||
}
|
||||
}
|
||||
match proxy.item.is_checked() {
|
||||
Ok(current) if current != checked => {
|
||||
logging_error!(Type::Tray, proxy.item.set_checked(checked))
|
||||
}
|
||||
Err(err) => logging!(warn, Type::Tray, "Failed to read proxy selection: {err}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh_latency_item(&self, group_name: &str) {
|
||||
let group_name = group_name.to_owned();
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
handle::Handle::app_handle().run_on_main_thread(move || {
|
||||
Self::global().refresh_latency_item_on_main_thread(&group_name);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh_latency_item_on_main_thread(&self, group_name: &str) {
|
||||
let running = latency::is_running(group_name);
|
||||
let item = {
|
||||
let mut items = self.latency_items.lock();
|
||||
items.get_mut(group_name).and_then(|entry| {
|
||||
if entry.running == running {
|
||||
return None;
|
||||
}
|
||||
entry.running = running;
|
||||
Some(entry.item.clone())
|
||||
})
|
||||
};
|
||||
if let Some(item) = item {
|
||||
let label = if running {
|
||||
clash_verge_i18n::t!("tray.testingLatency")
|
||||
} else {
|
||||
clash_verge_i18n::t!("tray.testLatency")
|
||||
};
|
||||
logging_error!(Type::Tray, item.set_text(label));
|
||||
logging_error!(Type::Tray, item.set_enabled(!running));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_icon(&self, verge: &IVerge) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
|
||||
@@ -583,175 +457,96 @@ fn create_profile_menu_item(
|
||||
.collect()
|
||||
}
|
||||
|
||||
const UNTESTED_DELAY_TEXT: &str = "-ms";
|
||||
const TIMEOUT_DELAY_TEXT: &str = "T/O";
|
||||
const TRAY_GROUP_DEPTH_LIMIT: usize = 8;
|
||||
|
||||
const fn member_name(member: &ProxyMemberRef) -> &str {
|
||||
match member {
|
||||
ProxyMemberRef::Node { name, .. }
|
||||
| ProxyMemberRef::Group { name }
|
||||
| ProxyMemberRef::Unresolved { name, .. } => name.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_group<'a>(view: &'a ProxyViewV1, name: &str) -> Option<&'a ProxyGroupView> {
|
||||
view.groups
|
||||
.iter()
|
||||
.chain(view.global.iter())
|
||||
.find(|group| group.name.as_str() == name)
|
||||
}
|
||||
|
||||
/// The delay recorded for `url`, falling back to the node's latest measurement under any URL, so a
|
||||
/// node the core tested on its own still shows a value.
|
||||
fn node_delay(view: &ProxyViewV1, record_id: &str, url: &str) -> Option<u16> {
|
||||
view.records
|
||||
.get(record_id)
|
||||
.map(|node| node.extra.get(url).map_or(&node.history, |extra| &extra.history))
|
||||
.and_then(|history| history.last())
|
||||
.map(|history| history.delay)
|
||||
}
|
||||
|
||||
/// A group row shows the delay of the node the group currently selects. The core never writes a
|
||||
/// history for the group itself, so `now` is followed until the chain reaches a node.
|
||||
fn selected_member_delay(view: &ProxyViewV1, group_name: &str, url: &str) -> Option<u16> {
|
||||
let mut current = group_name;
|
||||
for _ in 0..TRAY_GROUP_DEPTH_LIMIT {
|
||||
let Some(group) = find_group(view, current) else { break };
|
||||
let Some(now) = group.now.as_deref().filter(|now| !now.is_empty() && *now != current) else {
|
||||
break;
|
||||
};
|
||||
let Some(member) = group.members.iter().find(|member| member_name(member) == now) else {
|
||||
break;
|
||||
};
|
||||
match member {
|
||||
ProxyMemberRef::Node { record_id, .. } => return node_delay(view, record_id, url),
|
||||
ProxyMemberRef::Group { name } => current = name.as_str(),
|
||||
ProxyMemberRef::Unresolved { .. } => break,
|
||||
}
|
||||
}
|
||||
find_group(view, group_name)
|
||||
.map(|group| group.extra.get(url).map_or(&group.history, |extra| &extra.history))
|
||||
.and_then(|history| history.last())
|
||||
.map(|history| history.delay)
|
||||
}
|
||||
|
||||
fn member_provider<'a>(view: &'a ProxyViewV1, member: &ProxyMemberRef) -> Option<&'a str> {
|
||||
let ProxyMemberRef::Node { record_id, .. } = member else {
|
||||
return None;
|
||||
};
|
||||
match &view.records.get(record_id)?.source {
|
||||
ProxyNodeSource::Provider { provider_name, .. } => Some(provider_name),
|
||||
ProxyNodeSource::Core { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_item_text(view: &ProxyViewV1, member: &ProxyMemberRef, url: &str, verge: &IVerge) -> std::string::String {
|
||||
let timeout = verge
|
||||
.default_latency_timeout
|
||||
.filter(|timeout| *timeout > 0)
|
||||
.unwrap_or(10000);
|
||||
let delay = match member {
|
||||
ProxyMemberRef::Node { record_id, .. } => node_delay(view, record_id, url),
|
||||
ProxyMemberRef::Group { name } => selected_member_delay(view, name, url),
|
||||
ProxyMemberRef::Unresolved { .. } => None,
|
||||
};
|
||||
let delay_text = delay.map_or_else(
|
||||
|| UNTESTED_DELAY_TEXT.to_owned(),
|
||||
|delay| {
|
||||
if delay == 0 || u32::from(delay) >= timeout as u32 {
|
||||
TIMEOUT_DELAY_TEXT.to_owned()
|
||||
} else {
|
||||
format!("{delay}ms")
|
||||
}
|
||||
},
|
||||
);
|
||||
format!("{} | {delay_text}", member_name(member))
|
||||
}
|
||||
|
||||
fn create_subcreate_proxy_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
proxy_mode: &str,
|
||||
view: Option<ProxyViewV1>,
|
||||
latency_items: &mut HashMap<std::string::String, LatencyMenuItem>,
|
||||
texts: &MenuTexts,
|
||||
verge: &IVerge,
|
||||
profiles: &crate::config::IProfiles,
|
||||
proxy_group_order_map: Option<HashMap<String, usize>>,
|
||||
proxy_nodes_data: Option<Proxies>,
|
||||
) -> Vec<Submenu<Wry>> {
|
||||
let Some(view) = view else { return Vec::new() };
|
||||
view.groups
|
||||
.iter()
|
||||
.chain(view.global.iter())
|
||||
.filter_map(|group| {
|
||||
if group.hidden.unwrap_or_default() || (proxy_mode == "global") != (group.name == "GLOBAL") {
|
||||
return None;
|
||||
}
|
||||
let url = latency::test_url(group, verge, profiles);
|
||||
let proxies: Vec<ProxyLatencyItem> = group
|
||||
.members
|
||||
.iter()
|
||||
.filter_map(|member| {
|
||||
let name = member_name(member);
|
||||
let text = proxy_item_text(&view, member, &url, verge);
|
||||
let checked = group.now.as_deref() == Some(name);
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
format!("proxy_{}_{}", group.name, name),
|
||||
&text,
|
||||
true,
|
||||
checked,
|
||||
None::<&str>,
|
||||
)
|
||||
.map(|item| ProxyLatencyItem {
|
||||
item,
|
||||
name: name.to_owned(),
|
||||
provider_name: member_provider(&view, member).map(str::to_owned),
|
||||
text,
|
||||
let proxy_submenus: Vec<Submenu<Wry>> = {
|
||||
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
|
||||
|
||||
// TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息
|
||||
if let Some(proxy_nodes_data) = proxy_nodes_data {
|
||||
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
|
||||
let should_show = match proxy_mode {
|
||||
"global" => group_name == "GLOBAL",
|
||||
_ => group_name != "GLOBAL",
|
||||
} && !group_data.hidden.unwrap_or_default();
|
||||
|
||||
if !should_show {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(all_proxies) = group_data.all.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let now_proxy = group_data.now.as_deref().unwrap_or_default();
|
||||
|
||||
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
|
||||
.iter()
|
||||
.filter_map(|proxy_str| {
|
||||
let is_selected = *proxy_str == now_proxy;
|
||||
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
|
||||
|
||||
let delay_text = proxy_nodes_data
|
||||
.proxies
|
||||
.get(proxy_str)
|
||||
.and_then(|h| h.history.last())
|
||||
.map(|h| match h.delay {
|
||||
0 => "-ms".into(),
|
||||
delay if delay >= 10000 => "-ms".into(),
|
||||
_ => format!("{}ms", h.delay),
|
||||
})
|
||||
.unwrap_or_else(|| "-ms".into());
|
||||
|
||||
let display_text = format!("{} | {}", proxy_str, delay_text);
|
||||
|
||||
CheckMenuItem::with_id(app_handle, item_id, display_text, true, is_selected, None::<&str>)
|
||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e))
|
||||
.ok()
|
||||
})
|
||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {e}"))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
let group_items: Vec<_> = proxies.iter().map(|proxy| proxy.item.clone()).collect();
|
||||
if group_items.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let running = latency::is_running(&group.name);
|
||||
let test_item = MenuItem::with_id(
|
||||
app_handle,
|
||||
format!("{}_{}", MenuIds::TEST_LATENCY, group.name),
|
||||
if running {
|
||||
clash_verge_i18n::t!("tray.testingLatency")
|
||||
.collect();
|
||||
|
||||
if group_items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let group_display_name = group_name.to_string();
|
||||
|
||||
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> =
|
||||
group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>).collect();
|
||||
|
||||
if let Ok(submenu) = Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group_name),
|
||||
group_display_name,
|
||||
true,
|
||||
&group_items_refs,
|
||||
) {
|
||||
let insertion_index = submenus.len();
|
||||
submenus.push((group_name.into(), insertion_index, submenu));
|
||||
} else {
|
||||
texts.test_latency.clone()
|
||||
},
|
||||
!running,
|
||||
None::<&str>,
|
||||
)
|
||||
.ok()?;
|
||||
latency_items.insert(
|
||||
group.name.clone(),
|
||||
LatencyMenuItem {
|
||||
item: test_item.clone(),
|
||||
running,
|
||||
proxies,
|
||||
provider_state: view.provider_state,
|
||||
},
|
||||
);
|
||||
let separator = PredefinedMenuItem::separator(app_handle).ok()?;
|
||||
let mut items: Vec<&dyn IsMenuItem<Wry>> = vec![&test_item, &separator];
|
||||
items.extend(group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>));
|
||||
Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group.name),
|
||||
&group.name,
|
||||
true,
|
||||
&items,
|
||||
)
|
||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy group submenu: {e}"))
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(order_map) = proxy_group_order_map.as_ref() {
|
||||
submenus.sort_by(|(name_a, original_index_a, _), (name_b, original_index_b, _)| {
|
||||
match (order_map.get(name_a), order_map.get(name_b)) {
|
||||
(Some(index_a), Some(index_b)) => index_a.cmp(index_b),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => original_index_a.cmp(original_index_b),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submenus.into_iter().map(|(_, _, submenu)| submenu).collect()
|
||||
};
|
||||
proxy_submenus
|
||||
}
|
||||
|
||||
fn create_proxy_menu_item(
|
||||
@@ -798,38 +593,54 @@ async fn create_tray_menu(
|
||||
tun_mode_available: bool,
|
||||
profiles_preview: Vec<IProfilePreview<'_>>,
|
||||
options: TrayMenuOptions,
|
||||
) -> Result<(tauri::menu::Menu<Wry>, HashMap<std::string::String, LatencyMenuItem>)> {
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
|
||||
let mut verge_settings = Config::verge().await.latest_arc();
|
||||
let fetch_proxy_groups =
|
||||
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
||||
|
||||
// Proxy data must remain optional so a stopped Core cannot roll back unrelated settings.
|
||||
let proxy_view = if fetch_proxy_groups {
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(1000),
|
||||
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok)
|
||||
// TODO: should update tray menu again when it was timeout error
|
||||
let (proxy_nodes_data, runtime_proxy_groups_order) = if fetch_proxy_groups {
|
||||
let proxy_nodes_data =
|
||||
tokio::time::timeout(Duration::from_millis(1000), handle::Handle::mihomo().get_proxies())
|
||||
.await
|
||||
.map_or(None, |res| res.ok());
|
||||
|
||||
let runtime = Config::runtime().await.latest_arc();
|
||||
let runtime_proxy_groups_order = runtime.config.as_ref().map(|config| {
|
||||
config
|
||||
.get("proxy-groups")
|
||||
.and_then(|groups| groups.as_sequence())
|
||||
.map(|groups| {
|
||||
groups
|
||||
.iter()
|
||||
.filter_map(|group| group.get("name"))
|
||||
.filter_map(|name| name.as_str())
|
||||
.enumerate()
|
||||
.map(|(index, name)| (name.into(), index))
|
||||
.collect::<HashMap<String, usize>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
(proxy_nodes_data, runtime_proxy_groups_order)
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
if fetch_proxy_groups {
|
||||
verge_settings = Config::verge().await.latest_arc();
|
||||
}
|
||||
|
||||
let profiles_config = Config::profiles().await.latest_arc();
|
||||
|
||||
let tray_proxy_groups_display_mode = verge_settings
|
||||
.tray_proxy_groups_display_mode
|
||||
.as_deref()
|
||||
.unwrap_or("default");
|
||||
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable";
|
||||
|
||||
let proxy_group_order_map = runtime_proxy_groups_order;
|
||||
|
||||
let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false);
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
@@ -909,17 +720,9 @@ async fn create_tray_menu(
|
||||
&profile_menu_items_refs,
|
||||
)?;
|
||||
|
||||
let mut latency_items = HashMap::new();
|
||||
let (proxies_menu, inline_proxy_items) = if include_proxy_groups {
|
||||
let proxy_sub_menus = create_subcreate_proxy_menu_item(
|
||||
app_handle,
|
||||
current_proxy_mode,
|
||||
proxy_view,
|
||||
&mut latency_items,
|
||||
&texts,
|
||||
&verge_settings,
|
||||
&profiles_config,
|
||||
);
|
||||
let proxy_sub_menus =
|
||||
create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data);
|
||||
|
||||
match tray_proxy_groups_display_mode {
|
||||
"default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?,
|
||||
@@ -1063,25 +866,10 @@ async fn create_tray_menu(
|
||||
]);
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
|
||||
Ok((menu, latency_items))
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) {
|
||||
let refresh_proxies = match tray_event {
|
||||
TrayIconEvent::Enter { .. } => Tray::global().proxy_hover_limiter.check(),
|
||||
TrayIconEvent::Click {
|
||||
button: MouseButton::Left | MouseButton::Right,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} => true,
|
||||
_ => false,
|
||||
};
|
||||
if refresh_proxies {
|
||||
AsyncHandler::spawn(|| async {
|
||||
Tray::global().refresh_proxy_items().await;
|
||||
});
|
||||
}
|
||||
|
||||
if matches!(
|
||||
tray_event,
|
||||
TrayIconEvent::Move { .. } | TrayIconEvent::Leave { .. } | TrayIconEvent::Enter { .. }
|
||||
@@ -1203,15 +991,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
};
|
||||
feat::toggle_proxy_profile(profile_index.into()).await;
|
||||
}
|
||||
id if id.starts_with(MenuIds::TEST_LATENCY) => {
|
||||
let group_name = id
|
||||
.strip_prefix(MenuIds::TEST_LATENCY)
|
||||
.and_then(|rest| rest.strip_prefix('_'));
|
||||
if let Some(group_name) = group_name {
|
||||
logging!(info, Type::Tray, "Tray menu: test group latency {}", group_name);
|
||||
latency::test_proxy_group_delay(group_name).await;
|
||||
}
|
||||
}
|
||||
id if id.starts_with("proxy_") => {
|
||||
let rest = match id.strip_prefix("proxy_") {
|
||||
Some(r) => r,
|
||||
|
||||
@@ -16,7 +16,7 @@ fn main() -> ExitCode {
|
||||
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
|
||||
fn main() -> ExitCode {
|
||||
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
let worker_limit = std::cmp::min(default_parallelism, 8);
|
||||
let worker_limit = std::cmp::min(default_parallelism, 4);
|
||||
let blocking_limit = 2 * worker_limit;
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
|
||||
@@ -27,8 +27,12 @@ const TRAY_STATUS_ITEM_EXTRA_WIDTH: f64 = 30.0;
|
||||
const TRAY_STATUS_ITEM_MIN_LENGTH: f64 = 58.0;
|
||||
const NS_VARIABLE_STATUS_ITEM_LENGTH: f64 = -1.0;
|
||||
|
||||
/// (button_height, mono_advance, attributes).
|
||||
type CachedAttrs = (f64, f64, Retained<NSDictionary<NSString, AnyObject>>);
|
||||
|
||||
thread_local! {
|
||||
static LAST_DISPLAY_STR: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
static CACHED_ATTRS: RefCell<Option<CachedAttrs>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
fn format_tray_speed(up: u64, down: u64) -> String {
|
||||
@@ -74,8 +78,26 @@ fn create_attributed_string(
|
||||
}
|
||||
}
|
||||
|
||||
fn status_item_length_for_speed(attr_str: &NSAttributedString) -> f64 {
|
||||
(attr_str.size().width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH)
|
||||
/// Attributes and per-glyph advance, rebuilt only when the button height changes.
|
||||
fn cached_attributes(button_height: f64) -> (f64, Retained<NSDictionary<NSString, AnyObject>>) {
|
||||
CACHED_ATTRS.with(|cache| {
|
||||
if let Some((height, advance, attrs)) = cache.borrow().as_ref()
|
||||
&& *height == button_height
|
||||
{
|
||||
return (*advance, attrs.clone());
|
||||
}
|
||||
let attrs = build_attributes(button_height);
|
||||
let probe = NSString::from_str("0");
|
||||
let advance = create_attributed_string(&probe, Some(&attrs)).size().width;
|
||||
*cache.borrow_mut() = Some((button_height, advance, attrs.clone()));
|
||||
(advance, attrs)
|
||||
})
|
||||
}
|
||||
|
||||
/// Longest line times the monospaced advance; matches `size().width` for the ASCII-only speed text.
|
||||
fn arithmetic_width(text: &str, advance: f64) -> f64 {
|
||||
let glyphs = text.split('\n').map(str::len).max().unwrap_or(0) as f64;
|
||||
glyphs * advance
|
||||
}
|
||||
|
||||
fn sync_click_target_frame(button: &NSStatusBarButton) {
|
||||
@@ -89,7 +111,7 @@ fn sync_click_target_frame(button: &NSStatusBarButton) {
|
||||
}
|
||||
|
||||
/// `with_inner_tray_icon` should run this on the main thread; fail safely if it does not.
|
||||
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSString, show_speed: bool) {
|
||||
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, show_speed: bool) {
|
||||
let Some(mtm) = MainThreadMarker::new() else {
|
||||
logging!(warn, Type::Tray, "托盘速率富文本设置跳过:非主线程调用");
|
||||
return;
|
||||
@@ -97,17 +119,19 @@ fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSStrin
|
||||
let Some(button) = status_item.button(mtm) else {
|
||||
return;
|
||||
};
|
||||
let ns_string = NSString::from_str(text);
|
||||
let attr_str = if show_speed {
|
||||
let attrs = build_attributes(button.bounds().size.height);
|
||||
let (advance, attrs) = cached_attributes(button.bounds().size.height);
|
||||
let attrs: &NSDictionary<NSString, AnyObject> = &attrs;
|
||||
let attr_str = create_attributed_string(text, Some(attrs));
|
||||
let length = status_item_length_for_speed(&attr_str);
|
||||
let attr_str = create_attributed_string(&ns_string, Some(attrs));
|
||||
let width = arithmetic_width(text, advance);
|
||||
let length = (width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH);
|
||||
if status_item.length() != length {
|
||||
status_item.setLength(length);
|
||||
}
|
||||
attr_str
|
||||
} else {
|
||||
create_attributed_string(text, None)
|
||||
create_attributed_string(&ns_string, None)
|
||||
};
|
||||
button.setAttributedTitle(&attr_str);
|
||||
sync_click_target_frame(&button);
|
||||
@@ -121,8 +145,7 @@ pub fn set_speed_attributed_title(status_item: &NSStatusItem, up: u64, down: u64
|
||||
return;
|
||||
}
|
||||
*last = speed_text;
|
||||
let ns_string = NSString::from_str(last.as_str());
|
||||
apply_status_item_attributed_title(status_item, &ns_string, true);
|
||||
apply_status_item_attributed_title(status_item, last.as_str(), true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -130,7 +153,6 @@ pub fn clear_speed_attributed_title(status_item: &NSStatusItem) {
|
||||
LAST_DISPLAY_STR.with(|last| {
|
||||
last.borrow_mut().clear();
|
||||
});
|
||||
let empty = NSString::from_str("");
|
||||
status_item.setLength(NS_VARIABLE_STATUS_ITEM_LENGTH);
|
||||
apply_status_item_attributed_title(status_item, &empty, false);
|
||||
apply_status_item_attributed_title(status_item, "", false);
|
||||
}
|
||||
|
||||
@@ -47,10 +47,7 @@ import {
|
||||
useCoreDataStatus,
|
||||
useProxiesData,
|
||||
} from '@/providers/app-data-context'
|
||||
import { syncTrayProxySelection } from '@/services/cmds'
|
||||
import delayManager from '@/services/delay'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { profileTestUrls } from '@/services/proxy-test-url'
|
||||
import {
|
||||
findCurrentGroupMember,
|
||||
getRecord,
|
||||
@@ -678,20 +675,6 @@ export const CurrentProxyCard = () => {
|
||||
[proxyView],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedGroup) return
|
||||
const customUrl = currentProfile
|
||||
? profileTestUrls(currentProfile)[selectedGroup.name]
|
||||
: undefined
|
||||
delayManager.setUrl(
|
||||
selectedGroup.name,
|
||||
customUrl?.trim() ||
|
||||
selectedGroup.testUrl?.trim() ||
|
||||
verge?.default_latency_test?.trim() ||
|
||||
'http://cp.cloudflare.com/generate_204',
|
||||
)
|
||||
}, [selectedGroup, currentProfile, verge?.default_latency_test])
|
||||
|
||||
const unsortedProxyOptions = useMemo(
|
||||
() => optionsForGroup(selectedGroup),
|
||||
[optionsForGroup, selectedGroup],
|
||||
@@ -924,12 +907,6 @@ export const CurrentProxyCard = () => {
|
||||
}
|
||||
|
||||
refreshProxy()
|
||||
mutate(() => syncTrayProxySelection(), {
|
||||
id: 'sync-tray-selection',
|
||||
errorNotice: false,
|
||||
}).catch((error) => {
|
||||
console.error('[CurrentProxyCard] Failed to sync tray latency:', error)
|
||||
})
|
||||
})
|
||||
|
||||
const proxyOptions = useMemo(
|
||||
|
||||
@@ -75,6 +75,7 @@ interface ProxyChainProps {
|
||||
onMarkUnsavedChanges?: () => void
|
||||
mode?: string
|
||||
selectedGroup?: string | null
|
||||
onExitChainMode: () => void
|
||||
}
|
||||
|
||||
interface ProxyChainItemProps {
|
||||
@@ -331,6 +332,7 @@ export const ProxyChain = ({
|
||||
onMarkUnsavedChanges,
|
||||
mode,
|
||||
selectedGroup,
|
||||
onExitChainMode,
|
||||
}: ProxyChainProps) => {
|
||||
const theme = useTheme()
|
||||
const { t } = useTranslation()
|
||||
@@ -696,13 +698,18 @@ export const ProxyChain = ({
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
height: '100%',
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<Typography>{t('proxies.page.chain.empty')}</Typography>
|
||||
<Button size="small" variant="outlined" onClick={onExitChainMode}>
|
||||
{t('shared.actions.close')}
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<DragDropProvider
|
||||
|
||||
@@ -9,7 +9,7 @@ import VisibilityOffRounded from '@mui/icons-material/VisibilityOffRounded'
|
||||
import VisibilityRounded from '@mui/icons-material/VisibilityRounded'
|
||||
import WifiTetheringOffRounded from '@mui/icons-material/WifiTetheringOffRounded'
|
||||
import WifiTetheringRounded from '@mui/icons-material/WifiTetheringRounded'
|
||||
import { Box, IconButton, type SxProps } from '@mui/material'
|
||||
import { Box, IconButton, type SxProps, TextField } from '@mui/material'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { flushSync } from 'react-dom'
|
||||
@@ -18,11 +18,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager from '@/services/delay'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
|
||||
import { BaseSearchBox, type SearchState } from '../base'
|
||||
|
||||
import { ProxyTestUrlInput } from './proxy-test-url-input'
|
||||
import type { ProxySortType } from './use-filter-sort'
|
||||
import type { HeadState } from './use-head-state'
|
||||
|
||||
@@ -70,10 +69,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
delayManager.setUrl(
|
||||
groupName,
|
||||
testUrl?.trim() || url?.trim() || defaultLatencyUrl,
|
||||
)
|
||||
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
|
||||
}, [groupName, testUrl, defaultLatencyUrl, url])
|
||||
|
||||
// 过滤输入是高频操作,且每次都会触发整组代理的重新过滤/排序与虚拟列表重渲染,
|
||||
@@ -116,8 +112,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
)}
|
||||
|
||||
{textState === 'url' && (
|
||||
<ProxyTestUrlInput
|
||||
name={groupName}
|
||||
<TextField
|
||||
inputRef={inputRef}
|
||||
autoComplete="new-password"
|
||||
hiddenLabel
|
||||
@@ -130,7 +125,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onCommit={(testUrl) => onHeadState({ testUrl })}
|
||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
||||
sx={{ flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
||||
/>
|
||||
)}
|
||||
@@ -181,7 +176,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
if (testUrl?.trim() && textState !== 'filter') {
|
||||
onHeadState({ textState: 'url' })
|
||||
}
|
||||
if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) {
|
||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ interface GroupSelectMenuProps {
|
||||
interface ProxyGroupsChainProps {
|
||||
mode: string
|
||||
chainConfigData?: string | null
|
||||
onExitChainMode: () => void
|
||||
availableGroups: any[]
|
||||
activeSelectedGroup: string | null
|
||||
showScrollTop: boolean
|
||||
@@ -305,6 +306,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
const {
|
||||
mode,
|
||||
chainConfigData,
|
||||
onExitChainMode,
|
||||
availableGroups,
|
||||
activeSelectedGroup,
|
||||
showScrollTop,
|
||||
@@ -521,6 +523,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
chainConfigData={chainConfigData}
|
||||
mode={mode}
|
||||
selectedGroup={activeSelectedGroup}
|
||||
onExitChainMode={onExitChainMode}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -23,9 +23,7 @@ import { useProfiles } from '@/hooks/use-profiles'
|
||||
import { useProxySelection } from '@/hooks/use-proxy-selection'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { useProxiesData, useSystemData } from '@/providers/app-data-context'
|
||||
import { syncTrayProxySelection } from '@/services/cmds'
|
||||
import delayManager from '@/services/delay'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import {
|
||||
isInteractableMember,
|
||||
resolveMember,
|
||||
@@ -44,6 +42,7 @@ import {
|
||||
ProxyGroupNavigator,
|
||||
} from './proxy-group-navigator'
|
||||
import { ProxyRender } from './proxy-render'
|
||||
import type { HeadState } from './use-head-state'
|
||||
import {
|
||||
PROXY_GROUP_HEADER_SENSORS,
|
||||
useProxyGroupHeaderLayout,
|
||||
@@ -70,6 +69,7 @@ interface Props {
|
||||
mode: string
|
||||
isChainMode?: boolean
|
||||
chainConfigData?: string | null
|
||||
onExitChainMode: () => void
|
||||
}
|
||||
|
||||
function useEmptyRenderList() {
|
||||
@@ -90,11 +90,11 @@ function useProxyRenderState(
|
||||
) {
|
||||
const { verge } = useVerge()
|
||||
const { proxyView } = useProxiesData()
|
||||
const { renderList, onProxies, onHeadState } = useRenderList(
|
||||
mode,
|
||||
isChainMode,
|
||||
activeSelectedGroup,
|
||||
)
|
||||
const {
|
||||
renderList,
|
||||
onProxies,
|
||||
onHeadState: setListHeadState,
|
||||
} = useRenderList(mode, isChainMode, activeSelectedGroup)
|
||||
const scrollPositionKey = useMemo(
|
||||
() =>
|
||||
isChainMode
|
||||
@@ -103,6 +103,14 @@ function useProxyRenderState(
|
||||
[activeSelectedGroup, isChainMode, mode],
|
||||
)
|
||||
|
||||
const onHeadState = useCallback(
|
||||
(groupName: string, patch: Partial<HeadState>) => {
|
||||
if (patch.open) onProxies()
|
||||
setListHeadState(groupName, patch)
|
||||
},
|
||||
[onProxies, setListHeadState],
|
||||
)
|
||||
|
||||
const timeout = verge?.default_latency_timeout || 10000
|
||||
|
||||
const handleCheckAll = useStableCallback(
|
||||
@@ -135,12 +143,6 @@ function useProxyRenderState(
|
||||
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error)
|
||||
} finally {
|
||||
onProxies()
|
||||
mutate(() => syncTrayProxySelection(), {
|
||||
id: 'sync-tray-selection',
|
||||
errorNotice: false,
|
||||
}).catch((error) => {
|
||||
console.error('[ProxyGroups] Failed to sync tray latency:', error)
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -190,8 +192,9 @@ function useProxyRenderState(
|
||||
function ChainProxyGroups(props: {
|
||||
mode: string
|
||||
chainConfigData?: string | null
|
||||
onExitChainMode: () => void
|
||||
}) {
|
||||
const { mode, chainConfigData } = props
|
||||
const { mode, chainConfigData, onExitChainMode } = props
|
||||
const { proxyView } = useProxiesData()
|
||||
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
|
||||
|
||||
@@ -346,6 +349,7 @@ function ChainProxyGroups(props: {
|
||||
<ProxyGroupsChain
|
||||
mode={mode}
|
||||
chainConfigData={chainConfigData}
|
||||
onExitChainMode={onExitChainMode}
|
||||
availableGroups={availableGroups}
|
||||
activeSelectedGroup={activeSelectedGroup}
|
||||
showScrollTop={showScrollTop}
|
||||
@@ -617,7 +621,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
}
|
||||
|
||||
export const ProxyGroups = (props: Props) => {
|
||||
const { mode, isChainMode = false, chainConfigData } = props
|
||||
const { mode, isChainMode = false, chainConfigData, onExitChainMode } = props
|
||||
const { profiles, isLoading: isProfilesLoading } = useProfiles()
|
||||
const { isProxyViewPending } = useProxiesData()
|
||||
const { isRunningModePending } = useSystemData()
|
||||
@@ -639,7 +643,11 @@ export const ProxyGroups = (props: Props) => {
|
||||
return <ProxyEmptyState reason={listState.reason} />
|
||||
case 'render':
|
||||
return isChainMode ? (
|
||||
<ChainProxyGroups mode={mode} chainConfigData={chainConfigData} />
|
||||
<ChainProxyGroups
|
||||
mode={mode}
|
||||
chainConfigData={chainConfigData}
|
||||
onExitChainMode={onExitChainMode}
|
||||
/>
|
||||
) : (
|
||||
<NormalProxyGroups mode={mode} />
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SortByAlphaRounded,
|
||||
SortRounded,
|
||||
} from '@mui/icons-material'
|
||||
import { Box, IconButton, type SxProps } from '@mui/material'
|
||||
import { Box, IconButton, TextField, type SxProps } from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -20,9 +20,8 @@ import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager from '@/services/delay'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
|
||||
import { ProxyTestUrlInput } from './proxy-test-url-input'
|
||||
import type { ProxySortType } from './use-filter-sort'
|
||||
import type { HeadState } from './use-head-state'
|
||||
|
||||
@@ -73,10 +72,7 @@ export const ProxyHead = ({
|
||||
'http://cp.cloudflare.com/generate_204'
|
||||
|
||||
useEffect(() => {
|
||||
delayManager.setUrl(
|
||||
groupName,
|
||||
testUrl?.trim() || url?.trim() || defaultLatencyUrl,
|
||||
)
|
||||
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
|
||||
}, [groupName, testUrl, defaultLatencyUrl, url])
|
||||
|
||||
return (
|
||||
@@ -101,7 +97,7 @@ export const ProxyHead = ({
|
||||
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`)
|
||||
onHeadState({ textState: 'url' })
|
||||
}
|
||||
if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) {
|
||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||
return
|
||||
}
|
||||
@@ -192,8 +188,7 @@ export const ProxyHead = ({
|
||||
)}
|
||||
|
||||
{textState === 'url' && (
|
||||
<ProxyTestUrlInput
|
||||
name={groupName}
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
autoFocus={autoFocus}
|
||||
hiddenLabel
|
||||
@@ -202,7 +197,7 @@ export const ProxyHead = ({
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder={t('proxies.page.placeholders.delayCheckUrl')}
|
||||
onCommit={(testUrl) => onHeadState({ testUrl })}
|
||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
||||
sx={{ ml: 0.5, flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { TextField, type TextFieldProps } from '@mui/material'
|
||||
import { useEffect, useEffectEvent, useRef, useState } from 'react'
|
||||
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
|
||||
type Props = Omit<TextFieldProps, 'value' | 'onChange'> & {
|
||||
value: string
|
||||
onCommit: (url: string) => void
|
||||
}
|
||||
|
||||
export const ProxyTestUrlInput = ({ value, name, ...props }: Props) => {
|
||||
const { profiles } = useProfiles()
|
||||
return (
|
||||
<DraftTestUrlInput
|
||||
key={JSON.stringify([profiles?.current, name])}
|
||||
name={name}
|
||||
value={value}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DraftTestUrlInput = ({ value, onCommit, ...props }: Props) => {
|
||||
const [draft, setDraft] = useState<string | null>(null)
|
||||
const pendingRef = useRef<string | null>(null)
|
||||
const commitOnUnmount = useEffectEvent(() => {
|
||||
if (pendingRef.current !== null && pendingRef.current !== value)
|
||||
onCommit(pendingRef.current)
|
||||
})
|
||||
useEffect(() => () => commitOnUnmount(), [])
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
value={draft ?? value}
|
||||
onChange={(event) => {
|
||||
pendingRef.current = event.target.value
|
||||
setDraft(event.target.value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const next = pendingRef.current
|
||||
pendingRef.current = null
|
||||
setDraft(null)
|
||||
if (next !== null && next !== value) onCommit(next)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
const input = event.target as HTMLInputElement
|
||||
input.blur()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -13,17 +13,11 @@ const node = (memberIndex: number, delay: number, name = `${memberIndex}`) =>
|
||||
kind: 'node',
|
||||
ref: { kind: 'node', name, recordId: `${memberIndex}` },
|
||||
node: {
|
||||
recordId: `${memberIndex}`,
|
||||
history: [{ delay }],
|
||||
extra: {
|
||||
'http://cp.cloudflare.com/generate_204': {
|
||||
history: [{ time: new Date(0).toISOString(), delay }],
|
||||
},
|
||||
},
|
||||
source: { kind: 'provider', providerName: `${memberIndex}` },
|
||||
},
|
||||
},
|
||||
}) as unknown as ResolvedMemberOccurrence
|
||||
}) as ResolvedMemberOccurrence
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
@@ -42,9 +36,7 @@ test('matches the previous comparator for cached, fallback and sentinel delays',
|
||||
for (const cached of [false, true]) {
|
||||
const group = `sort-${cached}`
|
||||
if (cached) {
|
||||
values.forEach((delay, i) =>
|
||||
delayManager.setDelay(list[i].member, group, delay),
|
||||
)
|
||||
values.forEach((delay, i) => delayManager.setDelay(`${i}`, group, delay))
|
||||
}
|
||||
for (const timeout of [10000, 20, 0, NaN]) {
|
||||
const expected = list
|
||||
@@ -74,7 +66,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne
|
||||
node(3, 25),
|
||||
]
|
||||
const cachedOrder = [list[0], list[2], list[1], list[3]]
|
||||
delayManager.setDelay(list[0].member, 'expiry', 1)
|
||||
delayManager.setDelay('0', 'expiry', 1)
|
||||
const get = vi.spyOn(delayManager, 'getDelayFix')
|
||||
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
||||
expect(get).toHaveBeenCalledTimes(list.length)
|
||||
@@ -88,7 +80,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne
|
||||
list[3],
|
||||
list[0],
|
||||
])
|
||||
delayManager.setDelay(list[0].member, 'expiry', 2)
|
||||
delayManager.setDelay('0', 'expiry', 2)
|
||||
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
||||
get.mockClear()
|
||||
filterSort([], 'expiry', '', 1)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { useCallback, useEffect, useReducer } from 'react'
|
||||
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import {
|
||||
profileTestUrls,
|
||||
commitProfileTestUrl,
|
||||
} from '@/services/proxy-test-url'
|
||||
|
||||
import { ProxySortType } from './use-filter-sort'
|
||||
|
||||
@@ -36,45 +32,22 @@ export const DEFAULT_STATE: HeadState = {
|
||||
}
|
||||
|
||||
type HeadStateAction =
|
||||
| { type: 'reset'; profile: string }
|
||||
| { type: 'replace'; profile: string; payload: Record<string, HeadState> }
|
||||
| { type: 'reset' }
|
||||
| { type: 'replace'; payload: Record<string, HeadState> }
|
||||
| { type: 'update'; groupName: string; patch: Partial<HeadState> }
|
||||
| { type: 'urls'; urls: Record<string, string> }
|
||||
|
||||
interface LoadedHeadState {
|
||||
profile: string | null
|
||||
heads: Record<string, HeadState>
|
||||
}
|
||||
|
||||
function headStateReducer(
|
||||
state: LoadedHeadState,
|
||||
state: Record<string, HeadState>,
|
||||
action: HeadStateAction,
|
||||
): LoadedHeadState {
|
||||
): Record<string, HeadState> {
|
||||
switch (action.type) {
|
||||
case 'reset':
|
||||
return { profile: action.profile, heads: {} }
|
||||
return {}
|
||||
case 'replace':
|
||||
return { profile: action.profile, heads: action.payload }
|
||||
case 'urls': {
|
||||
let heads = state.heads
|
||||
for (const [name, testUrl] of Object.entries(action.urls)) {
|
||||
if (heads[name]?.testUrl === testUrl) continue
|
||||
heads = {
|
||||
...heads,
|
||||
[name]: { ...DEFAULT_STATE, ...heads[name], testUrl },
|
||||
}
|
||||
}
|
||||
return heads === state.heads ? state : { ...state, heads }
|
||||
}
|
||||
return action.payload
|
||||
case 'update': {
|
||||
const prev = state.heads[action.groupName] || DEFAULT_STATE
|
||||
return {
|
||||
...state,
|
||||
heads: {
|
||||
...state.heads,
|
||||
[action.groupName]: { ...prev, ...action.patch },
|
||||
},
|
||||
}
|
||||
const prev = state[action.groupName] || DEFAULT_STATE
|
||||
return { ...state, [action.groupName]: { ...prev, ...action.patch } }
|
||||
}
|
||||
default:
|
||||
return state
|
||||
@@ -82,42 +55,30 @@ function headStateReducer(
|
||||
}
|
||||
|
||||
export function useHeadStateNew() {
|
||||
const { profiles, current: profile } = useProfiles()
|
||||
const { profiles } = useProfiles()
|
||||
const current = profiles?.current || ''
|
||||
|
||||
const [{ profile: loadedProfile, heads: state }, dispatch] = useReducer(
|
||||
headStateReducer,
|
||||
{ profile: null, heads: {} },
|
||||
)
|
||||
const [state, dispatch] = useReducer(headStateReducer, {})
|
||||
|
||||
useEffect(() => {
|
||||
const urls = profile ? profileTestUrls(profile) : {}
|
||||
if (loadedProfile === current) {
|
||||
dispatch({ type: 'urls', urls })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(
|
||||
localStorage.getItem(HEAD_STATE_KEY) || '{}',
|
||||
localStorage.getItem(HEAD_STATE_KEY)!,
|
||||
) as HeadStateStorage
|
||||
|
||||
const value = data?.[current] || {}
|
||||
for (const [name, testUrl] of Object.entries(urls)) {
|
||||
value[name] = { ...DEFAULT_STATE, ...value[name], testUrl }
|
||||
}
|
||||
const value = data[current] || {}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
dispatch({ type: 'replace', profile: current, payload: value })
|
||||
dispatch({ type: 'replace', payload: value })
|
||||
} else {
|
||||
dispatch({ type: 'reset', profile: current })
|
||||
dispatch({ type: 'reset' })
|
||||
}
|
||||
} catch {
|
||||
dispatch({ type: 'reset', profile: current })
|
||||
dispatch({ type: 'reset' })
|
||||
}
|
||||
}, [current, loadedProfile, profile])
|
||||
}, [current])
|
||||
|
||||
useEffect(() => {
|
||||
if (loadedProfile !== current) return
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
const item = localStorage.getItem(HEAD_STATE_KEY)
|
||||
@@ -133,16 +94,13 @@ export function useHeadStateNew() {
|
||||
})
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [state, current, loadedProfile])
|
||||
}, [state, current])
|
||||
|
||||
const setHeadState = useCallback(
|
||||
(groupName: string, obj: Partial<HeadState>) => {
|
||||
dispatch({ type: 'update', groupName, patch: obj })
|
||||
if (current && obj.testUrl !== undefined) {
|
||||
void commitProfileTestUrl(current, groupName, obj.testUrl)
|
||||
}
|
||||
},
|
||||
[current],
|
||||
[],
|
||||
)
|
||||
|
||||
return [state, setHeadState] as const
|
||||
|
||||
@@ -116,7 +116,6 @@ const virtualGroup = (members: ProxyGroupView['members']): ProxyGroupView => ({
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
history: [],
|
||||
extra: {},
|
||||
members,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import Visibility from '@mui/icons-material/Visibility'
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
Grid,
|
||||
Stack,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
} from '@mui/material'
|
||||
import { Button, Grid, Stack, TextField } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useState, useRef, memo, useEffect } from 'react'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { saveWebdavConfig, createWebdavBackup } from '@/services/cmds'
|
||||
import { createWebdavBackup, saveWebdavConfig } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { errorDetail, showNotice } from '@/services/notice-service'
|
||||
import {
|
||||
@@ -43,7 +34,6 @@ export const BackupConfigViewer = memo(
|
||||
const { t } = useTranslation()
|
||||
const { verge, mutateVerge } = useVerge()
|
||||
const { webdav_url, webdav_username, webdav_password } = verge || {}
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const usernameRef = useRef<HTMLInputElement>(null)
|
||||
const passwordRef = useRef<HTMLInputElement>(null)
|
||||
const urlRef = useRef<HTMLInputElement>(null)
|
||||
@@ -68,10 +58,6 @@ export const BackupConfigViewer = memo(
|
||||
const webdavStatus = getWebdavStatus(webdavSignature)
|
||||
const shouldAutoInit = webdavStatus !== 'failed'
|
||||
|
||||
const handleClickShowPassword = () => {
|
||||
setShowPassword((prev) => !prev)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!webdav_url || !webdav_username || !webdav_password) {
|
||||
return
|
||||
@@ -215,7 +201,7 @@ export const BackupConfigViewer = memo(
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t('shared.labels.password')}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
type="password"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
autoCorrect="off"
|
||||
@@ -223,20 +209,6 @@ export const BackupConfigViewer = memo(
|
||||
spellCheck="false"
|
||||
inputRef={passwordRef}
|
||||
{...register('password')}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleClickShowPassword}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
|
||||
import type { ProfilePatch, ProfileSelection } from '@/services/bindings'
|
||||
import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { migrateProfileTestUrls } from '@/services/proxy-test-url'
|
||||
import { fetchCacheData, setCacheData, useQuery } from '@/services/query-client'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
|
||||
@@ -36,10 +35,6 @@ export const useProfiles = () => {
|
||||
refetchInterval: false,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (profiles) migrateProfileTestUrls(profiles)
|
||||
}, [profiles])
|
||||
|
||||
const refetchRef = useRef(refetch)
|
||||
refetchRef.current = refetch
|
||||
const mutateProfiles = useCallback(async () => {
|
||||
@@ -51,6 +46,7 @@ export const useProfiles = () => {
|
||||
try {
|
||||
const result = await mutate(() => patchProfilesConfig(value), {
|
||||
id: 'patch-profiles-config',
|
||||
errorNotice: false,
|
||||
onFulfilled: (outcome) => {
|
||||
if (outcome.status === 'valid') {
|
||||
void setCacheData<IProfilesConfig>(profilesQueryKey, (current) =>
|
||||
@@ -60,12 +56,8 @@ export const useProfiles = () => {
|
||||
},
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
// Backend Busy keeps local state untouched, as before.
|
||||
return result.value
|
||||
}
|
||||
const outcome = result.value
|
||||
if (outcome.status !== 'valid' && outcome.status !== 'busy') {
|
||||
if (outcome.status !== 'valid') {
|
||||
await mutateProfiles()
|
||||
}
|
||||
return outcome
|
||||
|
||||
@@ -34,6 +34,7 @@ export function useProxyDelayState(
|
||||
groupName: string,
|
||||
): UseProxyDelayState {
|
||||
const name = member.ref.name
|
||||
const details = memberDetails(member)
|
||||
const unresolved = member.kind === 'unresolved'
|
||||
const isPreset = unresolved || PRESET_PROXY_NAMES.includes(name)
|
||||
const [delayState, setDelayState] = useReducer(identity, INITIAL_DELAY)
|
||||
@@ -42,18 +43,18 @@ export function useProxyDelayState(
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreset) return
|
||||
delayManager.setListener(member, groupName, setDelayState)
|
||||
delayManager.setListener(name, groupName, setDelayState)
|
||||
return () => {
|
||||
delayManager.removeListener(member, groupName)
|
||||
delayManager.removeListener(name, groupName)
|
||||
}
|
||||
}, [member, groupName, isPreset])
|
||||
}, [name, groupName, isPreset])
|
||||
|
||||
const updateDelay = useCallback(() => {
|
||||
if (unresolved) {
|
||||
setDelayState(INITIAL_DELAY)
|
||||
return
|
||||
}
|
||||
const cachedUpdate = delayManager.getDelayUpdate(member, groupName)
|
||||
const cachedUpdate = delayManager.getDelayUpdate(name, groupName)
|
||||
if (cachedUpdate) {
|
||||
setDelayState({ ...cachedUpdate })
|
||||
return
|
||||
@@ -66,9 +67,7 @@ export function useProxyDelayState(
|
||||
}
|
||||
|
||||
let updatedAt = 0
|
||||
const history =
|
||||
delayManager.getHistory(member, groupName) ??
|
||||
memberDetails(member)?.history
|
||||
const history = details?.history
|
||||
if (history && history.length > 0) {
|
||||
const lastRecord = history[history.length - 1]
|
||||
const parsed = Date.parse(lastRecord.time)
|
||||
@@ -78,18 +77,16 @@ export function useProxyDelayState(
|
||||
}
|
||||
|
||||
setDelayState({ delay: fallbackDelay, updatedAt })
|
||||
}, [groupName, member, unresolved])
|
||||
}, [details?.history, groupName, member, name, unresolved])
|
||||
|
||||
useEffect(() => {
|
||||
updateDelay()
|
||||
return delayManager.addGroupListener(groupName, updateDelay)
|
||||
}, [groupName, updateDelay])
|
||||
}, [updateDelay])
|
||||
|
||||
const onDelay = useLockFn(async () => {
|
||||
if (!isInteractableMember(member)) return
|
||||
setDelayState({ delay: -2, updatedAt: Date.now() })
|
||||
await delayManager.checkDelay(member, groupName, timeout)
|
||||
updateDelay()
|
||||
setDelayState(await delayManager.checkDelay(member, groupName, timeout))
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Proxy-Kette konfigurieren",
|
||||
"warning": "Eine Proxy-Kette verringert die Netzwerkgeschwindigkeit deutlich. Wenn Sie keinen klaren Bedarf haben oder diese Konfiguration nicht verstehen, lassen Sie sie deaktiviert.",
|
||||
"confirmEnable": "Verstanden, aktivieren",
|
||||
"keepDisabled": "Deaktiviert lassen",
|
||||
"empty": "Eine Proxy-Kette verringert die Netzwerkgeschwindigkeit deutlich. Wenn Sie keinen klaren Bedarf haben oder diese Konfiguration nicht verstehen, lassen Sie sie deaktiviert.",
|
||||
"instruction": "Klicken Sie die Knoten nacheinander an, um sie zur Proxy-Kette hinzuzufügen",
|
||||
"minimumNodes": "Eine Proxy-Kette braucht mindestens 2 Knoten",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "체인 프록시 구성",
|
||||
"warning": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.",
|
||||
"confirmEnable": "이해했습니다, 활성화",
|
||||
"keepDisabled": "비활성화 유지",
|
||||
"empty": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.",
|
||||
"instruction": "프록시 체인에 추가하려면 순서대로 노드를 클릭하세요",
|
||||
"minimumNodes": "체인 프록시는 최소 2개의 노드가 필요합니다",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Configuração do proxy em cadeia",
|
||||
"warning": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.",
|
||||
"confirmEnable": "Entendi, ativar",
|
||||
"keepDisabled": "Manter desativado",
|
||||
"empty": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.",
|
||||
"instruction": "Clique nos nós em ordem para adicioná-los à cadeia de proxy",
|
||||
"minimumNodes": "O proxy em cadeia requer pelo menos 2 nós",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Цепочка прокси",
|
||||
"warning": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.",
|
||||
"confirmEnable": "Понятно, включить",
|
||||
"keepDisabled": "Оставить выключенным",
|
||||
"empty": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.",
|
||||
"instruction": "Нажимайте на узлы по порядку, чтобы добавить их в цепочку",
|
||||
"minimumNodes": "Для цепочки требуется минимум 2 узла",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Zincir Vekil Yapılandırması",
|
||||
"warning": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.",
|
||||
"confirmEnable": "Anladım, etkinleştir",
|
||||
"keepDisabled": "Devre dışı kalsın",
|
||||
"empty": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.",
|
||||
"instruction": "Vekil zincirine eklemek için düğümlere sırayla tıklayın",
|
||||
"minimumNodes": "Zincir vekil en az 2 düğüm gerektirir",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"confirmEnable": "I understand, enable",
|
||||
"keepDisabled": "Keep disabled",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "代理链配置",
|
||||
"warning": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭",
|
||||
"confirmEnable": "我明白,开启",
|
||||
"keepDisabled": "保持关闭",
|
||||
"empty": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭",
|
||||
"instruction": "顺序点击节点添加到代理链中",
|
||||
"minimumNodes": "链式代理至少需要 2 个节点",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"chain": {
|
||||
"header": "鏈式代理設定",
|
||||
"warning": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉",
|
||||
"confirmEnable": "我明白,開啟",
|
||||
"keepDisabled": "保持關閉",
|
||||
"empty": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉",
|
||||
"instruction": "依序點擊節點新增到鏈式代理中",
|
||||
"minimumNodes": "鏈式代理至少需要 2 個節點",
|
||||
|
||||
+43
-24
@@ -1,10 +1,10 @@
|
||||
import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material'
|
||||
import { Box, Button, ButtonGroup } from '@mui/material'
|
||||
import { Box, Button, ButtonGroup, Typography } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BasePage, TooltipIcon } from '@/components/base'
|
||||
import { BaseDialog, BasePage, TooltipIcon } from '@/components/base'
|
||||
import { ProviderButton } from '@/components/proxy/provider-button'
|
||||
import { ProxyGroups } from '@/components/proxy/proxy-groups'
|
||||
import {
|
||||
@@ -38,6 +38,7 @@ const ProxyPage = () => {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const [chainConfirmOpen, setChainConfirmOpen] = useState(false)
|
||||
|
||||
const [chainConfigData, dispatchChainConfigData] = useReducer(
|
||||
(_: string | null, action: string | null) => action,
|
||||
@@ -68,32 +69,34 @@ const ProxyPage = () => {
|
||||
}
|
||||
})
|
||||
|
||||
const onToggleChainMode = useLockFn(async () => {
|
||||
const newChainMode = !isChainMode
|
||||
const applyChainMode = (enabled: boolean) => {
|
||||
setIsChainMode(enabled)
|
||||
localStorage.setItem('proxy-chain-mode-enabled', enabled.toString())
|
||||
}
|
||||
|
||||
if (!newChainMode) {
|
||||
// 退出链式代理模式时,清除链式代理配置
|
||||
try {
|
||||
debugLog('Exiting chain mode, clearing chain configuration')
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(null),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
debugLog('Chain configuration cleared successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear chain configuration:', error)
|
||||
showNotice.error(error)
|
||||
return
|
||||
}
|
||||
const onExitChainMode = useLockFn(async () => {
|
||||
// 退出链式代理模式时,清除链式代理配置
|
||||
try {
|
||||
debugLog('Exiting chain mode, clearing chain configuration')
|
||||
const result = await mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
if (!result.ok) return
|
||||
debugLog('Chain configuration cleared successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear chain configuration:', error)
|
||||
showNotice.error(error)
|
||||
return
|
||||
}
|
||||
setIsChainMode(newChainMode)
|
||||
localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString())
|
||||
applyChainMode(false)
|
||||
})
|
||||
|
||||
const onToggleChainMode = () => {
|
||||
if (isChainMode) onExitChainMode().catch(() => {})
|
||||
else setChainConfirmOpen(true)
|
||||
}
|
||||
|
||||
// 当开启链式代理模式时,获取配置数据
|
||||
useEffect(() => {
|
||||
if (!isChainMode) {
|
||||
@@ -202,7 +205,23 @@ const ProxyPage = () => {
|
||||
mode={curMode ?? 'rule'}
|
||||
isChainMode={isChainMode}
|
||||
chainConfigData={chainConfigData}
|
||||
onExitChainMode={onExitChainMode}
|
||||
/>
|
||||
<BaseDialog
|
||||
open={chainConfirmOpen}
|
||||
title={t('proxies.page.actions.toggleChain')}
|
||||
okBtn={t('proxies.page.chain.confirmEnable')}
|
||||
cancelBtn={t('proxies.page.chain.keepDisabled')}
|
||||
contentSx={{ width: { xs: 320, sm: 420 } }}
|
||||
onCancel={() => setChainConfirmOpen(false)}
|
||||
onClose={() => setChainConfirmOpen(false)}
|
||||
onOk={() => {
|
||||
setChainConfirmOpen(false)
|
||||
applyChainMode(true)
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">{chainWarning}</Typography>
|
||||
</BaseDialog>
|
||||
</BasePage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -659,7 +659,6 @@ export type PrfItem_Deserialize = {
|
||||
desc?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
latency_test_urls?: { [key in string]: string } | null
|
||||
extra?: PrfExtra | null
|
||||
updated?: number | null
|
||||
option?: PrfOption_Deserialize | null
|
||||
@@ -675,7 +674,6 @@ export type PrfItem_Serialize = {
|
||||
desc?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
latency_test_urls?: { [key in string]: string } | null
|
||||
extra?: PrfExtra | null
|
||||
updated: number | null
|
||||
option?: PrfOption_Serialize | null
|
||||
@@ -770,7 +768,6 @@ export type ProfilePatch_Deserialize = {
|
||||
file?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
latency_test_urls?: { [key in string]: string } | null
|
||||
extra?: PrfExtra | null
|
||||
updated?: number | null
|
||||
option?: PrfOption_Deserialize | null
|
||||
@@ -783,7 +780,6 @@ export type ProfilePatch_Serialize = {
|
||||
file: string | null
|
||||
url: string | null
|
||||
selected: PrfSelected[] | null
|
||||
latency_test_urls: { [key in string]: string } | null
|
||||
extra: PrfExtra | null
|
||||
updated: number | null
|
||||
option: PrfOption_Serialize | null
|
||||
@@ -818,7 +814,6 @@ export type ProxyGroupView_Deserialize = {
|
||||
icon: string | null
|
||||
testUrl: string | null
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyLatencyHistory }
|
||||
members: ProxyMemberRef[]
|
||||
} & ProxyCapabilities
|
||||
|
||||
@@ -831,14 +826,9 @@ export type ProxyGroupView_Serialize = {
|
||||
icon?: string | null
|
||||
testUrl?: string | null
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyLatencyHistory }
|
||||
members: ProxyMemberRef[]
|
||||
} & ProxyCapabilities
|
||||
|
||||
export type ProxyLatencyHistory = {
|
||||
history: DelayHistory[]
|
||||
}
|
||||
|
||||
export type ProxyMemberRef =
|
||||
| { kind: 'group'; name: string }
|
||||
| { kind: 'node'; name: string; recordId: string }
|
||||
@@ -860,7 +850,6 @@ export type ProxyNodeView_Deserialize = {
|
||||
name: string
|
||||
type: string
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyLatencyHistory }
|
||||
id: string | null
|
||||
hidden: boolean | null
|
||||
icon: string | null
|
||||
@@ -873,7 +862,6 @@ export type ProxyNodeView_Serialize = {
|
||||
name: string
|
||||
type: string
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyLatencyHistory }
|
||||
id?: string | null
|
||||
hidden?: boolean | null
|
||||
icon?: string | null
|
||||
|
||||
+2
-13
@@ -91,18 +91,8 @@ export async function deleteProfile(index: string) {
|
||||
}
|
||||
|
||||
export async function patchProfile(index: string, profile: ProfilePatch) {
|
||||
const {
|
||||
type,
|
||||
name,
|
||||
desc,
|
||||
file,
|
||||
url,
|
||||
selected,
|
||||
latency_test_urls,
|
||||
extra,
|
||||
updated,
|
||||
option,
|
||||
} = profile
|
||||
const { type, name, desc, file, url, selected, extra, updated, option } =
|
||||
profile
|
||||
return commands.patchProfile(index, {
|
||||
type,
|
||||
name,
|
||||
@@ -110,7 +100,6 @@ export async function patchProfile(index: string, profile: ProfilePatch) {
|
||||
file,
|
||||
url,
|
||||
selected,
|
||||
latency_test_urls,
|
||||
extra,
|
||||
updated,
|
||||
option,
|
||||
|
||||
@@ -59,136 +59,3 @@ describe('group delay completion', () => {
|
||||
stop()
|
||||
})
|
||||
})
|
||||
|
||||
test('uses newer core history after a tray test without hiding an active page test', () => {
|
||||
const member = node('tray-refresh')
|
||||
if (member.kind !== 'node') throw new Error('Expected node')
|
||||
const cached = delayManager.setDelay(member, 'tray-group', 50)
|
||||
member.node.extra = {
|
||||
[delayManager.getUrl('tray-group')]: {
|
||||
history: [
|
||||
{ time: new Date(cached.updatedAt + 1000).toISOString(), delay: 300 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
expect(delayManager.getDelayFix(member, 'tray-group')).toBe(300)
|
||||
expect(delayManager.getDelayUpdate(member, 'tray-group')?.delay).toBe(300)
|
||||
|
||||
delayManager.setDelay(member, 'tray-group', -2)
|
||||
expect(delayManager.getDelayFix(member, 'tray-group')).toBe(-2)
|
||||
})
|
||||
|
||||
test('latency URL validation rejects malformed hosts and ports and trims valid URLs', () => {
|
||||
for (const url of [
|
||||
'http://?',
|
||||
'http://host:invalid',
|
||||
'http://bad host/',
|
||||
'ftp://localhost',
|
||||
'http:localhost',
|
||||
]) {
|
||||
delayManager.setUrl('url-validation', url)
|
||||
expect(delayManager.getUrl('url-validation')).toBe(
|
||||
'http://cp.cloudflare.com/generate_204',
|
||||
)
|
||||
}
|
||||
delayManager.setUrl('url-validation', ' HTTP://localhost:8080/ ')
|
||||
expect(delayManager.getUrl('url-validation')).toBe('HTTP://localhost:8080/')
|
||||
})
|
||||
|
||||
test('another URL cannot replace a group measurement, but a newer matching URL can', () => {
|
||||
const member = node('url-isolation')
|
||||
if (member.kind !== 'node') throw new Error('Expected node')
|
||||
delayManager.setUrl('url-a', 'http://localhost/a')
|
||||
const cached = delayManager.setDelay(member, 'url-a', 50)
|
||||
const newer = new Date(cached.updatedAt + 1000).toISOString()
|
||||
Object.assign(member.node, {
|
||||
history: [{ time: newer, delay: 126 }],
|
||||
extra: {
|
||||
'http://localhost/a': {
|
||||
history: [
|
||||
{ time: new Date(cached.updatedAt - 1000).toISOString(), delay: 49 },
|
||||
],
|
||||
},
|
||||
'http://localhost/b': {
|
||||
history: [{ time: newer, delay: 126 }],
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(delayManager.getDelayFix(member, 'url-a')).toBe(50)
|
||||
Object.assign(member.node, {
|
||||
extra: {
|
||||
'http://localhost/a': {
|
||||
history: [{ time: newer, delay: 75 }],
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(delayManager.getDelayFix(member, 'url-a')).toBe(75)
|
||||
})
|
||||
|
||||
test('same-named provider members keep separate measurements and listeners', async () => {
|
||||
const first = node('same-provider-name')
|
||||
const second = node('same-provider-name')
|
||||
if (first.kind !== 'node' || second.kind !== 'node')
|
||||
throw new Error('Expected nodes')
|
||||
first.node.recordId = 'provider-a:N'
|
||||
first.node.source = { kind: 'provider', providerName: 'a', proxyName: 'N' }
|
||||
second.node.recordId = 'provider-b:N'
|
||||
second.node.source = { kind: 'provider', providerName: 'b', proxyName: 'N' }
|
||||
const { healthcheckNodeInProvider } = await import('tauri-plugin-mihomo-api')
|
||||
vi.mocked(healthcheckNodeInProvider)
|
||||
.mockResolvedValueOnce({ delay: 40 })
|
||||
.mockResolvedValueOnce({ delay: 125 })
|
||||
const firstListener = vi.fn()
|
||||
const secondListener = vi.fn()
|
||||
delayManager.setListener(first, 'provider-collision', firstListener)
|
||||
delayManager.setListener(second, 'provider-collision', secondListener)
|
||||
await delayManager.checkListDelay([first, second], 'provider-collision', 5000)
|
||||
await flush()
|
||||
expect(firstListener).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ delay: 40 }),
|
||||
)
|
||||
expect(secondListener).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ delay: 125 }),
|
||||
)
|
||||
delayManager.removeListener(first, 'provider-collision')
|
||||
delayManager.removeListener(second, 'provider-collision')
|
||||
expect(delayManager.getDelayFix(first, 'provider-collision')).toBe(40)
|
||||
expect(delayManager.getDelayFix(second, 'provider-collision')).toBe(125)
|
||||
})
|
||||
|
||||
test.each(['core', 'provider'] as const)(
|
||||
'cached %s delays follow source identity after record IDs move',
|
||||
(kind) => {
|
||||
const original = node(`stable-${kind}`)
|
||||
const replacement = node(`replacement-${kind}`)
|
||||
if (original.kind !== 'node' || replacement.kind !== 'node')
|
||||
throw new Error('Expected nodes')
|
||||
const source = (proxyName: string) =>
|
||||
kind === 'core'
|
||||
? { kind, proxyName }
|
||||
: { kind, providerName: 'subscription', proxyName }
|
||||
original.node.source = source(original.ref.name)
|
||||
replacement.node.source = source(replacement.ref.name)
|
||||
original.node.recordId = 'position:0'
|
||||
replacement.node.recordId = 'position:0'
|
||||
delayManager.setDelay(original, `reorder-${kind}`, 40)
|
||||
expect(delayManager.getDelayFix(replacement, `reorder-${kind}`)).toBe(-1)
|
||||
original.node.recordId = 'position:1'
|
||||
expect(delayManager.getDelayFix(original, `reorder-${kind}`)).toBe(40)
|
||||
},
|
||||
)
|
||||
|
||||
test('unmeasured selectors show automatic history without replacing a manual URL result', () => {
|
||||
const member = node('selector-member')
|
||||
if (member.kind !== 'node') throw new Error('Expected node')
|
||||
member.node.history = [
|
||||
{ time: new Date(Date.now() + 1000).toISOString(), delay: 126 },
|
||||
]
|
||||
member.node.extra = {
|
||||
'http://localhost/automatic': { history: member.node.history },
|
||||
}
|
||||
expect(delayManager.getDelayFix(member, 'selector')).toBe(126)
|
||||
delayManager.setDelay(member, 'selector', 50)
|
||||
expect(delayManager.getDelayFix(member, 'selector')).toBe(50)
|
||||
})
|
||||
|
||||
+38
-58
@@ -12,23 +12,13 @@ import {
|
||||
} from '@/types/proxy-view'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { classifyDelay, DEFAULT_DELAY_TIMEOUT } from '@/utils/delay'
|
||||
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
|
||||
export type DelaySnapshot = {
|
||||
of: (member: ResolvedProxyMember) => number
|
||||
}
|
||||
|
||||
const hashKey = (member: ResolvedProxyMember, group: string) => {
|
||||
if (member.kind !== 'node')
|
||||
return JSON.stringify([group, member.kind, member.ref.name])
|
||||
const source = member.node.source
|
||||
return JSON.stringify([
|
||||
group,
|
||||
source.kind,
|
||||
source.kind === 'provider' ? source.providerName : null,
|
||||
source.proxyName,
|
||||
])
|
||||
}
|
||||
const hashKey = (name: string, group: string) => `${group ?? ''}::${name}`
|
||||
|
||||
export interface DelayUpdate {
|
||||
delay: number
|
||||
@@ -39,7 +29,7 @@ export interface DelayUpdate {
|
||||
const CACHE_TTL = 30 * 60 * 1000
|
||||
|
||||
class DelayManager {
|
||||
private cache = new Map<string, DelayUpdate & { url: string }>()
|
||||
private cache = new Map<string, DelayUpdate>()
|
||||
private urlMap = new Map<string, string>()
|
||||
|
||||
private listenerMap = new Map<string, (update: DelayUpdate) => void>()
|
||||
@@ -163,13 +153,13 @@ class DelayManager {
|
||||
}
|
||||
|
||||
setUrl(group: string, url: string) {
|
||||
const previous = this.getUrl(group)
|
||||
if (isValidLatencyTestUrl(url)) {
|
||||
this.urlMap.set(group, url.trim())
|
||||
} else {
|
||||
if (!isValidUrl(url)) {
|
||||
debugLog(`[DelayManager] 拒绝无效测试URL,组: ${group}, URL: ${url}`)
|
||||
this.urlMap.delete(group)
|
||||
return
|
||||
}
|
||||
if (previous !== this.getUrl(group)) this.queueGroupNotification(group)
|
||||
debugLog(`[DelayManager] 设置测试URL,组: ${group}, URL: ${url}`)
|
||||
this.urlMap.set(group, url)
|
||||
}
|
||||
|
||||
getUrl(group: string) {
|
||||
@@ -181,16 +171,16 @@ class DelayManager {
|
||||
}
|
||||
|
||||
setListener(
|
||||
member: ResolvedProxyMember,
|
||||
name: string,
|
||||
group: string,
|
||||
listener: (update: DelayUpdate) => void,
|
||||
) {
|
||||
const key = hashKey(member, group)
|
||||
const key = hashKey(name, group)
|
||||
this.listenerMap.set(key, listener)
|
||||
}
|
||||
|
||||
removeListener(member: ResolvedProxyMember, group: string) {
|
||||
const key = hashKey(member, group)
|
||||
removeListener(name: string, group: string) {
|
||||
const key = hashKey(name, group)
|
||||
this.listenerMap.delete(key)
|
||||
}
|
||||
|
||||
@@ -209,14 +199,14 @@ class DelayManager {
|
||||
}
|
||||
|
||||
setDelay(
|
||||
member: ResolvedProxyMember,
|
||||
name: string,
|
||||
group: string,
|
||||
delay: number,
|
||||
meta?: { elapsed?: number; url?: string },
|
||||
meta?: { elapsed?: number },
|
||||
): DelayUpdate {
|
||||
const key = hashKey(member, group)
|
||||
const key = hashKey(name, group)
|
||||
debugLog(
|
||||
`[DelayManager] 设置延迟,代理: ${member.ref.name}, 组: ${group}, 延迟: ${delay}`,
|
||||
`[DelayManager] 设置延迟,代理: ${name}, 组: ${group}, 延迟: ${delay}`,
|
||||
)
|
||||
const update: DelayUpdate = {
|
||||
delay,
|
||||
@@ -224,9 +214,7 @@ class DelayManager {
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
const url = meta?.url ?? this.getUrl(group)
|
||||
if (url !== this.getUrl(group)) return update
|
||||
this.cache.set(key, { ...update, url })
|
||||
this.cache.set(key, update)
|
||||
|
||||
const queue = this.pendingItemUpdates.get(key)
|
||||
if (queue) {
|
||||
@@ -239,16 +227,9 @@ class DelayManager {
|
||||
return update
|
||||
}
|
||||
|
||||
getDelayUpdate(member: ResolvedProxyMember, group: string) {
|
||||
const history = this.getHistory(member, group)
|
||||
const key = hashKey(member, group)
|
||||
const cached = this.cache.get(key)
|
||||
const entry = cached?.url === this.getUrl(group) ? cached : undefined
|
||||
const latest = history?.at(-1)
|
||||
const updatedAt = latest ? Date.parse(latest.time) : 0
|
||||
if (latest && entry?.delay !== -2 && updatedAt > (entry?.updatedAt ?? 0)) {
|
||||
return { delay: latest.delay || 1e6, updatedAt }
|
||||
}
|
||||
getDelayUpdate(name: string, group: string) {
|
||||
const key = hashKey(name, group)
|
||||
const entry = this.cache.get(key)
|
||||
if (!entry) return undefined
|
||||
|
||||
if (Date.now() - entry.updatedAt > CACHE_TTL) {
|
||||
@@ -259,25 +240,23 @@ class DelayManager {
|
||||
return { ...entry }
|
||||
}
|
||||
|
||||
getDelay(member: ResolvedProxyMember, group: string) {
|
||||
const update = this.getDelayUpdate(member, group)
|
||||
getDelay(name: string, group: string) {
|
||||
const update = this.getDelayUpdate(name, group)
|
||||
return update ? update.delay : -1
|
||||
}
|
||||
|
||||
getHistory(member: ResolvedProxyMember, group: string) {
|
||||
return memberDetails(member)?.extra?.[this.getUrl(group)]?.history
|
||||
}
|
||||
|
||||
getDelayFix(member: ResolvedProxyMember, group: string) {
|
||||
if (member.kind === 'unresolved') return -1
|
||||
const history =
|
||||
this.getHistory(member, group) ?? memberDetails(member)?.history
|
||||
const update = this.getDelayUpdate(member, group)
|
||||
const details = memberDetails(member)
|
||||
const name = member.ref.name
|
||||
const update = this.getDelayUpdate(name, group)
|
||||
if (update && (update.delay >= 0 || update.delay === -2)) {
|
||||
return update.delay
|
||||
}
|
||||
|
||||
if (history?.length) return history[history.length - 1].delay || 1e6
|
||||
if (details?.history && details.history.length > 0) {
|
||||
return details.history[details.history.length - 1].delay || 1e6
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -319,12 +298,12 @@ class DelayManager {
|
||||
`[DelayManager] 开始测试延迟,代理: ${name}, 组: ${group}, 超时: ${timeout}ms`,
|
||||
)
|
||||
|
||||
const url = this.getUrl(group)
|
||||
this.setDelay(member, group, -2, { url })
|
||||
this.setDelay(name, group, -2)
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const url = this.getUrl(group)
|
||||
debugLog(`[DelayManager] 调用API测试延迟,代理: ${name}, URL: ${url}`)
|
||||
|
||||
const timeoutPromise = new Promise<ProxyDelay>((resolve) => {
|
||||
@@ -345,14 +324,14 @@ class DelayManager {
|
||||
const elapsed = elapsedTime
|
||||
debugLog(`[DelayManager] 延迟测试完成,代理: ${name}, 结果: ${delay}ms`)
|
||||
|
||||
return this.setDelay(member, group, delay, { elapsed, url })
|
||||
return this.setDelay(name, group, delay, { elapsed })
|
||||
} catch (error) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
console.error(`[DelayManager] 延迟测试出错,代理: ${name}`, error)
|
||||
const delay = 1e6 // error
|
||||
const elapsed = Date.now() - startTime
|
||||
|
||||
return this.setDelay(member, group, delay, { elapsed, url })
|
||||
return this.setDelay(name, group, delay, { elapsed })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,9 +344,10 @@ class DelayManager {
|
||||
debugLog(
|
||||
`[DelayManager] 批量测试延迟开始,组: ${group}, 数量: ${proxies.length}, 并发数: ${concurrency}`,
|
||||
)
|
||||
const names = proxies.map((member) => member.ref.name)
|
||||
this.activeBatches.set(group, (this.activeBatches.get(group) ?? 0) + 1)
|
||||
proxies.forEach((member) => {
|
||||
this.setDelay(member, group, -2)
|
||||
names.forEach((name) => {
|
||||
this.setDelay(name, group, -2)
|
||||
})
|
||||
|
||||
let index = 0
|
||||
@@ -379,7 +359,7 @@ class DelayManager {
|
||||
const currName = currMember.ref.name
|
||||
|
||||
try {
|
||||
this.setDelay(currMember, group, -2)
|
||||
this.setDelay(currName, group, -2)
|
||||
|
||||
// Stagger requests so a batch does not hit the core at once.
|
||||
if (index > 1) {
|
||||
@@ -395,13 +375,13 @@ class DelayManager {
|
||||
`[DelayManager] 批量测试单个代理出错,代理: ${currName}`,
|
||||
error,
|
||||
)
|
||||
this.setDelay(currMember, group, 1e6)
|
||||
this.setDelay(currName, group, 1e6)
|
||||
}
|
||||
|
||||
return help()
|
||||
}
|
||||
|
||||
const actualConcurrency = Math.min(concurrency, proxies.length, 10)
|
||||
const actualConcurrency = Math.min(concurrency, names.length, 10)
|
||||
debugLog(`[DelayManager] 实际并发数: ${actualConcurrency}`)
|
||||
|
||||
const promiseList: Promise<void>[] = []
|
||||
|
||||
+207
-216
@@ -32,230 +32,221 @@ export const forgetShownStartupError = () => {
|
||||
* type makes a backend status without a frontend entry a compile error (and
|
||||
* contract regeneration keeps the union in sync).
|
||||
*/
|
||||
const handlers: Record<
|
||||
NoticeStatus,
|
||||
(msg: string, navigate: NavigateFunction) => void
|
||||
> = {
|
||||
info: (msg) => {
|
||||
if (msg) showNotice.info(msg)
|
||||
},
|
||||
'import_sub_url::ok': (_msg, navigate) => {
|
||||
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
|
||||
// navigate("/profile", { state: { current: msg } });
|
||||
navigate('/profile')
|
||||
showNotice.success(
|
||||
'shared.feedback.notifications.importSubscriptionSuccess',
|
||||
)
|
||||
},
|
||||
'import_sub_url::error': (msg, navigate) => {
|
||||
navigate('/profile')
|
||||
showNotice.error(msg)
|
||||
},
|
||||
'set_config::error': (msg) => showNotice.error(msg),
|
||||
'core_start::error': () => {
|
||||
const read = ++startupErrorReads
|
||||
void getCoreStartupError()
|
||||
.then(async (failure) => {
|
||||
if (read < settledStartupErrorRead) return
|
||||
settledStartupErrorRead = read
|
||||
if (!failure) {
|
||||
shownStartupError = null
|
||||
return
|
||||
}
|
||||
const window = getCurrentWindow()
|
||||
const [visible, minimized] = await Promise.all([
|
||||
window.isVisible(),
|
||||
window.isMinimized(),
|
||||
])
|
||||
const shown = `${failure.kind}:${failure.detail}`
|
||||
if (
|
||||
read === settledStartupErrorRead &&
|
||||
visible &&
|
||||
!minimized &&
|
||||
shownStartupError !== shown
|
||||
) {
|
||||
shownStartupError = shown
|
||||
showNotice.error(
|
||||
failure.kind === 'serviceCoreStopped'
|
||||
? 'settings.feedback.errors.clash.serviceCoreStopped'
|
||||
: 'settings.feedback.errors.clash.startFailed',
|
||||
failure.detail,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending core startup error', error)
|
||||
})
|
||||
},
|
||||
'service_core::repair_required': () => {
|
||||
void takeServiceRepairNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
requestService({ reason: 'serviceLocationRefused' })
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending service repair notice', error)
|
||||
})
|
||||
},
|
||||
'service_core::app_data_not_owned': () => {
|
||||
void takeServiceOwnerNotice()
|
||||
.then((command) => {
|
||||
if (command) {
|
||||
if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice)
|
||||
shownOwnerNotice = showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.appDataNotOwned',
|
||||
{ command },
|
||||
0,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending service owner notice', error)
|
||||
})
|
||||
},
|
||||
'service_core::sidecar_fallback': () => {
|
||||
void takeServiceFallbackNotice()
|
||||
.then((notice) => {
|
||||
if (notice?.kind === 'coreRejected') {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.permissionFallback',
|
||||
{ reason: notice.reason },
|
||||
0,
|
||||
)
|
||||
} else if (notice?.kind === 'notAutoStarted') {
|
||||
requestService({ reason: 'serviceNotAutoStarted' })
|
||||
} else if (notice) {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.sidecarFallback',
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service fallback notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'dns_override::auto_disabled': () => {
|
||||
void takeDnsOverrideNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
showNotice.info('settings.modals.dns.protection.autoDisabled')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending DNS override notice', error)
|
||||
})
|
||||
},
|
||||
'enhance::discarded_keys': () => {
|
||||
void takeDiscardedKeysNotice()
|
||||
.then((keys) => {
|
||||
if (keys) {
|
||||
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
|
||||
keys,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending discarded keys notice', error)
|
||||
})
|
||||
},
|
||||
'tun_mode::auto_disabled': () =>
|
||||
showNotice.info(
|
||||
'settings.sections.system.notifications.tunMode.autoDisabled',
|
||||
),
|
||||
'tun_mode::auto_disable_failed': () =>
|
||||
showNotice.error(
|
||||
'settings.sections.system.notifications.tunMode.autoDisableFailed',
|
||||
),
|
||||
'app_restart::core_stop_failed': (msg) =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.restartCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
'app_quit::core_stop_failed': (msg) =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.quitCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
update_with_clash_proxy: (msg) =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.updater.withClashProxySuccess',
|
||||
msg,
|
||||
),
|
||||
update_failed_even_with_clash: (msg) =>
|
||||
showNotice.error(
|
||||
'settings.feedback.notifications.updater.withClashProxyFailed',
|
||||
msg,
|
||||
),
|
||||
'reactivate_profiles::error': (msg) => showNotice.error(msg),
|
||||
update_failed: (msg) => showNotice.error(msg),
|
||||
'config_validate::boot_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.config.bootFailed', msg),
|
||||
'config_validate::error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.config.failed', msg),
|
||||
'config_validate::process_terminated': () =>
|
||||
showNotice.error('shared.feedback.validation.config.processTerminated'),
|
||||
'config_validate::script_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.fileError', msg),
|
||||
'config_validate::script_syntax_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.syntaxError', msg),
|
||||
'config_validate::script_missing_main': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.missingMain', msg),
|
||||
'config_validate::file_not_found': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.fileNotFound', msg),
|
||||
'config_validate::yaml_syntax_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.yaml.syntaxError', msg),
|
||||
'config_validate::yaml_read_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.yaml.readError', msg),
|
||||
'config_validate::yaml_mapping_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.yaml.mappingError', msg),
|
||||
'config_validate::merge_syntax_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.merge.syntaxError', msg),
|
||||
'config_validate::merge_mapping_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.merge.mappingError', msg),
|
||||
'config_core::change_success': (msg) =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.clash.changeSuccess',
|
||||
msg,
|
||||
),
|
||||
'config_core::change_error': (msg) =>
|
||||
showNotice.error('settings.feedback.notifications.clash.changeFailed', msg),
|
||||
'mixed_port::fallback': (msg) => {
|
||||
const [oldPort, newPort] = msg.split(',')
|
||||
showNotice.info('settings.modals.clashPort.messages.automaticFallback', {
|
||||
oldPort,
|
||||
newPort,
|
||||
})
|
||||
},
|
||||
'mixed_port::fallback_error': (msg) =>
|
||||
showNotice.error(
|
||||
'settings.modals.clashPort.messages.automaticFallbackFailed',
|
||||
{ error: msg },
|
||||
),
|
||||
}
|
||||
|
||||
export const handleNoticeMessage = (
|
||||
status: string,
|
||||
msg: string,
|
||||
t: TranslateFunction,
|
||||
navigate: NavigateFunction,
|
||||
) => {
|
||||
const handlers: Record<NoticeStatus, () => void> = {
|
||||
info: () => {
|
||||
if (msg) showNotice.info(msg)
|
||||
},
|
||||
'import_sub_url::ok': () => {
|
||||
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
|
||||
// navigate("/profile", { state: { current: msg } });
|
||||
navigate('/profile')
|
||||
showNotice.success(
|
||||
'shared.feedback.notifications.importSubscriptionSuccess',
|
||||
)
|
||||
},
|
||||
'import_sub_url::error': () => {
|
||||
navigate('/profile')
|
||||
showNotice.error(msg)
|
||||
},
|
||||
'set_config::error': () => showNotice.error(msg),
|
||||
'core_start::error': () => {
|
||||
const read = ++startupErrorReads
|
||||
void getCoreStartupError()
|
||||
.then(async (failure) => {
|
||||
if (read < settledStartupErrorRead) return
|
||||
settledStartupErrorRead = read
|
||||
if (!failure) {
|
||||
shownStartupError = null
|
||||
return
|
||||
}
|
||||
const window = getCurrentWindow()
|
||||
const [visible, minimized] = await Promise.all([
|
||||
window.isVisible(),
|
||||
window.isMinimized(),
|
||||
])
|
||||
const shown = `${failure.kind}:${failure.detail}`
|
||||
if (
|
||||
read === settledStartupErrorRead &&
|
||||
visible &&
|
||||
!minimized &&
|
||||
shownStartupError !== shown
|
||||
) {
|
||||
shownStartupError = shown
|
||||
showNotice.error(
|
||||
failure.kind === 'serviceCoreStopped'
|
||||
? 'settings.feedback.errors.clash.serviceCoreStopped'
|
||||
: 'settings.feedback.errors.clash.startFailed',
|
||||
failure.detail,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending core startup error', error)
|
||||
})
|
||||
},
|
||||
'service_core::repair_required': () => {
|
||||
void takeServiceRepairNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
requestService({ reason: 'serviceLocationRefused' })
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service repair notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'service_core::app_data_not_owned': () => {
|
||||
void takeServiceOwnerNotice()
|
||||
.then((command) => {
|
||||
if (command) {
|
||||
if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice)
|
||||
shownOwnerNotice = showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.appDataNotOwned',
|
||||
{ command },
|
||||
0,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service owner notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'service_core::sidecar_fallback': () => {
|
||||
void takeServiceFallbackNotice()
|
||||
.then((notice) => {
|
||||
if (notice?.kind === 'coreRejected') {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.permissionFallback',
|
||||
{ reason: notice.reason },
|
||||
0,
|
||||
)
|
||||
} else if (notice?.kind === 'notAutoStarted') {
|
||||
requestService({ reason: 'serviceNotAutoStarted' })
|
||||
} else if (notice) {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.sidecarFallback',
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service fallback notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'dns_override::auto_disabled': () => {
|
||||
void takeDnsOverrideNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
showNotice.info('settings.modals.dns.protection.autoDisabled')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending DNS override notice', error)
|
||||
})
|
||||
},
|
||||
'enhance::discarded_keys': () => {
|
||||
void takeDiscardedKeysNotice()
|
||||
.then((keys) => {
|
||||
if (keys) {
|
||||
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
|
||||
keys,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending discarded keys notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'tun_mode::auto_disabled': () =>
|
||||
showNotice.info(
|
||||
'settings.sections.system.notifications.tunMode.autoDisabled',
|
||||
),
|
||||
'tun_mode::auto_disable_failed': () =>
|
||||
showNotice.error(
|
||||
'settings.sections.system.notifications.tunMode.autoDisableFailed',
|
||||
),
|
||||
'app_restart::core_stop_failed': () =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.restartCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
'app_quit::core_stop_failed': () =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.quitCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
update_with_clash_proxy: () =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.updater.withClashProxySuccess',
|
||||
msg,
|
||||
),
|
||||
update_failed_even_with_clash: () =>
|
||||
showNotice.error(
|
||||
'settings.feedback.notifications.updater.withClashProxyFailed',
|
||||
msg,
|
||||
),
|
||||
'reactivate_profiles::error': () => showNotice.error(msg),
|
||||
update_failed: () => showNotice.error(msg),
|
||||
'config_validate::boot_error': () =>
|
||||
showNotice.error('shared.feedback.validation.config.bootFailed', msg),
|
||||
'config_validate::error': () =>
|
||||
showNotice.error('shared.feedback.validation.config.failed', msg),
|
||||
'config_validate::process_terminated': () =>
|
||||
showNotice.error('shared.feedback.validation.config.processTerminated'),
|
||||
'config_validate::script_error': () =>
|
||||
showNotice.error('shared.feedback.validation.script.fileError', msg),
|
||||
'config_validate::script_syntax_error': () =>
|
||||
showNotice.error('shared.feedback.validation.script.syntaxError', msg),
|
||||
'config_validate::script_missing_main': () =>
|
||||
showNotice.error('shared.feedback.validation.script.missingMain', msg),
|
||||
'config_validate::file_not_found': () =>
|
||||
showNotice.error('shared.feedback.validation.script.fileNotFound', msg),
|
||||
'config_validate::yaml_syntax_error': () =>
|
||||
showNotice.error('shared.feedback.validation.yaml.syntaxError', msg),
|
||||
'config_validate::yaml_read_error': () =>
|
||||
showNotice.error('shared.feedback.validation.yaml.readError', msg),
|
||||
'config_validate::yaml_mapping_error': () =>
|
||||
showNotice.error('shared.feedback.validation.yaml.mappingError', msg),
|
||||
'config_validate::merge_syntax_error': () =>
|
||||
showNotice.error('shared.feedback.validation.merge.syntaxError', msg),
|
||||
'config_validate::merge_mapping_error': () =>
|
||||
showNotice.error('shared.feedback.validation.merge.mappingError', msg),
|
||||
'config_core::change_success': () =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.clash.changeSuccess',
|
||||
msg,
|
||||
),
|
||||
'config_core::change_error': () =>
|
||||
showNotice.error(
|
||||
'settings.feedback.notifications.clash.changeFailed',
|
||||
msg,
|
||||
),
|
||||
'mixed_port::fallback': () => {
|
||||
const [oldPort, newPort] = msg.split(',')
|
||||
showNotice.info('settings.modals.clashPort.messages.automaticFallback', {
|
||||
oldPort,
|
||||
newPort,
|
||||
})
|
||||
},
|
||||
'mixed_port::fallback_error': () =>
|
||||
showNotice.error(
|
||||
'settings.modals.clashPort.messages.automaticFallbackFailed',
|
||||
{ error: msg },
|
||||
),
|
||||
}
|
||||
|
||||
const handler = handlers[status as NoticeStatus]
|
||||
if (handler) {
|
||||
handler()
|
||||
handler(msg, navigate)
|
||||
} else {
|
||||
console.warn(`未处理的通知状态: ${status}`)
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { afterEach, expect, test, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/services/cmds', () => ({
|
||||
getProfiles: vi.fn(),
|
||||
patchProfile: vi.fn().mockResolvedValue(undefined),
|
||||
syncTrayProxySelection: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
vi.mock('@/services/query-client', () => ({ revalidateQueries: vi.fn() }))
|
||||
vi.mock('@/services/notice-service', () => ({ showNotice: { error: vi.fn() } }))
|
||||
|
||||
import {
|
||||
getProfiles,
|
||||
patchProfile,
|
||||
syncTrayProxySelection,
|
||||
} from '@/services/cmds'
|
||||
import { revalidateQueries } from '@/services/query-client'
|
||||
|
||||
import {
|
||||
profileTestUrls,
|
||||
saveProfileTestUrls,
|
||||
commitProfileTestUrl,
|
||||
} from './proxy-test-url'
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
test('committed edits retain their profile and merge with fresh saved URLs', async () => {
|
||||
vi.mocked(getProfiles).mockResolvedValue({
|
||||
current: 'new-profile',
|
||||
items: [
|
||||
{
|
||||
uid: 'old-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { untouched: 'https://example.com/' },
|
||||
},
|
||||
],
|
||||
})
|
||||
await commitProfileTestUrl('old-profile', 'A', ' http://localhost/new ')
|
||||
expect(patchProfile).toHaveBeenCalledExactlyOnceWith('old-profile', {
|
||||
latency_test_urls: {
|
||||
untouched: 'https://example.com/',
|
||||
A: 'http://localhost/new',
|
||||
},
|
||||
})
|
||||
expect(revalidateQueries).toHaveBeenCalledWith([['getProfiles']])
|
||||
expect(syncTrayProxySelection).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
test('migration cannot overwrite a previously saved or cleared override', async () => {
|
||||
vi.mocked(getProfiles).mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
uid: 'p',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: '' },
|
||||
},
|
||||
],
|
||||
})
|
||||
await saveProfileTestUrls('p', { A: 'http://localhost/stale' }, true)
|
||||
expect(patchProfile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('pending edits survive a reload and older save acknowledgements', async () => {
|
||||
vi.useFakeTimers()
|
||||
const profile = {
|
||||
uid: 'pending-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: 'http://localhost/old' },
|
||||
}
|
||||
vi.mocked(getProfiles).mockResolvedValue({ items: [profile] })
|
||||
let finish!: () => void
|
||||
vi.mocked(patchProfile).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<null>((resolve) => {
|
||||
finish = () => resolve(null)
|
||||
}),
|
||||
)
|
||||
const first = commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new')
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/new')
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/new')
|
||||
const second = commitProfileTestUrl(
|
||||
profile.uid,
|
||||
'A',
|
||||
'http://localhost/newest',
|
||||
)
|
||||
finish()
|
||||
await first
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/newest')
|
||||
await second
|
||||
expect(patchProfile).toHaveBeenLastCalledWith(profile.uid, {
|
||||
latency_test_urls: { A: 'http://localhost/newest' },
|
||||
})
|
||||
})
|
||||
|
||||
test('a failed save preserves the committed input across profile refreshes', async () => {
|
||||
const profile = {
|
||||
uid: 'failed-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: 'http://localhost/old' },
|
||||
}
|
||||
vi.mocked(getProfiles).mockResolvedValue({ items: [profile] })
|
||||
vi.mocked(patchProfile).mockRejectedValueOnce(new Error('save failed'))
|
||||
const log = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
await commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new')
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/new')
|
||||
await commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new')
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/old')
|
||||
log.mockRestore()
|
||||
})
|
||||
@@ -1,101 +0,0 @@
|
||||
import {
|
||||
getProfiles,
|
||||
patchProfile,
|
||||
syncTrayProxySelection,
|
||||
} from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
|
||||
function readLegacyTestUrls(uid: string): Record<string, string> {
|
||||
try {
|
||||
const states = JSON.parse(localStorage.getItem('proxy-head-state') || '{}')[
|
||||
uid
|
||||
]
|
||||
return Object.fromEntries(
|
||||
Object.entries(states || {}).flatMap(([name, state]) => {
|
||||
const url = (state as { testUrl?: string }).testUrl
|
||||
return typeof url === 'string' && url.trim() ? [[name, url.trim()]] : []
|
||||
}),
|
||||
)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveProfileTestUrls(
|
||||
uid: string,
|
||||
urls: Record<string, string>,
|
||||
migrateOnly = false,
|
||||
) {
|
||||
return mutate(
|
||||
async () => {
|
||||
const profiles = await getProfiles()
|
||||
const profile = profiles.items?.find((item) => item.uid === uid)
|
||||
if (!profile || (migrateOnly && profile.latency_test_urls !== undefined))
|
||||
return
|
||||
await patchProfile(uid, {
|
||||
latency_test_urls: {
|
||||
...(profile.latency_test_urls ?? readLegacyTestUrls(uid)),
|
||||
...urls,
|
||||
},
|
||||
})
|
||||
await syncTrayProxySelection().catch((error) => {
|
||||
console.error(
|
||||
'Failed to refresh tray after saving latency test URL:',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
{ id: `patch-profile:${uid}`, revalidate: [['getProfiles']] },
|
||||
)
|
||||
}
|
||||
|
||||
const migrating = new Set<string>()
|
||||
|
||||
export function migrateProfileTestUrls(profiles: IProfilesConfig) {
|
||||
for (const profile of profiles.items || []) {
|
||||
const uid = profile.uid
|
||||
if (!uid) continue
|
||||
if (profile.latency_test_urls !== undefined || migrating.has(uid)) continue
|
||||
const urls = readLegacyTestUrls(uid)
|
||||
if (!Object.keys(urls).length) continue
|
||||
migrating.add(uid)
|
||||
void saveProfileTestUrls(uid, urls, true)
|
||||
.catch((error) => {
|
||||
console.error('Failed to migrate latency test URLs:', error)
|
||||
})
|
||||
.finally(() => migrating.delete(uid))
|
||||
}
|
||||
}
|
||||
|
||||
const pending = new Map<string, Record<string, { url: string }>>()
|
||||
|
||||
export async function commitProfileTestUrl(
|
||||
uid: string,
|
||||
group: string,
|
||||
url: string,
|
||||
) {
|
||||
const edit = { url: url.trim() }
|
||||
const urls = { ...pending.get(uid), [group]: edit }
|
||||
pending.set(uid, urls)
|
||||
try {
|
||||
await saveProfileTestUrls(uid, { [group]: edit.url })
|
||||
const current = pending.get(uid)
|
||||
if (current?.[group] === edit) {
|
||||
delete current[group]
|
||||
if (!Object.keys(current).length) pending.delete(uid)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save latency test URL:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function profileTestUrls(profile: IProfileItem): Record<string, string> {
|
||||
return {
|
||||
...profile.latency_test_urls,
|
||||
...Object.fromEntries(
|
||||
Object.entries(
|
||||
(profile.uid ? pending.get(profile.uid) : undefined) ?? {},
|
||||
).map(([group, edit]) => [group, edit.url]),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,7 @@ const withLoading = (
|
||||
next.delete(uid)
|
||||
}
|
||||
}
|
||||
if (next.size === state.profileUpdates.loading.size) return state
|
||||
return {
|
||||
...state,
|
||||
profileUpdates: { ...state.profileUpdates, loading: next },
|
||||
|
||||
@@ -322,6 +322,8 @@ export const translationKeys = [
|
||||
'proxies.page.empty.actions.openLogs',
|
||||
'proxies.page.chain.header',
|
||||
'proxies.page.chain.warning',
|
||||
'proxies.page.chain.confirmEnable',
|
||||
'proxies.page.chain.keepDisabled',
|
||||
'proxies.page.chain.empty',
|
||||
'proxies.page.chain.instruction',
|
||||
'proxies.page.chain.minimumNodes',
|
||||
|
||||
@@ -525,6 +525,7 @@ export interface TranslationResources {
|
||||
toggleChain: string
|
||||
}
|
||||
chain: {
|
||||
confirmEnable: string
|
||||
connectFailed: string
|
||||
disconnectFailed: string
|
||||
duplicateNode: string
|
||||
@@ -533,6 +534,7 @@ export interface TranslationResources {
|
||||
exitNode: string
|
||||
header: string
|
||||
instruction: string
|
||||
keepDisabled: string
|
||||
minimumNodes: string
|
||||
minimumNodesHint: string
|
||||
warning: string
|
||||
|
||||
@@ -20,19 +20,6 @@ export const isValidUrl = (value: string) =>
|
||||
require_tld: false,
|
||||
})
|
||||
|
||||
export const isValidLatencyTestUrl = (value: string) => {
|
||||
const raw = value.trim()
|
||||
if (!/^https?:\/\//i.test(raw)) return false
|
||||
try {
|
||||
const url = new URL(raw)
|
||||
return (
|
||||
(url.protocol === 'http:' || url.protocol === 'https:') && !!url.hostname
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const isValidPort = (value: string) => isPort(value.trim())
|
||||
|
||||
export const normalizeHost = (value: string): string | null => {
|
||||
|
||||
Reference in New Issue
Block a user