mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 02:32:09 +08:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd6be642e | ||
|
|
433230aedc | ||
|
|
05475c7bb5 | ||
|
|
eb60c21c62 | ||
|
|
4489c9a26f | ||
|
|
98a7932742 | ||
|
|
c678d60964 | ||
|
|
d5bc4ff58b | ||
|
|
b81981162b | ||
|
|
6fa6d7664f | ||
|
|
4967ad347e | ||
|
|
6394f347f5 | ||
|
|
e56711b2db | ||
|
|
3e28b287d1 | ||
|
|
51a4aa80dd |
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:2": {
|
"ghcr.io/devcontainers/features/node:2": {
|
||||||
"version": "26.13.0",
|
"version": "24.22.1",
|
||||||
"pnpmVersion": "12.15.0"
|
"pnpmVersion": "12.15.0"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/rust:1": {
|
"ghcr.io/devcontainers/features/rust:1": {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -185,7 +185,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -282,7 +282,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -426,7 +426,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -524,7 +524,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6.1.0
|
- uses: pnpm/action-setup@v6.1.0
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Restore pnpm cache
|
- name: Restore pnpm cache
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -331,7 +331,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v6
|
uses: pnpm/action-setup@v6
|
||||||
@@ -466,7 +466,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -547,7 +547,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -573,7 +573,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -638,7 +638,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v6
|
uses: pnpm/action-setup@v6
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
nodejs 26.13.0
|
nodejs 24.22.1
|
||||||
pnpm 12.15.0
|
pnpm 12.15.0
|
||||||
rust 1.99.1
|
rust 1.99.1
|
||||||
|
|||||||
Generated
+24
-25
@@ -1175,7 +1175,7 @@ dependencies = [
|
|||||||
"windows-service",
|
"windows-service",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
"winreg 0.56.0",
|
"winreg 0.56.0",
|
||||||
"zip 9.1.1",
|
"zip 9.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2034,7 +2034,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.2",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2293,7 +2293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2809,7 +2809,7 @@ dependencies = [
|
|||||||
"gobject-sys 0.21.5",
|
"gobject-sys 0.21.5",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 7.0.8",
|
"system-deps 7.0.8",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4051,9 +4051,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lzma-rust2"
|
name = "lzma-rust2"
|
||||||
version = "0.23.0"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
|
checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sha2 0.11.1",
|
"sha2 0.11.1",
|
||||||
]
|
]
|
||||||
@@ -4234,7 +4234,7 @@ dependencies = [
|
|||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4401,7 +4401,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4878,7 +4878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.45.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5543,7 +5543,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6060,7 +6060,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6119,7 +6119,7 @@ dependencies = [
|
|||||||
"security-framework",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6739,7 +6739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7805,10 +7805,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.4.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8474,7 +8474,7 @@ dependencies = [
|
|||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8553,9 +8553,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typed-path"
|
name = "typed-path"
|
||||||
version = "0.13.0"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e"
|
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
@@ -8583,7 +8583,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9125,7 +9125,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9146,7 +9146,7 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-quartz-core",
|
"objc2-quartz-core",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -9721,7 +9721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10066,9 +10066,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "9.1.1"
|
version = "9.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "518b847fd864440c379748ad684c449f293af68978a485a4ff16f7c901c1f7c4"
|
checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"bzip2",
|
"bzip2",
|
||||||
@@ -10087,7 +10087,6 @@ dependencies = [
|
|||||||
"sha1 0.11.0",
|
"sha1 0.11.0",
|
||||||
"time",
|
"time",
|
||||||
"typed-path",
|
"typed-path",
|
||||||
"windows-sys 0.61.2",
|
|
||||||
"zeroize",
|
"zeroize",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
"zstd",
|
"zstd",
|
||||||
|
|||||||
@@ -11,10 +11,6 @@
|
|||||||
- 修复 代理 在全局模式下无法展开节点组
|
- 修复 代理 在全局模式下无法展开节点组
|
||||||
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
||||||
|
|
||||||
**🍎 macOS**
|
|
||||||
|
|
||||||
- macOS:修复 Bigsur 上首页测速和流量图无法显示的问题
|
|
||||||
|
|
||||||
**🐧 Linux**
|
**🐧 Linux**
|
||||||
|
|
||||||
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
||||||
|
|||||||
+3
-3
@@ -79,7 +79,7 @@
|
|||||||
"react-dom": "19.3.1",
|
"react-dom": "19.3.1",
|
||||||
"react-error-boundary": "6.1.6",
|
"react-error-boundary": "6.1.6",
|
||||||
"react-hook-form": "^7.88.0",
|
"react-hook-form": "^7.88.0",
|
||||||
"react-i18next": "17.1.9",
|
"react-i18next": "17.1.8",
|
||||||
"react-markdown": "10.1.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-router": "^8.4.0",
|
"react-router": "^8.4.0",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@tauri-apps/cli": "2.12.3",
|
"@tauri-apps/cli": "2.12.3",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^26.0.0",
|
"@types/node": "^24.13.6",
|
||||||
"@types/react": "19.3.0",
|
"@types/react": "19.3.0",
|
||||||
"@types/react-dom": "19.3.0",
|
"@types/react-dom": "19.3.0",
|
||||||
"@types/validator": "^13.15.10",
|
"@types/validator": "^13.15.10",
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.5.7",
|
"eslint-plugin-react-refresh": "^0.5.7",
|
||||||
"eslint-plugin-sukka": "^10.2.0",
|
"eslint-plugin-sukka": "^10.2.0",
|
||||||
"eslint-plugin-unused-imports": "^4.4.1",
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
"glob": "^14.0.0",
|
"glob": "^13.0.6",
|
||||||
"globals": "^17.12.0",
|
"globals": "^17.12.0",
|
||||||
"https-proxy-agent": "^9.1.0",
|
"https-proxy-agent": "^9.1.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
|||||||
Generated
+41
-74
@@ -275,8 +275,8 @@ importers:
|
|||||||
specifier: ^7.88.0
|
specifier: ^7.88.0
|
||||||
version: 7.88.0(react@19.3.1)
|
version: 7.88.0(react@19.3.1)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: 17.1.9
|
specifier: 17.1.8
|
||||||
version: 17.1.9(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3)
|
version: 17.1.8(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3)
|
||||||
react-markdown:
|
react-markdown:
|
||||||
specifier: 10.1.0
|
specifier: 10.1.0
|
||||||
version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)
|
version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)
|
||||||
@@ -324,8 +324,8 @@ importers:
|
|||||||
specifier: ^4.17.12
|
specifier: ^4.17.12
|
||||||
version: 4.17.12
|
version: 4.17.12
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^26.0.0
|
specifier: ^24.13.6
|
||||||
version: 26.6.5
|
version: 24.13.6
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 19.3.0
|
specifier: 19.3.0
|
||||||
version: 19.3.0
|
version: 19.3.0
|
||||||
@@ -337,10 +337,10 @@ importers:
|
|||||||
version: 13.15.10
|
version: 13.15.10
|
||||||
'@vitejs/plugin-legacy':
|
'@vitejs/plugin-legacy':
|
||||||
specifier: ^8.2.3
|
specifier: ^8.2.3
|
||||||
version: 8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
version: 8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^6.1.1
|
specifier: ^6.1.1
|
||||||
version: 6.1.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
version: 6.1.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
||||||
'@wdio/tauri-service':
|
'@wdio/tauri-service':
|
||||||
specifier: 1.5.0
|
specifier: 1.5.0
|
||||||
version: 1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
|
version: 1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
|
||||||
@@ -387,8 +387,8 @@ importers:
|
|||||||
specifier: ^4.4.1
|
specifier: ^4.4.1
|
||||||
version: 4.4.1(@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))
|
version: 4.4.1(@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))
|
||||||
glob:
|
glob:
|
||||||
specifier: ^14.0.0
|
specifier: ^13.0.6
|
||||||
version: 14.0.3
|
version: 13.0.6
|
||||||
globals:
|
globals:
|
||||||
specifier: ^17.12.0
|
specifier: ^17.12.0
|
||||||
version: 17.12.0
|
version: 17.12.0
|
||||||
@@ -427,13 +427,13 @@ importers:
|
|||||||
version: 8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
|
version: 8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.3.0
|
specifier: ^8.3.0
|
||||||
version: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
version: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
||||||
vite-plugin-svgr:
|
vite-plugin-svgr:
|
||||||
specifier: ^5.2.0
|
specifier: ^5.2.0
|
||||||
version: 5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
version: 5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^5.0.1
|
specifier: ^5.0.1
|
||||||
version: 5.0.1(@types/node@26.6.5)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
version: 5.0.1(@types/node@24.13.6)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
||||||
webdriverio:
|
webdriverio:
|
||||||
specifier: 10.1.0
|
specifier: 10.1.0
|
||||||
version: 10.1.0(supports-color@7.2.0)(yauzl@2.10.0)
|
version: 10.1.0(supports-color@7.2.0)(yauzl@2.10.0)
|
||||||
@@ -1387,9 +1387,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@isaacs/nonrecursive-glob-match@1.0.2':
|
|
||||||
resolution: {integrity: sha512-GJPHIgPPtFl+nLhFHyF0zZoFcOFLsZWbHFFzo+Zr+PWDXJbl9hHxb2w+9SiF2fHmLgKaXIpUzQo3SbBBZ9WObw==}
|
|
||||||
|
|
||||||
'@jest/diff-sequences@30.5.0':
|
'@jest/diff-sequences@30.5.0':
|
||||||
resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==}
|
resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==}
|
||||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
@@ -2324,8 +2321,8 @@ packages:
|
|||||||
'@types/node@22.20.5':
|
'@types/node@22.20.5':
|
||||||
resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==}
|
resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==}
|
||||||
|
|
||||||
'@types/node@26.6.5':
|
'@types/node@24.13.6':
|
||||||
resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==}
|
resolution: {integrity: sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==}
|
||||||
|
|
||||||
'@types/parse-json@4.0.2':
|
'@types/parse-json@4.0.2':
|
||||||
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
||||||
@@ -3597,10 +3594,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
glob@14.0.3:
|
|
||||||
resolution: {integrity: sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==}
|
|
||||||
engines: {node: 18 || 20 || >=22}
|
|
||||||
|
|
||||||
globals@17.12.0:
|
globals@17.12.0:
|
||||||
resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==}
|
resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4183,10 +4176,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
|
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
minimatch@11.0.2:
|
|
||||||
resolution: {integrity: sha512-pWTgz5CN3tjqyZNSK2tePlSmu9IwM41q8yn8Usdqqwe9TIHh+9dEpLLjF4uApsfPEAZY71qtSiA2B2APEr+1Yw==}
|
|
||||||
engines: {node: 18 || 20 || >=22}
|
|
||||||
|
|
||||||
minipass@7.1.3:
|
minipass@7.1.3:
|
||||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
@@ -4348,10 +4337,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
path-scurry@3.0.0:
|
|
||||||
resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==}
|
|
||||||
engines: {node: 18 || 20 || >=22}
|
|
||||||
|
|
||||||
path-type@4.0.0:
|
path-type@4.0.0:
|
||||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4458,8 +4443,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
|
|
||||||
react-i18next@17.1.9:
|
react-i18next@17.1.8:
|
||||||
resolution: {integrity: sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==}
|
resolution: {integrity: sha512-vGH+Tbhqbv5LdN9KvC9GEmQ6ATxJAjyxPEK0zSCSRlkeDqCPNxVtEOvDNRAz+51HTxc6QPwhnHaFD7ICu6pD1A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
i18next: '>= 26.2.0'
|
i18next: '>= 26.2.0'
|
||||||
react: '>= 16.8.0'
|
react: '>= 16.8.0'
|
||||||
@@ -4880,8 +4865,8 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
undici-types@8.9.0:
|
undici-types@7.18.2:
|
||||||
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
|
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||||
|
|
||||||
undici@6.28.1:
|
undici@6.28.1:
|
||||||
resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==}
|
resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==}
|
||||||
@@ -6318,8 +6303,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minipass: 7.1.3
|
minipass: 7.1.3
|
||||||
|
|
||||||
'@isaacs/nonrecursive-glob-match@1.0.2': {}
|
|
||||||
|
|
||||||
'@jest/diff-sequences@30.5.0': {}
|
'@jest/diff-sequences@30.5.0': {}
|
||||||
|
|
||||||
'@jest/expect-utils@30.5.2':
|
'@jest/expect-utils@30.5.2':
|
||||||
@@ -6330,7 +6313,7 @@ snapshots:
|
|||||||
|
|
||||||
'@jest/pattern@30.5.0':
|
'@jest/pattern@30.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 26.6.5
|
'@types/node': 24.13.6
|
||||||
jest-regex-util: 30.5.0
|
jest-regex-util: 30.5.0
|
||||||
|
|
||||||
'@jest/schemas@30.5.0':
|
'@jest/schemas@30.5.0':
|
||||||
@@ -6343,7 +6326,7 @@ snapshots:
|
|||||||
'@jest/schemas': 30.5.0
|
'@jest/schemas': 30.5.0
|
||||||
'@types/istanbul-lib-coverage': 2.0.6
|
'@types/istanbul-lib-coverage': 2.0.6
|
||||||
'@types/istanbul-reports': 3.0.4
|
'@types/istanbul-reports': 3.0.4
|
||||||
'@types/node': 26.6.5
|
'@types/node': 24.13.6
|
||||||
'@types/yargs': 17.0.35
|
'@types/yargs': 17.0.35
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|
||||||
@@ -7034,9 +7017,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/node@26.6.5':
|
'@types/node@24.13.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 8.9.0
|
undici-types: 7.18.2
|
||||||
|
|
||||||
'@types/parse-json@4.0.2': {}
|
'@types/parse-json@4.0.2': {}
|
||||||
|
|
||||||
@@ -7058,7 +7041,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/split2@4.2.3':
|
'@types/split2@4.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 26.6.5
|
'@types/node': 24.13.6
|
||||||
|
|
||||||
'@types/stack-utils@2.0.3': {}
|
'@types/stack-utils@2.0.3': {}
|
||||||
|
|
||||||
@@ -7073,7 +7056,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 26.6.5
|
'@types/node': 24.13.6
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3': {}
|
'@types/yargs-parser@21.0.3': {}
|
||||||
|
|
||||||
@@ -7244,7 +7227,7 @@ snapshots:
|
|||||||
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
|
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitejs/plugin-legacy@8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))':
|
'@vitejs/plugin-legacy@8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||||
'@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
|
'@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
|
||||||
@@ -7259,23 +7242,23 @@ snapshots:
|
|||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
systemjs: 6.15.1
|
systemjs: 6.15.1
|
||||||
terser: 5.51.2
|
terser: 5.51.2
|
||||||
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitejs/plugin-react@6.1.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))':
|
'@vitejs/plugin-react@6.1.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.1
|
'@rolldown/pluginutils': 1.0.1
|
||||||
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
||||||
|
|
||||||
'@vitest/mocker@5.0.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))':
|
'@vitest/mocker@5.0.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
'@vitest/spy': 5.0.1
|
'@vitest/spy': 5.0.1
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 1.4.1
|
magic-string: 1.4.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
||||||
|
|
||||||
'@vitest/pretty-format@4.1.11':
|
'@vitest/pretty-format@4.1.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8453,12 +8436,6 @@ snapshots:
|
|||||||
minipass: 7.1.3
|
minipass: 7.1.3
|
||||||
path-scurry: 2.0.2
|
path-scurry: 2.0.2
|
||||||
|
|
||||||
glob@14.0.3:
|
|
||||||
dependencies:
|
|
||||||
minimatch: 11.0.2
|
|
||||||
minipass: 7.1.3
|
|
||||||
path-scurry: 3.0.0
|
|
||||||
|
|
||||||
globals@17.12.0: {}
|
globals@17.12.0: {}
|
||||||
|
|
||||||
gopd@1.2.0: {}
|
gopd@1.2.0: {}
|
||||||
@@ -8708,7 +8685,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jest/expect-utils': 30.5.2
|
'@jest/expect-utils': 30.5.2
|
||||||
'@jest/types': 30.5.1
|
'@jest/types': 30.5.1
|
||||||
'@types/node': 26.6.5
|
'@types/node': 24.13.6
|
||||||
jest-util: 30.5.1
|
jest-util: 30.5.1
|
||||||
|
|
||||||
jest-regex-util@30.5.0: {}
|
jest-regex-util@30.5.0: {}
|
||||||
@@ -8716,7 +8693,7 @@ snapshots:
|
|||||||
jest-util@30.5.1:
|
jest-util@30.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/types': 30.5.1
|
'@jest/types': 30.5.1
|
||||||
'@types/node': 26.6.5
|
'@types/node': 24.13.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
ci-info: 4.4.0
|
ci-info: 4.4.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -9273,11 +9250,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.12
|
brace-expansion: 5.0.12
|
||||||
|
|
||||||
minimatch@11.0.2:
|
|
||||||
dependencies:
|
|
||||||
'@isaacs/nonrecursive-glob-match': 1.0.2
|
|
||||||
brace-expansion: 5.0.12
|
|
||||||
|
|
||||||
minipass@7.1.3: {}
|
minipass@7.1.3: {}
|
||||||
|
|
||||||
minizlib@3.1.0:
|
minizlib@3.1.0:
|
||||||
@@ -9478,11 +9450,6 @@ snapshots:
|
|||||||
lru-cache: 11.5.3
|
lru-cache: 11.5.3
|
||||||
minipass: 7.1.3
|
minipass: 7.1.3
|
||||||
|
|
||||||
path-scurry@3.0.0:
|
|
||||||
dependencies:
|
|
||||||
lru-cache: 11.5.3
|
|
||||||
minipass: 7.1.3
|
|
||||||
|
|
||||||
path-type@4.0.0: {}
|
path-type@4.0.0: {}
|
||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
@@ -9562,7 +9529,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.3.1
|
react: 19.3.1
|
||||||
|
|
||||||
react-i18next@17.1.9(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3):
|
react-i18next@17.1.8(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.7
|
'@babel/runtime': 7.29.7
|
||||||
html-parse-stringify: 4.0.1
|
html-parse-stringify: 4.0.1
|
||||||
@@ -10016,7 +9983,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
undici-types@8.9.0: {}
|
undici-types@7.18.2: {}
|
||||||
|
|
||||||
undici@6.28.1: {}
|
undici@6.28.1: {}
|
||||||
|
|
||||||
@@ -10134,18 +10101,18 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
vite-plugin-svgr@5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)):
|
vite-plugin-svgr@5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.4.0
|
'@rollup/pluginutils': 5.4.0
|
||||||
'@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@6.0.3)
|
'@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@6.0.3)
|
||||||
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))(supports-color@7.2.0)
|
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))(supports-color@7.2.0)
|
||||||
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1):
|
vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.33.0
|
lightningcss: 1.33.0
|
||||||
picomatch: 4.0.7
|
picomatch: 4.0.7
|
||||||
@@ -10153,7 +10120,7 @@ snapshots:
|
|||||||
rolldown: 1.2.9
|
rolldown: 1.2.9
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.6.5
|
'@types/node': 24.13.6
|
||||||
esbuild: 0.28.2
|
esbuild: 0.28.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
@@ -10161,10 +10128,10 @@ snapshots:
|
|||||||
terser: 5.51.2
|
terser: 5.51.2
|
||||||
yaml: 2.9.1
|
yaml: 2.9.1
|
||||||
|
|
||||||
vitest@5.0.1(@types/node@26.6.5)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)):
|
vitest@5.0.1(@types/node@24.13.6)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/mocker': 5.0.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
'@vitest/mocker': 5.0.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
|
||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
es-module-lexer: 2.3.2
|
es-module-lexer: 2.3.2
|
||||||
expect-type: 1.4.0
|
expect-type: 1.4.0
|
||||||
@@ -10175,10 +10142,10 @@ snapshots:
|
|||||||
tinybench: 6.1.4
|
tinybench: 6.1.4
|
||||||
tinyexec: 1.3.0
|
tinyexec: 1.3.0
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.6.5
|
'@types/node': 24.13.6
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,27 @@ These rules supplement the repository's root `AGENTS.md`.
|
|||||||
- **Command ownership.** `services` owns backend command invocation and transport
|
- **Command ownership.** `services` owns backend command invocation and transport
|
||||||
wrappers. Components and hooks use those boundaries; plugin mutations follow
|
wrappers. Components and hooks use those boundaries; plugin mutations follow
|
||||||
the same intent ownership as backend commands.
|
the same intent ownership as backend commands.
|
||||||
|
- **State liveness.** Every `useState`/`useRef` member has a reachable write
|
||||||
|
that differs from its initial value and a read that observes the difference.
|
||||||
|
A value that can only ever hold its initial value is a constant: declare the
|
||||||
|
constant and delete the state. A ref nothing reads is deleted, not kept.
|
||||||
|
- **Surface liveness.** Anything a module exposes — props, parameters, context
|
||||||
|
members, imperative-handle methods — ships with at least one external caller
|
||||||
|
in the same change. "Reserved for future use" fields, `_`-prefixed params
|
||||||
|
that callers still pass, and error or callback channels that every receiver
|
||||||
|
ignores are removed, not maintained.
|
||||||
|
- **Path liveness.** A fallback or branch names the concrete runtime condition
|
||||||
|
that reaches it. If no supported runtime can reach it, the branch is not
|
||||||
|
added — or it replaces the primary path. Constant subexpressions and cleanup
|
||||||
|
of resources that cannot exist yet are simplified away.
|
||||||
|
- **Effect economy.** One user interaction produces one state write and one
|
||||||
|
command. A handler does not re-apply a value its wrapper already applied,
|
||||||
|
and an effect does not re-run a refresh another branch of the same effect
|
||||||
|
just ran.
|
||||||
|
- **Mount economy.** Components that issue commands or subscriptions on mount
|
||||||
|
are mounted only where their UI is reachable. A dialog no rendered control
|
||||||
|
can open stays unmounted, not mounted-and-idle.
|
||||||
|
- **Resource bounds.** Every cache key, timer and subscription a feature
|
||||||
|
allocates is reused or released on refresh; refresh never allocates an
|
||||||
|
unbounded sequence of new keys. Retries re-create per-attempt resources so
|
||||||
|
a retry can actually succeed.
|
||||||
|
|||||||
@@ -22,23 +22,16 @@ interface BaseSplitChipEditorProps {
|
|||||||
error?: boolean
|
error?: boolean
|
||||||
helperText?: ReactNode
|
helperText?: ReactNode
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
rows?: number
|
|
||||||
separator?: string
|
separator?: string
|
||||||
splitPattern?: RegExp
|
|
||||||
defaultMode?: BaseSplitChipEditorMode
|
|
||||||
showModeToggle?: boolean
|
|
||||||
ariaLabel?: string
|
ariaLabel?: string
|
||||||
addLabel?: ReactNode
|
|
||||||
emptyLabel?: ReactNode
|
|
||||||
modeLabels?: Partial<Record<BaseSplitChipEditorMode, ReactNode>>
|
|
||||||
renderHeader?: (modeToggle: ReactNode) => ReactNode
|
renderHeader?: (modeToggle: ReactNode) => ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/
|
const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/
|
||||||
|
|
||||||
const splitValue = (value: string, splitPattern: RegExp) =>
|
const splitValue = (value: string) =>
|
||||||
value
|
value
|
||||||
.split(splitPattern)
|
.split(DEFAULT_SPLIT_PATTERN)
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
@@ -49,35 +42,25 @@ export const BaseSplitChipEditor = ({
|
|||||||
error = false,
|
error = false,
|
||||||
helperText,
|
helperText,
|
||||||
placeholder,
|
placeholder,
|
||||||
rows = 4,
|
|
||||||
separator = ',',
|
separator = ',',
|
||||||
splitPattern = DEFAULT_SPLIT_PATTERN,
|
|
||||||
defaultMode = 'visual',
|
|
||||||
showModeToggle = true,
|
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
addLabel,
|
|
||||||
emptyLabel,
|
|
||||||
modeLabels,
|
|
||||||
renderHeader,
|
renderHeader,
|
||||||
}: BaseSplitChipEditorProps) => {
|
}: BaseSplitChipEditorProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [mode, setMode] = useState<BaseSplitChipEditorMode>(defaultMode)
|
const [mode, setMode] = useState<BaseSplitChipEditorMode>('visual')
|
||||||
const [draft, setDraft] = useState('')
|
const [draft, setDraft] = useState('')
|
||||||
|
|
||||||
const resolvedLabels = useMemo(
|
const resolvedLabels = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
visual: modeLabels?.visual ?? t('shared.editorModes.visualization'),
|
visual: t('shared.editorModes.visualization'),
|
||||||
advanced: modeLabels?.advanced ?? t('shared.editorModes.advanced'),
|
advanced: t('shared.editorModes.advanced'),
|
||||||
add: addLabel ?? t('shared.actions.new'),
|
add: t('shared.actions.new'),
|
||||||
empty: emptyLabel ?? t('shared.statuses.empty'),
|
empty: t('shared.statuses.empty'),
|
||||||
}),
|
}),
|
||||||
[t, modeLabels, addLabel, emptyLabel],
|
[t],
|
||||||
)
|
)
|
||||||
|
|
||||||
const values = useMemo(
|
const values = useMemo(() => splitValue(value), [value])
|
||||||
() => splitValue(value, splitPattern),
|
|
||||||
[value, splitPattern],
|
|
||||||
)
|
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const counts = new Map<string, number>()
|
const counts = new Map<string, number>()
|
||||||
@@ -92,7 +75,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
}, [values])
|
}, [values])
|
||||||
|
|
||||||
const handleAddDraft = () => {
|
const handleAddDraft = () => {
|
||||||
const nextValues = splitValue(draft, splitPattern)
|
const nextValues = splitValue(draft)
|
||||||
if (!nextValues.length) {
|
if (!nextValues.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -113,7 +96,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
const resolvedAriaLabel =
|
const resolvedAriaLabel =
|
||||||
ariaLabel ?? (typeof toggleLabel === 'string' ? toggleLabel : undefined)
|
ariaLabel ?? (typeof toggleLabel === 'string' ? toggleLabel : undefined)
|
||||||
|
|
||||||
const modeToggle = showModeToggle ? (
|
const modeToggle = (
|
||||||
<Tooltip title={toggleLabel}>
|
<Tooltip title={toggleLabel}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -128,7 +111,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
<ToggleIcon fontSize="small" />
|
<ToggleIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -202,7 +185,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
size="small"
|
size="small"
|
||||||
multiline
|
multiline
|
||||||
rows={rows}
|
rows={4}
|
||||||
sx={{ width: '100%' }}
|
sx={{ width: '100%' }}
|
||||||
value={value}
|
value={value}
|
||||||
helperText={helperText}
|
helperText={helperText}
|
||||||
|
|||||||
@@ -596,7 +596,7 @@ export const CurrentProxyCard = () => {
|
|||||||
[getProfileStorageKey],
|
[getProfileStorageKey],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { handleSelectChange } = useProxySelection({
|
const { select } = useProxySelection({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
refreshProxy()
|
refreshProxy()
|
||||||
},
|
},
|
||||||
@@ -766,15 +766,9 @@ export const CurrentProxyCard = () => {
|
|||||||
const previousProxy = selectedGroup.now
|
const previousProxy = selectedGroup.now
|
||||||
const nextName = option.member.ref.name
|
const nextName = option.member.ref.name
|
||||||
// The profile selection is the durable source across core restarts and run modes.
|
// The profile selection is the durable source across core restarts and run modes.
|
||||||
handleSelectChange(
|
select(selectedGroup.name, nextName, previousProxy!, selectedGroup.fixed!)
|
||||||
selectedGroup.name,
|
|
||||||
previousProxy!,
|
|
||||||
selectedGroup.fixed!,
|
|
||||||
)({
|
|
||||||
target: { value: nextName },
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[handleSelectChange, isDirectMode, selectedGroup, unsortedProxyOptions],
|
[isDirectMode, select, selectedGroup, unsortedProxyOptions],
|
||||||
)
|
)
|
||||||
|
|
||||||
const goToProxies = useCallback(() => {
|
const goToProxies = useCallback(() => {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
import { useTrafficGraphDataEnhanced } from '@/hooks/use-traffic-monitor'
|
import { useTrafficGraphDataEnhanced } from '@/hooks/use-traffic-monitor'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { debugLog } from '@/utils/debug'
|
|
||||||
import parseTraffic from '@/utils/parse-traffic'
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
import {
|
import {
|
||||||
formatTrafficHourMinute,
|
formatTrafficHourMinute,
|
||||||
@@ -23,14 +22,7 @@ import {
|
|||||||
isSameTrafficData,
|
isSameTrafficData,
|
||||||
} from '@/utils/traffic-sampler'
|
} from '@/utils/traffic-sampler'
|
||||||
|
|
||||||
interface ITrafficItem {
|
|
||||||
up: number
|
|
||||||
down: number
|
|
||||||
timestamp?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnhancedCanvasTrafficGraphRef {
|
export interface EnhancedCanvasTrafficGraphRef {
|
||||||
appendData: (data: ITrafficItem) => void
|
|
||||||
toggleStyle: () => void
|
toggleStyle: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +134,6 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
const debounceTimeoutRef = useRef<number | null>(null)
|
const debounceTimeoutRef = useRef<number | null>(null)
|
||||||
const [currentFPS, setCurrentFPS] = useState(GRAPH_CONFIG.targetFPS)
|
|
||||||
|
|
||||||
const colors = useMemo(
|
const colors = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -183,8 +174,6 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
if (displayData.length === 0) {
|
if (displayData.length === 0) {
|
||||||
lastDataTimestampRef.current = 0
|
lastDataTimestampRef.current = 0
|
||||||
dataStaleRef.current = false
|
dataStaleRef.current = false
|
||||||
// eslint-disable-next-line @eslint-react/set-state-in-effect
|
|
||||||
setCurrentFPS(GRAPH_CONFIG.targetFPS)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,18 +189,11 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
}
|
}
|
||||||
}, [displayData])
|
}, [displayData])
|
||||||
|
|
||||||
const handleFocusStateChange = useCallback(
|
const handleFocusStateChange = useCallback((focused: boolean) => {
|
||||||
(focused: boolean) => {
|
isWindowFocusedRef.current = focused
|
||||||
isWindowFocusedRef.current = focused
|
|
||||||
|
|
||||||
if (focused || !pause_render_traffic_stats_on_blur) {
|
scheduleDrawGraphRef.current()
|
||||||
setCurrentFPS(GRAPH_CONFIG.targetFPS)
|
}, [])
|
||||||
}
|
|
||||||
|
|
||||||
scheduleDrawGraphRef.current()
|
|
||||||
},
|
|
||||||
[pause_render_traffic_stats_on_blur],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||||
@@ -684,19 +666,13 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(getX(0), getY(0))
|
ctx.moveTo(getX(0), getY(0))
|
||||||
|
|
||||||
if (chartStyle === 'bezier') {
|
for (let i = 1; i < data.length; i++) {
|
||||||
for (let i = 1; i < data.length; i++) {
|
const currentX = getX(i)
|
||||||
const currentX = getX(i)
|
const currentY = getY(i)
|
||||||
const currentY = getY(i)
|
const nextIndex = Math.min(i + 1, lastIndex)
|
||||||
const nextIndex = Math.min(i + 1, lastIndex)
|
const controlX = (currentX + getX(nextIndex)) / 2
|
||||||
const controlX = (currentX + getX(nextIndex)) / 2
|
const controlY = (currentY + getY(nextIndex)) / 2
|
||||||
const controlY = (currentY + getY(nextIndex)) / 2
|
ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
|
||||||
ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 1; i < data.length; i++) {
|
|
||||||
ctx.lineTo(getX(i), getY(i))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.lineTo(getX(lastIndex), height - padding.bottom)
|
ctx.lineTo(getX(lastIndex), height - padding.bottom)
|
||||||
@@ -984,21 +960,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
setChartStyle((prev) => (prev === 'bezier' ? 'line' : 'bezier'))
|
setChartStyle((prev) => (prev === 'bezier' ? 'line' : 'bezier'))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const appendData = useCallback((data: ITrafficItem) => {
|
useImperativeHandle(ref, () => ({ toggleStyle }), [toggleStyle])
|
||||||
debugLog(
|
|
||||||
'[EnhancedCanvasTrafficGraphV2] appendData called (using global data):',
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useImperativeHandle(
|
|
||||||
ref,
|
|
||||||
() => ({
|
|
||||||
appendData,
|
|
||||||
toggleStyle,
|
|
||||||
}),
|
|
||||||
[appendData, toggleStyle],
|
|
||||||
)
|
|
||||||
|
|
||||||
const getTimeRangeText = useCallback(() => {
|
const getTimeRangeText = useCallback(() => {
|
||||||
return t('home.components.traffic.patterns.minutes', {
|
return t('home.components.traffic.patterns.minutes', {
|
||||||
@@ -1142,7 +1104,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
{t('home.components.traffic.diagnostics', {
|
{t('home.components.traffic.diagnostics', {
|
||||||
points: displayData.length,
|
points: displayData.length,
|
||||||
compressed: samplerStats.compressedBufferSize,
|
compressed: samplerStats.compressedBufferSize,
|
||||||
fps: currentFPS,
|
fps: GRAPH_CONFIG.targetFPS,
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import {
|
|||||||
ArrowUpwardRounded,
|
ArrowUpwardRounded,
|
||||||
CloudDownloadRounded,
|
CloudDownloadRounded,
|
||||||
CloudUploadRounded,
|
CloudUploadRounded,
|
||||||
|
ErrorOutlineRounded,
|
||||||
LinkRounded,
|
LinkRounded,
|
||||||
MemoryRounded,
|
MemoryRounded,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
Grid,
|
Grid,
|
||||||
PaletteColor,
|
PaletteColor,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -15,9 +19,9 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { ReactNode, memo, useMemo, useRef } from 'react'
|
import { ReactNode, memo, useMemo, useRef } from 'react'
|
||||||
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { TrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
|
|
||||||
import { useConnectionSummaryData } from '@/hooks/use-connection-data'
|
import { useConnectionSummaryData } from '@/hooks/use-connection-data'
|
||||||
import { useMemoryData } from '@/hooks/use-memory-data'
|
import { useMemoryData } from '@/hooks/use-memory-data'
|
||||||
import { useTrafficData } from '@/hooks/use-traffic-data'
|
import { useTrafficData } from '@/hooks/use-traffic-data'
|
||||||
@@ -127,6 +131,50 @@ const CompactStatCard = memo(
|
|||||||
// 添加显示名称
|
// 添加显示名称
|
||||||
CompactStatCard.displayName = 'CompactStatCard'
|
CompactStatCard.displayName = 'CompactStatCard'
|
||||||
|
|
||||||
|
const fallbackSx = {
|
||||||
|
p: 2,
|
||||||
|
minHeight: 200,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
border: '1px dashed',
|
||||||
|
borderColor: 'error.main',
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: 'error.light',
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloadPage = () => window.location.reload()
|
||||||
|
|
||||||
|
const TrafficFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={fallbackSx}>
|
||||||
|
<ErrorOutlineRounded color="error" sx={{ fontSize: 48, mb: 2 }} />
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{t('shared.feedback.errors.trafficStats')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
{t('shared.feedback.errors.trafficStatsDescription')}
|
||||||
|
</Typography>
|
||||||
|
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
||||||
|
{error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: t('shared.feedback.errors.unknown')}
|
||||||
|
</Alert>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Button variant="contained" size="small" onClick={resetErrorBoundary}>
|
||||||
|
{t('shared.actions.retry')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" size="small" onClick={reloadPage}>
|
||||||
|
{t('shared.actions.refreshPage')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const EnhancedTrafficStats = () => {
|
export const EnhancedTrafficStats = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@@ -254,10 +302,11 @@ export const EnhancedTrafficStats = () => {
|
|||||||
}, [t, parsedData, displayMemory])
|
}, [t, parsedData, displayMemory])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrafficErrorBoundary
|
<ErrorBoundary
|
||||||
onError={(error, errorInfo) => {
|
FallbackComponent={TrafficFallback}
|
||||||
console.error('[EnhancedTrafficStats] 组件错误:', error, errorInfo)
|
onError={(error, errorInfo) =>
|
||||||
}}
|
console.error('[EnhancedTrafficStats]', error, errorInfo)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||||
{trafficGraph && (
|
{trafficGraph && (
|
||||||
@@ -273,6 +322,6 @@ export const EnhancedTrafficStats = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</TrafficErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,16 +86,11 @@ export const SystemInfoCard = () => {
|
|||||||
}, [verge, patchVerge])
|
}, [verge, patchVerge])
|
||||||
|
|
||||||
const handleRunningModeClick = useCallback(async () => {
|
const handleRunningModeClick = useCallback(async () => {
|
||||||
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
|
if (isSidecarMode) {
|
||||||
await installServiceAndRestartCore()
|
await installServiceAndRestartCore()
|
||||||
await mutateSystemState()
|
await mutateSystemState()
|
||||||
}
|
}
|
||||||
}, [
|
}, [isSidecarMode, installServiceAndRestartCore, mutateSystemState])
|
||||||
isSidecarMode,
|
|
||||||
isAdminMode,
|
|
||||||
installServiceAndRestartCore,
|
|
||||||
mutateSystemState,
|
|
||||||
])
|
|
||||||
|
|
||||||
const onCheckUpdate = useLockFn(async () => {
|
const onCheckUpdate = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -122,18 +117,16 @@ export const SystemInfoCard = () => {
|
|||||||
|
|
||||||
const runningModeStyle = useMemo(
|
const runningModeStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
cursor:
|
cursor: isSidecarMode ? 'pointer' : 'default',
|
||||||
isSidecarMode || (isAdminMode && isSidecarMode) ? 'pointer' : 'default',
|
textDecoration: isSidecarMode ? 'underline' : 'none',
|
||||||
textDecoration:
|
|
||||||
isSidecarMode || (isAdminMode && isSidecarMode) ? 'underline' : 'none',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
|
opacity: isSidecarMode ? 0.7 : 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[isSidecarMode, isAdminMode],
|
[isSidecarMode],
|
||||||
)
|
)
|
||||||
|
|
||||||
const getModeIcon = () => {
|
const getModeIcon = () => {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const TestCard = () => {
|
|||||||
x.uid === uid ? { ...x, ...patch } : x,
|
x.uid === uid ? { ...x, ...patch } : x,
|
||||||
)
|
)
|
||||||
|
|
||||||
mutateVerge({ ...verge, test_list: newList }, false)
|
mutateVerge({ ...verge, test_list: newList })
|
||||||
},
|
},
|
||||||
[testList, verge, mutateVerge],
|
[testList, verge, mutateVerge],
|
||||||
)
|
)
|
||||||
@@ -96,7 +96,7 @@ export const TestCard = () => {
|
|||||||
(uid: string) => {
|
(uid: string) => {
|
||||||
const newList = testList.filter((x) => x.uid !== uid)
|
const newList = testList.filter((x) => x.uid !== uid)
|
||||||
patchVerge({ test_list: newList })
|
patchVerge({ test_list: newList })
|
||||||
mutateVerge({ ...verge, test_list: newList }, false)
|
mutateVerge({ ...verge, test_list: newList })
|
||||||
},
|
},
|
||||||
[testList, verge, patchVerge, mutateVerge],
|
[testList, verge, patchVerge, mutateVerge],
|
||||||
)
|
)
|
||||||
@@ -129,7 +129,7 @@ export const TestCard = () => {
|
|||||||
newList.splice(newIndex, 0, removed)
|
newList.splice(newIndex, 0, removed)
|
||||||
|
|
||||||
// 优化:先本地更新,再异步 patch,避免UI卡死
|
// 优化:先本地更新,再异步 patch,避免UI卡死
|
||||||
mutateVerge({ ...verge, test_list: newList }, false)
|
mutateVerge({ ...verge, test_list: newList })
|
||||||
const patchFn = () => {
|
const patchFn = () => {
|
||||||
try {
|
try {
|
||||||
patchVerge({ test_list: newList })
|
patchVerge({ test_list: newList })
|
||||||
|
|||||||
@@ -35,10 +35,7 @@ export const LayoutSidebar = (props: LayoutSidebarProps) => {
|
|||||||
|
|
||||||
const handleMenuOrderOptimisticUpdate = useCallback(
|
const handleMenuOrderOptimisticUpdate = useCallback(
|
||||||
(order: string[]) => {
|
(order: string[]) => {
|
||||||
mutateVerge(
|
mutateVerge((prev) => (prev ? { ...prev, menu_order: order } : prev))
|
||||||
(prev) => (prev ? { ...prev, menu_order: order } : prev),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
[mutateVerge],
|
[mutateVerge],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
ArrowDownwardRounded,
|
ArrowDownwardRounded,
|
||||||
ArrowUpwardRounded,
|
ArrowUpwardRounded,
|
||||||
|
ErrorOutlineRounded,
|
||||||
MemoryRounded,
|
MemoryRounded,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import type { BoxProps, SvgIconProps, TypographyProps } from '@mui/material'
|
import type { BoxProps, SvgIconProps, TypographyProps } from '@mui/material'
|
||||||
import { Box, Typography } from '@mui/material'
|
import { Box, Typography } from '@mui/material'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { LightweightTrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
|
|
||||||
import { useMemoryData } from '@/hooks/use-memory-data'
|
import { useMemoryData } from '@/hooks/use-memory-data'
|
||||||
import { useTrafficData } from '@/hooks/use-traffic-data'
|
import { useTrafficData } from '@/hooks/use-traffic-data'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
@@ -17,6 +18,30 @@ import parseTraffic from '@/utils/parse-traffic'
|
|||||||
|
|
||||||
import { TrafficGraph, type TrafficRef } from './traffic-graph'
|
import { TrafficGraph, type TrafficRef } from './traffic-graph'
|
||||||
|
|
||||||
|
const unavailableSx = {
|
||||||
|
p: 1,
|
||||||
|
minHeight: 60,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bgcolor: 'error.light',
|
||||||
|
borderRadius: 1,
|
||||||
|
color: 'error.contrastText',
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrafficUnavailableFallback = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={unavailableSx}>
|
||||||
|
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
|
||||||
|
<Typography variant="caption">
|
||||||
|
{t('shared.feedback.errors.trafficUnavailable')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const LayoutTraffic = () => {
|
export const LayoutTraffic = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
@@ -75,7 +100,12 @@ export const LayoutTraffic = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LightweightTrafficErrorBoundary>
|
<ErrorBoundary
|
||||||
|
FallbackComponent={TrafficUnavailableFallback}
|
||||||
|
onError={(error, errorInfo) =>
|
||||||
|
console.error('[LayoutTraffic]', error, errorInfo)
|
||||||
|
}
|
||||||
|
>
|
||||||
<Box sx={{ position: 'relative' }}>
|
<Box sx={{ position: 'relative' }}>
|
||||||
{trafficGraph && pageVisible && (
|
{trafficGraph && pageVisible && (
|
||||||
<div
|
<div
|
||||||
@@ -130,7 +160,6 @@ export const LayoutTraffic = () => {
|
|||||||
cursor: 'auto',
|
cursor: 'auto',
|
||||||
}}
|
}}
|
||||||
color={'disabled'}
|
color={'disabled'}
|
||||||
onClick={async () => {}}
|
|
||||||
>
|
>
|
||||||
<MemoryRounded {...iconStyle} />
|
<MemoryRounded {...iconStyle} />
|
||||||
<Typography {...valStyle}>{inuse}</Typography>
|
<Typography {...valStyle}>{inuse}</Typography>
|
||||||
@@ -139,6 +168,6 @@ export const LayoutTraffic = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</LightweightTrafficErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { Close, CropSquare, FilterNone, Minimize } from '@mui/icons-material'
|
import { Close, CropSquare, FilterNone, Minimize } from '@mui/icons-material'
|
||||||
import { Box, IconButton } from '@mui/material'
|
import { Box, IconButton } from '@mui/material'
|
||||||
import {
|
import { type PointerEvent, useCallback } from 'react'
|
||||||
forwardRef,
|
|
||||||
type PointerEvent,
|
|
||||||
useCallback,
|
|
||||||
useImperativeHandle,
|
|
||||||
} from 'react'
|
|
||||||
|
|
||||||
import { useWindowControls } from '@/hooks/use-window'
|
import { useWindowControls } from '@/hooks/use-window'
|
||||||
import getSystem from '@/utils/get-system'
|
import getSystem from '@/utils/get-system'
|
||||||
@@ -63,36 +58,9 @@ export const WindowResizeHandles = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
export const WindowControls = () => {
|
||||||
const OS = getSystem()
|
const OS = getSystem()
|
||||||
const {
|
const { maximized, minimize, close, toggleMaximize } = useWindowControls()
|
||||||
currentWindow,
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
toggleMaximize,
|
|
||||||
} = useWindowControls()
|
|
||||||
|
|
||||||
useImperativeHandle(
|
|
||||||
ref,
|
|
||||||
() => ({
|
|
||||||
currentWindow,
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
toggleMaximize,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
currentWindow,
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
toggleMaximize,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
||||||
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
|
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
|
||||||
@@ -186,4 +154,4 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
|||||||
}
|
}
|
||||||
}, [handleScroll])
|
}, [handleScroll])
|
||||||
|
|
||||||
const { handleProxyGroupChange } = useProxySelection({
|
const { select } = useProxySelection({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
onProxies()
|
onProxies()
|
||||||
},
|
},
|
||||||
@@ -477,9 +477,9 @@ function NormalProxyGroups(props: { mode: string }) {
|
|||||||
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
|
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
|
||||||
if (!isInteractableMember(member)) return
|
if (!isInteractableMember(member)) return
|
||||||
|
|
||||||
handleProxyGroupChange(group, { name: member.ref.name })
|
select(group.name, member.ref.name, group.now!, group.fixed!)
|
||||||
},
|
},
|
||||||
[handleProxyGroupChange],
|
[select],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleLocation = useStableCallback((group: ProxyGroupView) => {
|
const handleLocation = useStableCallback((group: ProxyGroupView) => {
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ export const useProxyGroupHeaderLayout = () => {
|
|||||||
if (canceled || !operation.source || !operation.target) return
|
if (canceled || !operation.source || !operation.target) return
|
||||||
|
|
||||||
const position = toolsOnLeft ? 'right' : 'left'
|
const position = toolsOnLeft ? 'right' : 'left'
|
||||||
mutateVerge(
|
mutateVerge((prev) =>
|
||||||
(prev) =>
|
prev ? { ...prev, proxy_group_tools_position: position } : prev,
|
||||||
prev ? { ...prev, proxy_group_tools_position: position } : prev,
|
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
await patchVerge({ proxy_group_tools_position: position })
|
await patchVerge({ proxy_group_tools_position: position })
|
||||||
|
|||||||
@@ -113,17 +113,15 @@ export const BackupConfigViewer = memo(
|
|||||||
errorNotice: false,
|
errorNotice: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await mutateVerge(
|
await mutateVerge((current) =>
|
||||||
(current) =>
|
current
|
||||||
current
|
? {
|
||||||
? {
|
...current,
|
||||||
...current,
|
webdav_url: trimmedUrl,
|
||||||
webdav_url: trimmedUrl,
|
webdav_username: trimmedUsername,
|
||||||
webdav_username: trimmedUsername,
|
webdav_password: data.password,
|
||||||
webdav_password: data.password,
|
}
|
||||||
}
|
: current,
|
||||||
: current,
|
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
setWebdavStatus(signature, 'unknown')
|
setWebdavStatus(signature, 'unknown')
|
||||||
showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
|
showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
showNotice.error(err)
|
showNotice.error(err)
|
||||||
}
|
}
|
||||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false)
|
mutateVerge({ ...verge, ...patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -15,44 +15,33 @@ import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
|
|||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
|
|
||||||
|
const buildInitialValues = (verge?: IVergeConfig | null) => ({
|
||||||
|
appLogLevel: verge?.app_log_level ?? 'warn',
|
||||||
|
appLogMaxSize: verge?.app_log_max_size ?? 128,
|
||||||
|
appLogMaxCount: verge?.app_log_max_count ?? 8,
|
||||||
|
autoCloseConnection: verge?.auto_close_connection ?? true,
|
||||||
|
autoCheckUpdate: verge?.auto_check_update ?? true,
|
||||||
|
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
||||||
|
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
||||||
|
enableAutoDelayDetection: verge?.enable_auto_delay_detection ?? false,
|
||||||
|
autoDelayDetectionIntervalMinutes:
|
||||||
|
verge?.auto_delay_detection_interval_minutes ?? 5,
|
||||||
|
defaultLatencyTest: verge?.default_latency_test || '',
|
||||||
|
autoLogClean: verge?.auto_log_clean || 0,
|
||||||
|
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
|
||||||
|
})
|
||||||
|
|
||||||
export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { verge, patchVerge } = useVerge()
|
const { verge, patchVerge } = useVerge()
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [values, setValues] = useState({
|
const [values, setValues] = useState(() => buildInitialValues(null))
|
||||||
appLogLevel: 'warn',
|
|
||||||
appLogMaxSize: 8,
|
|
||||||
appLogMaxCount: 12,
|
|
||||||
autoCloseConnection: true,
|
|
||||||
autoCheckUpdate: true,
|
|
||||||
enableBuiltinEnhanced: true,
|
|
||||||
proxyLayoutColumn: 6,
|
|
||||||
enableAutoDelayDetection: false,
|
|
||||||
autoDelayDetectionIntervalMinutes: 5,
|
|
||||||
defaultLatencyTest: '',
|
|
||||||
autoLogClean: 2,
|
|
||||||
defaultLatencyTimeout: 10000,
|
|
||||||
})
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
open: () => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setValues({
|
setValues(buildInitialValues(verge))
|
||||||
appLogLevel: verge?.app_log_level ?? 'warn',
|
|
||||||
appLogMaxSize: verge?.app_log_max_size ?? 128,
|
|
||||||
appLogMaxCount: verge?.app_log_max_count ?? 8,
|
|
||||||
autoCloseConnection: verge?.auto_close_connection ?? true,
|
|
||||||
autoCheckUpdate: verge?.auto_check_update ?? true,
|
|
||||||
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
|
||||||
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
|
||||||
enableAutoDelayDetection: verge?.enable_auto_delay_detection ?? false,
|
|
||||||
autoDelayDetectionIntervalMinutes:
|
|
||||||
verge?.auto_delay_detection_interval_minutes ?? 5,
|
|
||||||
defaultLatencyTest: verge?.default_latency_test || '',
|
|
||||||
autoLogClean: verge?.auto_log_clean || 0,
|
|
||||||
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ const splitBypass = (value?: string) =>
|
|||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const buildInitialValues = (verge?: IVergeConfig | null) => ({
|
||||||
|
guard: verge?.enable_proxy_guard,
|
||||||
|
enable_bypass_check: verge?.enable_bypass_check ?? true,
|
||||||
|
bypass: verge?.system_proxy_bypass,
|
||||||
|
duration: verge?.proxy_guard_duration ?? 10,
|
||||||
|
use_default: verge?.use_default_bypass ?? true,
|
||||||
|
pac: verge?.proxy_auto_config,
|
||||||
|
pac_content: verge?.pac_file_content ?? DEFAULT_PAC,
|
||||||
|
proxy_host: verge?.proxy_host ?? '127.0.0.1',
|
||||||
|
})
|
||||||
|
|
||||||
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const systemName = getSystem()
|
const systemName = getSystem()
|
||||||
@@ -116,16 +127,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
proxy_host,
|
proxy_host,
|
||||||
} = verge ?? {}
|
} = verge ?? {}
|
||||||
|
|
||||||
const [value, setValue] = useState({
|
const [value, setValue] = useState(() => buildInitialValues(null))
|
||||||
guard: enable_proxy_guard,
|
|
||||||
enable_bypass_check: enable_bypass_check ?? true,
|
|
||||||
bypass: system_proxy_bypass,
|
|
||||||
duration: proxy_guard_duration ?? 10,
|
|
||||||
use_default: use_default_bypass ?? true,
|
|
||||||
pac: proxy_auto_config,
|
|
||||||
pac_content: pac_file_content ?? DEFAULT_PAC,
|
|
||||||
proxy_host: proxy_host ?? '127.0.0.1',
|
|
||||||
})
|
|
||||||
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
|
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
@@ -192,16 +194,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
open: () => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setValue({
|
setValue(buildInitialValues(verge))
|
||||||
guard: enable_proxy_guard,
|
|
||||||
enable_bypass_check: enable_bypass_check ?? true,
|
|
||||||
bypass: system_proxy_bypass,
|
|
||||||
duration: proxy_guard_duration ?? 10,
|
|
||||||
use_default: use_default_bypass ?? true,
|
|
||||||
pac: proxy_auto_config,
|
|
||||||
pac_content: pac_file_content ?? DEFAULT_PAC,
|
|
||||||
proxy_host: proxy_host ?? '127.0.0.1',
|
|
||||||
})
|
|
||||||
fetchNetworkInterfaces()
|
fetchNetworkInterfaces()
|
||||||
},
|
},
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
@@ -339,7 +332,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
Promise.resolve().then(async () => {
|
Promise.resolve().then(async () => {
|
||||||
try {
|
try {
|
||||||
if (Object.keys(patch).length > 0) {
|
if (Object.keys(patch).length > 0) {
|
||||||
mutateVerge({ ...verge, ...patch }, false)
|
mutateVerge({ ...verge, ...patch })
|
||||||
}
|
}
|
||||||
if (Object.keys(patch).length > 0) {
|
if (Object.keys(patch).length > 0) {
|
||||||
await patchVerge(patch)
|
await patchVerge(patch)
|
||||||
|
|||||||
@@ -76,34 +76,28 @@ const proxyOptionToken = ({ memberIndex, member }: TunnelProxyOption) =>
|
|||||||
member.kind === 'node' ? member.node.recordId : member.ref.name
|
member.kind === 'node' ? member.node.recordId : member.ref.name
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
const buildInitialValues = (): TunnelFormValues => ({
|
||||||
|
localAddr: '',
|
||||||
|
localPort: '',
|
||||||
|
targetAddr: '',
|
||||||
|
targetPort: '',
|
||||||
|
network: 'tcp+udp',
|
||||||
|
group: '',
|
||||||
|
proxy: null,
|
||||||
|
})
|
||||||
|
|
||||||
export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
|
export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { clash, mutateClash, patchClash } = useClash()
|
const { clash, mutateClash, patchClash } = useClash()
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [values, setValues] = useState<TunnelFormValues>({
|
const [values, setValues] = useState<TunnelFormValues>(buildInitialValues)
|
||||||
localAddr: '',
|
|
||||||
localPort: '',
|
|
||||||
targetAddr: '',
|
|
||||||
targetPort: '',
|
|
||||||
network: 'tcp+udp',
|
|
||||||
group: '',
|
|
||||||
proxy: null,
|
|
||||||
})
|
|
||||||
const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([])
|
const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([])
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
open: () => {
|
||||||
setValues(() => ({
|
setValues(buildInitialValues())
|
||||||
localAddr: '',
|
|
||||||
localPort: '',
|
|
||||||
targetAddr: '',
|
|
||||||
targetPort: '',
|
|
||||||
network: 'tcp+udp',
|
|
||||||
group: '',
|
|
||||||
proxy: null,
|
|
||||||
}))
|
|
||||||
setDraftTunnels(() => clash?.tunnels ?? [])
|
setDraftTunnels(() => clash?.tunnels ?? [])
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
// 如果没有隧道,则自动展开
|
// 如果没有隧道,则自动展开
|
||||||
|
|||||||
@@ -50,21 +50,21 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
const handleAdd = useLockFn(async (value: string) => {
|
const handleAdd = useLockFn(async (value: string) => {
|
||||||
const newList = [...webUIList, value]
|
const newList = [...webUIList, value]
|
||||||
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false)
|
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
|
||||||
await patchVerge({ web_ui_list: newList })
|
await patchVerge({ web_ui_list: newList })
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleChange = useLockFn(async (index: number, value?: string) => {
|
const handleChange = useLockFn(async (index: number, value?: string) => {
|
||||||
const newList = [...webUIList]
|
const newList = [...webUIList]
|
||||||
newList[index] = value ?? ''
|
newList[index] = value ?? ''
|
||||||
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false)
|
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
|
||||||
await patchVerge({ web_ui_list: newList })
|
await patchVerge({ web_ui_list: newList })
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleDelete = useLockFn(async (index: number) => {
|
const handleDelete = useLockFn(async (index: number) => {
|
||||||
const newList = [...webUIList]
|
const newList = [...webUIList]
|
||||||
newList.splice(index, 1)
|
newList.splice(index, 1)
|
||||||
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false)
|
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
|
||||||
await patchVerge({ web_ui_list: newList })
|
await patchVerge({ web_ui_list: newList })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import React, { useRef } from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { DialogRef, Switch, TooltipIcon } from '@/components/base'
|
import { Switch, TooltipIcon } from '@/components/base'
|
||||||
import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
|
import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
|
|
||||||
import { GuardState } from './mods/guard-state'
|
import { GuardState } from './mods/guard-state'
|
||||||
import { SettingList, SettingItem } from './mods/setting-comp'
|
import { SettingList, SettingItem } from './mods/setting-comp'
|
||||||
import { SysproxyViewer } from './mods/sysproxy-viewer'
|
|
||||||
import { TunViewer } from './mods/tun-viewer'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onError?: (err: Error) => void
|
onError?: (err: Error) => void
|
||||||
@@ -21,22 +19,16 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
|
|
||||||
const { enable_auto_launch, enable_silent_start } = verge ?? {}
|
const { enable_auto_launch, enable_silent_start } = verge ?? {}
|
||||||
|
|
||||||
const sysproxyRef = useRef<DialogRef>(null)
|
|
||||||
const tunRef = useRef<DialogRef>(null)
|
|
||||||
|
|
||||||
const onSwitchFormat = (
|
const onSwitchFormat = (
|
||||||
_e: React.ChangeEvent<HTMLInputElement>,
|
_e: React.ChangeEvent<HTMLInputElement>,
|
||||||
value: boolean,
|
value: boolean,
|
||||||
) => value
|
) => value
|
||||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false)
|
mutateVerge({ ...verge, ...patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingList title={t('settings.sections.system.title')}>
|
<SettingList title={t('settings.sections.system.title')}>
|
||||||
<SysproxyViewer ref={sysproxyRef} />
|
|
||||||
<TunViewer ref={tunRef} />
|
|
||||||
|
|
||||||
<ProxyControlSwitches
|
<ProxyControlSwitches
|
||||||
label={t('settings.sections.system.toggles.tunMode')}
|
label={t('settings.sections.system.toggles.tunMode')}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
@@ -58,8 +50,6 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
}}
|
}}
|
||||||
onGuard={async (e) => {
|
onGuard={async (e) => {
|
||||||
try {
|
try {
|
||||||
// 先触发UI更新立即看到反馈
|
|
||||||
onChangeData({ enable_auto_launch: e })
|
|
||||||
await patchVerge({ enable_auto_launch: e })
|
await patchVerge({ enable_auto_launch: e })
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
|||||||
const backupRef = useRef<DialogRef>(null)
|
const backupRef = useRef<DialogRef>(null)
|
||||||
|
|
||||||
const onChangeData = (patch: any) => {
|
const onChangeData = (patch: any) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false)
|
mutateVerge({ ...verge, ...patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCopyClashEnv = useCallback(async () => {
|
const onCopyClashEnv = useCallback(async () => {
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ const ProxyControlSwitches = ({
|
|||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
mutateVerge({ ...verge, enable_tun_mode: value }, false)
|
mutateVerge({ ...verge, enable_tun_mode: value })
|
||||||
await patchVerge({ enable_tun_mode: value })
|
await patchVerge({ enable_tun_mode: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,8 +229,8 @@ const ProxyControlSwitches = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SysproxyViewer ref={sysproxyRef} />
|
{isSystemProxyMode && <SysproxyViewer ref={sysproxyRef} />}
|
||||||
<TunViewer ref={tunRef} />
|
{isTunMode && <TunViewer ref={tunRef} />}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,301 +0,0 @@
|
|||||||
import {
|
|
||||||
ErrorOutlineRounded,
|
|
||||||
RefreshRounded,
|
|
||||||
BugReportRounded,
|
|
||||||
} from '@mui/icons-material'
|
|
||||||
import { Box, Typography, Button, Alert, Collapse } from '@mui/material'
|
|
||||||
import React, { Component, ErrorInfo, ReactNode } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: ReactNode
|
|
||||||
fallbackComponent?: ReactNode
|
|
||||||
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
hasError: boolean
|
|
||||||
error: Error | null
|
|
||||||
errorInfo: ErrorInfo | null
|
|
||||||
showDetails: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TrafficErrorBoundary extends Component<Props, State> {
|
|
||||||
private retryCount = 0
|
|
||||||
private maxRetries = 3
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
hasError: false,
|
|
||||||
error: null,
|
|
||||||
errorInfo: null,
|
|
||||||
showDetails: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
|
||||||
return { hasError: true, error }
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
||||||
console.error('[TrafficErrorBoundary] 捕获到组件错误:', error, errorInfo)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
error,
|
|
||||||
errorInfo,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.props.onError) {
|
|
||||||
this.props.onError(error, errorInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reportError(error, errorInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
private reportError = (error: Error, errorInfo: ErrorInfo) => {
|
|
||||||
const errorReport = {
|
|
||||||
message: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
componentStack: errorInfo.componentStack,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
url: window.location.href,
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('[TrafficErrorBoundary] 错误报告:', errorReport)
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRetry = () => {
|
|
||||||
if (this.retryCount < this.maxRetries) {
|
|
||||||
this.retryCount++
|
|
||||||
console.log(
|
|
||||||
`[TrafficErrorBoundary] 尝试重试 (${this.retryCount}/${this.maxRetries})`,
|
|
||||||
)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
hasError: false,
|
|
||||||
error: null,
|
|
||||||
errorInfo: null,
|
|
||||||
showDetails: false,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.warn('[TrafficErrorBoundary] 已达到最大重试次数')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRefresh = () => {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleDetails = () => {
|
|
||||||
this.setState((prev) => ({ showDetails: !prev.showDetails }))
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
if (this.props.fallbackComponent) {
|
|
||||||
return this.props.fallbackComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TrafficErrorFallback
|
|
||||||
error={this.state.error}
|
|
||||||
errorInfo={this.state.errorInfo}
|
|
||||||
showDetails={this.state.showDetails}
|
|
||||||
canRetry={this.retryCount < this.maxRetries}
|
|
||||||
retryCount={this.retryCount}
|
|
||||||
maxRetries={this.maxRetries}
|
|
||||||
onRetry={this.handleRetry}
|
|
||||||
onRefresh={this.handleRefresh}
|
|
||||||
onToggleDetails={this.toggleDetails}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TrafficErrorFallbackProps {
|
|
||||||
error: Error | null
|
|
||||||
errorInfo: ErrorInfo | null
|
|
||||||
showDetails: boolean
|
|
||||||
canRetry: boolean
|
|
||||||
retryCount: number
|
|
||||||
maxRetries: number
|
|
||||||
onRetry: () => void
|
|
||||||
onRefresh: () => void
|
|
||||||
onToggleDetails: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
|
||||||
error,
|
|
||||||
errorInfo,
|
|
||||||
showDetails,
|
|
||||||
canRetry,
|
|
||||||
retryCount,
|
|
||||||
maxRetries,
|
|
||||||
onRetry,
|
|
||||||
onRefresh,
|
|
||||||
onToggleDetails,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
minHeight: 200,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
border: '1px dashed',
|
|
||||||
borderColor: 'error.main',
|
|
||||||
borderRadius: 2,
|
|
||||||
bgcolor: 'error.light',
|
|
||||||
color: 'error.contrastText',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: 'error.main' }} />
|
|
||||||
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
{t('shared.feedback.errors.trafficStats')}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ mb: 2, textAlign: 'center' }}
|
|
||||||
>
|
|
||||||
{t('shared.feedback.errors.trafficStatsDescription')}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
|
||||||
<Typography variant="body2">
|
|
||||||
<strong>{t('shared.feedback.errors.label')}:</strong>{' '}
|
|
||||||
{error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: t('shared.feedback.errors.unknown')}
|
|
||||||
</Typography>
|
|
||||||
{retryCount > 0 && (
|
|
||||||
<Typography variant="caption" sx={{ mt: 1, display: 'block' }}>
|
|
||||||
{t('shared.labels.retryAttempts')}: {retryCount}/{maxRetries}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
|
||||||
{canRetry && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<RefreshRounded />}
|
|
||||||
onClick={onRetry}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('shared.actions.retry')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="outlined" onClick={onRefresh} size="small">
|
|
||||||
{t('shared.actions.refreshPage')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
startIcon={<BugReportRounded />}
|
|
||||||
onClick={onToggleDetails}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{showDetails
|
|
||||||
? t('shared.actions.hideDetails')
|
|
||||||
: t('shared.actions.showDetails')}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Collapse in={showDetails} sx={{ width: '100%', maxWidth: 600 }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
borderRadius: 1,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
|
||||||
{t('shared.feedback.errors.details')}:
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
component="pre"
|
|
||||||
sx={{
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
color: 'text.secondary',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{error?.stack}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{errorInfo?.componentStack && (
|
|
||||||
<>
|
|
||||||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
|
|
||||||
{t('shared.feedback.errors.componentStack')}:
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
component="pre"
|
|
||||||
sx={{
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
color: 'text.secondary',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{errorInfo.componentStack}
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Collapse>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LightweightTrafficErrorBoundary: React.FC<{
|
|
||||||
children: ReactNode
|
|
||||||
}> = ({ children }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TrafficErrorBoundary
|
|
||||||
fallbackComponent={
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
minHeight: 60,
|
|
||||||
bgcolor: 'error.light',
|
|
||||||
borderRadius: 1,
|
|
||||||
color: 'error.contrastText',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
|
|
||||||
<Typography variant="caption">
|
|
||||||
{t('shared.feedback.errors.trafficUnavailable')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</TrafficErrorBoundary>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { useMemo } from 'react'
|
|
||||||
|
|
||||||
import { downloadIconCache } from '@/services/cmds'
|
import { downloadIconCache } from '@/services/cmds'
|
||||||
import { useQuery } from '@/services/query-client'
|
import { useQuery } from '@/services/query-client'
|
||||||
@@ -23,9 +22,8 @@ export const useIconCache = ({
|
|||||||
const iconValue = icon?.trim() ?? ''
|
const iconValue = icon?.trim() ?? ''
|
||||||
const cacheKeyValue = cacheKey?.trim() ?? ''
|
const cacheKeyValue = cacheKey?.trim() ?? ''
|
||||||
|
|
||||||
const isEnabled = useMemo(() => {
|
const isEnabled =
|
||||||
return enabled && iconValue.startsWith('http') && cacheKeyValue !== ''
|
enabled && iconValue.startsWith('http') && cacheKeyValue !== ''
|
||||||
}, [enabled, iconValue, cacheKeyValue])
|
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ['icon-cache', iconValue, cacheKeyValue],
|
queryKey: ['icon-cache', iconValue, cacheKeyValue],
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ export const useLogData = () => {
|
|||||||
const hasLoadedInitialLogsRef = useRef(false)
|
const hasLoadedInitialLogsRef = useRef(false)
|
||||||
|
|
||||||
const { response, refresh, setData } = useMihomoWsSubscription<ILogItem[]>({
|
const { response, refresh, setData } = useMihomoWsSubscription<ILogItem[]>({
|
||||||
storageKey: 'mihomo_logs_date',
|
subscriptionKey: enableLog ? 'getClashLog' : null,
|
||||||
buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null),
|
|
||||||
fallbackData: [],
|
fallbackData: [],
|
||||||
connect: () => MihomoWebSocket.connect_logs(logLevel),
|
connect: () => MihomoWebSocket.connect_logs(logLevel),
|
||||||
setupHandlers: ({ next, scheduleReconnect, isMounted }) => {
|
setupHandlers: ({ next, scheduleReconnect, isMounted }) => {
|
||||||
@@ -77,14 +76,13 @@ export const useLogData = () => {
|
|||||||
}
|
}
|
||||||
const pendingLogs = buffer.splice(0, buffer.length)
|
const pendingLogs = buffer.splice(0, buffer.length)
|
||||||
flushTimeStr = null
|
flushTimeStr = null
|
||||||
next(null, (current) => appendLogs(current, pendingLogs))
|
next((current) => appendLogs(current, pendingLogs))
|
||||||
flushTimer = null
|
flushTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleMessage: (data) => {
|
handleMessage: (data) => {
|
||||||
if (data.startsWith('Websocket error')) {
|
if (data.startsWith('Websocket error')) {
|
||||||
next(data)
|
|
||||||
void scheduleReconnect()
|
void scheduleReconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -108,9 +106,7 @@ export const useLogData = () => {
|
|||||||
if (!flushTimer) {
|
if (!flushTimer) {
|
||||||
flushTimer = setTimeout(flush, FLUSH_DELAY_MS)
|
flushTimer = setTimeout(flush, FLUSH_DELAY_MS)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {}
|
||||||
next(error)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async onConnected() {
|
async onConnected() {
|
||||||
if (hasLoadedInitialLogsRef.current) {
|
if (hasLoadedInitialLogsRef.current) {
|
||||||
@@ -119,7 +115,7 @@ export const useLogData = () => {
|
|||||||
const logs = await getClashLogs()
|
const logs = await getClashLogs()
|
||||||
hasLoadedInitialLogsRef.current = true
|
hasLoadedInitialLogsRef.current = true
|
||||||
if (isMounted()) {
|
if (isMounted()) {
|
||||||
next(null, (current) => {
|
next((current) => {
|
||||||
if (!current || current.length === 0) {
|
if (!current || current.length === 0) {
|
||||||
return clampLogs(filterLogsByLevel(logs, allowedTypes))
|
return clampLogs(filterLogsByLevel(logs, allowedTypes))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,15 +33,13 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
|
|||||||
const enabled = options?.enabled ?? true
|
const enabled = options?.enabled ?? true
|
||||||
|
|
||||||
const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({
|
const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({
|
||||||
storageKey: 'mihomo_memory_date',
|
subscriptionKey: enabled ? 'getClashMemory' : null,
|
||||||
buildSubscriptKey: (date) => (enabled ? `getClashMemory-${date}` : null),
|
|
||||||
fallbackData: FALLBACK_MEMORY_USAGE,
|
fallbackData: FALLBACK_MEMORY_USAGE,
|
||||||
connect: () => MihomoWebSocket.connect_memory(),
|
connect: () => MihomoWebSocket.connect_memory(),
|
||||||
throttleMs: 500,
|
throttleMs: 500,
|
||||||
setupHandlers: ({ next, scheduleReconnect }) => ({
|
setupHandlers: ({ next, scheduleReconnect }) => ({
|
||||||
handleMessage: (data) => {
|
handleMessage: (data) => {
|
||||||
if (data.startsWith('Websocket error')) {
|
if (data.startsWith('Websocket error')) {
|
||||||
next(data, FALLBACK_MEMORY_USAGE)
|
|
||||||
void scheduleReconnect()
|
void scheduleReconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -51,10 +49,8 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
|
|||||||
if (shouldSkipDuplicateMemory(parsed)) {
|
if (shouldSkipDuplicateMemory(parsed)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next(null, parsed)
|
next(parsed)
|
||||||
} catch (error) {
|
} catch {}
|
||||||
next(error, FALLBACK_MEMORY_USAGE)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useCallback, useEffect, useRef } from 'react'
|
|
||||||
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import {
|
import { setCacheData, useQuery } from '@/services/query-client'
|
||||||
removeCacheData,
|
|
||||||
setCacheData,
|
|
||||||
useQuery,
|
|
||||||
} from '@/services/query-client'
|
|
||||||
|
|
||||||
const RECONNECT_DELAY_MS = 1000
|
const RECONNECT_DELAY_MS = 1000
|
||||||
|
|
||||||
@@ -31,7 +26,6 @@ interface SharedSubscriptionEntry {
|
|||||||
|
|
||||||
const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>()
|
const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>()
|
||||||
const subscriptionSnapshots = new Map<string, unknown>()
|
const subscriptionSnapshots = new Map<string, unknown>()
|
||||||
const initialSubscriptionDate = Date.now()
|
|
||||||
|
|
||||||
const getSubscriptionSnapshot = <T>(key: string) =>
|
const getSubscriptionSnapshot = <T>(key: string) =>
|
||||||
subscriptionSnapshots.get(key) as T | undefined
|
subscriptionSnapshots.get(key) as T | undefined
|
||||||
@@ -136,15 +130,13 @@ const createSharedSubscriptionEntry = (
|
|||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Updater<T> = (current?: T) => T
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirrors SWR's MutatorCallback: consumers can pass either a plain value or a
|
* Pass either a plain value or a functional updater; the updater is resolved
|
||||||
* functional updater `(current?: T) => T`. The functional form is resolved
|
* against the current subscription snapshot before the cache is written.
|
||||||
* against the current subscription snapshot before updating SWR.
|
|
||||||
*/
|
*/
|
||||||
type NextFn<T> = (
|
type NextFn<T> = (data: T | Updater<T>) => void
|
||||||
error?: any,
|
|
||||||
data?: T | ((current?: T) => T | undefined),
|
|
||||||
) => void
|
|
||||||
|
|
||||||
interface HandlerContext<T> {
|
interface HandlerContext<T> {
|
||||||
next: NextFn<T>
|
next: NextFn<T>
|
||||||
@@ -159,8 +151,7 @@ interface HandlerResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UseMihomoWsSubscriptionOptions<T> {
|
interface UseMihomoWsSubscriptionOptions<T> {
|
||||||
storageKey: string
|
subscriptionKey: string | null
|
||||||
buildSubscriptKey: (date: number) => string | null
|
|
||||||
fallbackData: T
|
fallbackData: T
|
||||||
connect: () => Promise<MihomoWebSocket>
|
connect: () => Promise<MihomoWebSocket>
|
||||||
/**
|
/**
|
||||||
@@ -178,18 +169,12 @@ interface UseMihomoWsSubscriptionOptions<T> {
|
|||||||
export const useMihomoWsSubscription = <T>(
|
export const useMihomoWsSubscription = <T>(
|
||||||
options: UseMihomoWsSubscriptionOptions<T>,
|
options: UseMihomoWsSubscriptionOptions<T>,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const { subscriptionKey, fallbackData, connect, throttleMs, setupHandlers } =
|
||||||
storageKey,
|
options
|
||||||
buildSubscriptKey,
|
|
||||||
fallbackData,
|
|
||||||
connect,
|
|
||||||
throttleMs,
|
|
||||||
setupHandlers,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const [date, setDate] = useLocalStorage(storageKey, initialSubscriptionDate)
|
const subscriptionCacheKey = subscriptionKey
|
||||||
const subscriptKey = buildSubscriptKey(date)
|
? `$sub$${subscriptionKey}`
|
||||||
const subscriptionCacheKey = subscriptKey ? `$sub$${subscriptKey}` : null
|
: null
|
||||||
const lastSubscriptionCacheKeyRef = useRef<string | null>(null)
|
const lastSubscriptionCacheKeyRef = useRef<string | null>(null)
|
||||||
if (subscriptionCacheKey) {
|
if (subscriptionCacheKey) {
|
||||||
lastSubscriptionCacheKeyRef.current = subscriptionCacheKey
|
lastSubscriptionCacheKeyRef.current = subscriptionCacheKey
|
||||||
@@ -197,20 +182,9 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
const responseCacheKey =
|
const responseCacheKey =
|
||||||
subscriptionCacheKey ?? lastSubscriptionCacheKeyRef.current
|
subscriptionCacheKey ?? lastSubscriptionCacheKeyRef.current
|
||||||
|
|
||||||
const resolveNextData = useCallback(
|
// Bumping the epoch re-runs the subscription effect, rebuilding the shared
|
||||||
(
|
// entry (and its captured `connect`/handler closures) under the same key.
|
||||||
data: T | ((current?: T) => T | undefined) | undefined,
|
const [refreshEpoch, setRefreshEpoch] = useState(0)
|
||||||
cacheKey: string,
|
|
||||||
): T => {
|
|
||||||
if (typeof data === 'function') {
|
|
||||||
const updater = data as (current?: T) => T | undefined
|
|
||||||
const current = getSubscriptionSnapshot<T>(cacheKey)
|
|
||||||
return updater(current) ?? fallbackData
|
|
||||||
}
|
|
||||||
return data ?? fallbackData
|
|
||||||
},
|
|
||||||
[fallbackData],
|
|
||||||
)
|
|
||||||
|
|
||||||
const response = useQuery<T>({
|
const response = useQuery<T>({
|
||||||
queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'],
|
queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'],
|
||||||
@@ -238,44 +212,35 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
let throttleCleanup: (() => void) | undefined
|
let throttleCleanup: (() => void) | undefined
|
||||||
let wrappedNext: NextFn<T>
|
let wrappedNext: NextFn<T>
|
||||||
|
|
||||||
const baseNext: NextFn<T> = (error, data) => {
|
const baseNext: NextFn<T> = (data) => {
|
||||||
if (error !== undefined && error !== null) {
|
const resolved =
|
||||||
return
|
typeof data === 'function'
|
||||||
}
|
? (data as Updater<T>)(
|
||||||
if (data === undefined) return
|
getSubscriptionSnapshot<T>(subscriptionCacheKey),
|
||||||
const resolved = resolveNextData(data, subscriptionCacheKey)
|
)
|
||||||
|
: (data as T)
|
||||||
writeSubscriptionSnapshot(subscriptionCacheKey, resolved)
|
writeSubscriptionSnapshot(subscriptionCacheKey, resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (throttleMs && throttleMs > 0) {
|
if (throttleMs && throttleMs > 0) {
|
||||||
let pendingData: T | ((current?: T) => T | undefined) | undefined
|
let pendingData: T | Updater<T> | undefined
|
||||||
let hasPending = false
|
|
||||||
let timerId: ReturnType<typeof setTimeout> | null = null
|
let timerId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
const flush = () => {
|
const flush = () => {
|
||||||
timerId = null
|
timerId = null
|
||||||
if (hasPending) {
|
if (pendingData !== undefined) {
|
||||||
const data = pendingData
|
const data = pendingData
|
||||||
pendingData = undefined
|
pendingData = undefined
|
||||||
hasPending = false
|
baseNext(data)
|
||||||
baseNext(undefined, data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wrappedNext = (
|
wrappedNext = (data) => {
|
||||||
error?: any,
|
|
||||||
data?: T | ((current?: T) => T | undefined),
|
|
||||||
) => {
|
|
||||||
if (error !== undefined && error !== null) {
|
|
||||||
baseNext(error, data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!timerId) {
|
if (!timerId) {
|
||||||
baseNext(undefined, data)
|
baseNext(data)
|
||||||
timerId = setTimeout(flush, throttleMs)
|
timerId = setTimeout(flush, throttleMs)
|
||||||
} else {
|
} else {
|
||||||
pendingData = data
|
pendingData = data
|
||||||
hasPending = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,15 +306,14 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line react-compiler/react-compiler
|
// eslint-disable-next-line react-compiler/react-compiler
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps
|
||||||
}, [subscriptionCacheKey])
|
}, [subscriptionCacheKey, refreshEpoch])
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
if (subscriptionCacheKey) {
|
if (subscriptionCacheKey) {
|
||||||
subscriptionSnapshots.delete(subscriptionCacheKey)
|
writeSubscriptionSnapshot(subscriptionCacheKey, fallbackData)
|
||||||
void removeCacheData([subscriptionCacheKey])
|
|
||||||
}
|
}
|
||||||
setDate(Date.now())
|
setRefreshEpoch((epoch) => epoch + 1)
|
||||||
}, [subscriptionCacheKey, setDate])
|
}, [subscriptionCacheKey, fallbackData])
|
||||||
|
|
||||||
const setData = useCallback(
|
const setData = useCallback(
|
||||||
(data: T) => {
|
(data: T) => {
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ it('applies and records independent group selections while another group is pend
|
|||||||
const secondSuccess = vi.fn()
|
const secondSuccess = vi.fn()
|
||||||
const first = useProxySelection({ onSuccess: firstSuccess })
|
const first = useProxySelection({ onSuccess: firstSuccess })
|
||||||
const second = useProxySelection({ onSuccess: secondSuccess })
|
const second = useProxySelection({ onSuccess: secondSuccess })
|
||||||
first.changeProxy('Group A', 'Node A')
|
first.select('Group A', 'Node A')
|
||||||
second.changeProxy('Group B', 'Node B')
|
second.select('Group B', 'Node B')
|
||||||
await vi.waitFor(() =>
|
await vi.waitFor(() =>
|
||||||
expect(record).toHaveBeenCalledWith('Group B', 'Node B'),
|
expect(record).toHaveBeenCalledWith('Group B', 'Node B'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const cleanupConnections = async (previousProxy: string) => {
|
|||||||
interface ProxySelectionOptions {
|
interface ProxySelectionOptions {
|
||||||
onSuccess?: () => void
|
onSuccess?: () => void
|
||||||
onError?: (error: any) => void
|
onError?: (error: any) => void
|
||||||
enableConnectionCleanup?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProxyChangeRequest {
|
interface ProxyChangeRequest {
|
||||||
@@ -53,7 +52,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
const pendingRequestRef = useRef<ProxyChangeRequest | null>(null)
|
const pendingRequestRef = useRef<ProxyChangeRequest | null>(null)
|
||||||
const isProcessingRef = useRef(false)
|
const isProcessingRef = useRef(false)
|
||||||
|
|
||||||
const { onSuccess, onError, enableConnectionCleanup = true } = options
|
const { onSuccess, onError } = options
|
||||||
|
|
||||||
const autoCloseConnection = verge?.auto_close_connection ?? false
|
const autoCloseConnection = verge?.auto_close_connection ?? false
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableConnectionCleanup && autoCloseConnection && previousProxy) {
|
if (autoCloseConnection && previousProxy) {
|
||||||
void cleanupConnections(previousProxy)
|
void cleanupConnections(previousProxy)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -112,7 +111,6 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
[
|
[
|
||||||
autoCloseConnection,
|
autoCloseConnection,
|
||||||
forgetSelection,
|
forgetSelection,
|
||||||
enableConnectionCleanup,
|
|
||||||
onError,
|
onError,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
recordSelection,
|
recordSelection,
|
||||||
@@ -138,7 +136,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
}
|
}
|
||||||
}, [executeChange])
|
}, [executeChange])
|
||||||
|
|
||||||
const changeProxy = useCallback(
|
const select = useCallback(
|
||||||
(
|
(
|
||||||
groupName: string,
|
groupName: string,
|
||||||
proxyName: string,
|
proxyName: string,
|
||||||
@@ -156,27 +154,5 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
[flushChangeQueue],
|
[flushChangeQueue],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSelectChange = useCallback(
|
return { select }
|
||||||
(groupName: string, previousProxy?: string, fixed?: string) =>
|
|
||||||
(event: { target: { value: string } }) => {
|
|
||||||
changeProxy(groupName, event.target.value, previousProxy, fixed)
|
|
||||||
},
|
|
||||||
[changeProxy],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleProxyGroupChange = useCallback(
|
|
||||||
(
|
|
||||||
group: { name: string; now?: string | null; fixed?: string | null },
|
|
||||||
proxy: { name: string },
|
|
||||||
) => {
|
|
||||||
changeProxy(group.name, proxy.name, group.now!, group.fixed!)
|
|
||||||
},
|
|
||||||
[changeProxy],
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
changeProxy,
|
|
||||||
handleSelectChange,
|
|
||||||
handleProxyGroupChange,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,8 @@ export const useSystemProxyState = () => {
|
|||||||
const toggleSystemProxy = async (enabled: boolean) => {
|
const toggleSystemProxy = async (enabled: boolean) => {
|
||||||
// Roll failed optimistic writes back to the latest confirmed state.
|
// Roll failed optimistic writes back to the latest confirmed state.
|
||||||
let confirmed = verge?.enable_system_proxy ?? false
|
let confirmed = verge?.enable_system_proxy ?? false
|
||||||
mutateVerge(
|
mutateVerge((prev) =>
|
||||||
(prev) => (prev ? { ...prev, enable_system_proxy: enabled } : prev),
|
prev ? { ...prev, enable_system_proxy: enabled } : prev,
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
pendingRef.current = enabled
|
pendingRef.current = enabled
|
||||||
|
|
||||||
@@ -78,9 +77,8 @@ export const useSystemProxyState = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
mutateVerge(
|
mutateVerge((prev) =>
|
||||||
(prev) => (prev ? { ...prev, enable_system_proxy: confirmed } : prev),
|
prev ? { ...prev, enable_system_proxy: confirmed } : prev,
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
// Queued requests were based on a state that never landed.
|
// Queued requests were based on a state that never landed.
|
||||||
pendingRef.current = null
|
pendingRef.current = null
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export function useAppUptime() {
|
|||||||
const { data: uptime = 0 } = useQuery({
|
const { data: uptime = 0 } = useQuery({
|
||||||
queryKey: appUptimeQueryKey,
|
queryKey: appUptimeQueryKey,
|
||||||
queryFn: getAppUptime,
|
queryFn: getAppUptime,
|
||||||
staleTime: 5000,
|
// The wrapper maps staleTime to SWR's dedupingInterval, which drops
|
||||||
|
// refetchInterval ticks while it exceeds the interval.
|
||||||
refetchInterval: 3000,
|
refetchInterval: 3000,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
|
|||||||
@@ -32,15 +32,13 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
|
|||||||
graphData: { appendData },
|
graphData: { appendData },
|
||||||
} = useTrafficMonitorEnhanced({ subscribe: false, enabled })
|
} = useTrafficMonitorEnhanced({ subscribe: false, enabled })
|
||||||
const { response, refresh } = useMihomoWsSubscription<ITrafficItem>({
|
const { response, refresh } = useMihomoWsSubscription<ITrafficItem>({
|
||||||
storageKey: 'mihomo_traffic_date',
|
subscriptionKey: enabled ? 'getClashTraffic' : null,
|
||||||
buildSubscriptKey: (date) => (enabled ? `getClashTraffic-${date}` : null),
|
|
||||||
fallbackData: FALLBACK_TRAFFIC,
|
fallbackData: FALLBACK_TRAFFIC,
|
||||||
connect: () => MihomoWebSocket.connect_traffic(),
|
connect: () => MihomoWebSocket.connect_traffic(),
|
||||||
throttleMs: 200,
|
throttleMs: 200,
|
||||||
setupHandlers: ({ next, scheduleReconnect }) => ({
|
setupHandlers: ({ next, scheduleReconnect }) => ({
|
||||||
handleMessage: (data) => {
|
handleMessage: (data) => {
|
||||||
if (data.startsWith('Websocket error')) {
|
if (data.startsWith('Websocket error')) {
|
||||||
next(data, FALLBACK_TRAFFIC)
|
|
||||||
void scheduleReconnect()
|
void scheduleReconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -51,10 +49,8 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
appendData(parsed)
|
appendData(parsed)
|
||||||
next(null, parsed)
|
next(parsed)
|
||||||
} catch (error) {
|
} catch {}
|
||||||
next(error, FALLBACK_TRAFFIC)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Traffic } from 'tauri-plugin-mihomo-api'
|
|||||||
|
|
||||||
import { useVisibility } from '@/hooks/use-visibility'
|
import { useVisibility } from '@/hooks/use-visibility'
|
||||||
import { debugLog } from '@/utils/debug'
|
import { debugLog } from '@/utils/debug'
|
||||||
import { TrafficDataSampler, isSameTrafficData } from '@/utils/traffic-sampler'
|
|
||||||
|
|
||||||
// 引用计数管理器
|
// 引用计数管理器
|
||||||
class ReferenceCounter {
|
class ReferenceCounter {
|
||||||
@@ -32,119 +31,11 @@ const WORKER_CONFIG = {
|
|||||||
defaultRangeMinutes: 10,
|
defaultRangeMinutes: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
class InlineTrafficMonitor {
|
|
||||||
private config = { ...WORKER_CONFIG }
|
|
||||||
private sampler = new TrafficDataSampler(this.config)
|
|
||||||
private throttleTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
private agingTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
private currentRange = this.config.defaultRangeMinutes
|
|
||||||
private lastTimestamp: number | undefined
|
|
||||||
private lastBroadcast: ITrafficDataPoint[] = []
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private emit: (snapshot: ITrafficWorkerSnapshotMessage) => void,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
start(rangeMinutes?: number) {
|
|
||||||
this.currentRange = rangeMinutes ?? this.currentRange
|
|
||||||
this.handle({
|
|
||||||
type: 'init',
|
|
||||||
config: {
|
|
||||||
...this.config,
|
|
||||||
defaultRangeMinutes: this.currentRange,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
if (this.throttleTimer !== null) {
|
|
||||||
clearTimeout(this.throttleTimer)
|
|
||||||
this.throttleTimer = null
|
|
||||||
}
|
|
||||||
if (this.agingTimer !== null) {
|
|
||||||
clearInterval(this.agingTimer)
|
|
||||||
this.agingTimer = null
|
|
||||||
}
|
|
||||||
this.sampler.clear()
|
|
||||||
this.lastTimestamp = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
handle(message: TrafficWorkerRequestMessage) {
|
|
||||||
switch (message.type) {
|
|
||||||
case 'init': {
|
|
||||||
this.config = { ...message.config }
|
|
||||||
this.sampler = new TrafficDataSampler(this.config)
|
|
||||||
this.currentRange = message.config.defaultRangeMinutes
|
|
||||||
// Re-broadcast when points age out of the range, not on every tick
|
|
||||||
this.agingTimer = setInterval(() => {
|
|
||||||
if (this.throttleTimer !== null) return
|
|
||||||
const slice = this.sampler.getDataForTimeRange(this.currentRange)
|
|
||||||
if (!isSameTrafficData(this.lastBroadcast, slice)) {
|
|
||||||
this.emitSnapshot()
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
this.emitSnapshot()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'append': {
|
|
||||||
const timestamp = message.payload.timestamp ?? Date.now()
|
|
||||||
const dataPoint: ITrafficDataPoint = {
|
|
||||||
up: message.payload.up || 0,
|
|
||||||
down: message.payload.down || 0,
|
|
||||||
timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastTimestamp = timestamp
|
|
||||||
this.sampler.addDataPoint(dataPoint)
|
|
||||||
this.scheduleSnapshot()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'setRange': {
|
|
||||||
if (this.currentRange !== message.minutes) {
|
|
||||||
this.currentRange = message.minutes
|
|
||||||
this.emitSnapshot()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'requestSnapshot': {
|
|
||||||
this.emitSnapshot()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitSnapshot() {
|
|
||||||
const dataPoints = this.sampler.getDataForTimeRange(this.currentRange)
|
|
||||||
|
|
||||||
this.emit({
|
|
||||||
type: 'snapshot',
|
|
||||||
dataPoints,
|
|
||||||
samplerStats: this.sampler.getStats(),
|
|
||||||
lastTimestamp: this.lastTimestamp,
|
|
||||||
})
|
|
||||||
this.lastBroadcast = dataPoints
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleSnapshot() {
|
|
||||||
if (this.throttleTimer !== null) return
|
|
||||||
this.throttleTimer = setTimeout(() => {
|
|
||||||
this.throttleTimer = null
|
|
||||||
this.emitSnapshot()
|
|
||||||
}, this.config.snapshotIntervalMs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TrafficWorkerClient {
|
class TrafficWorkerClient {
|
||||||
private worker: Worker | null = null
|
private worker: Worker | null = null
|
||||||
private inlineMonitor: InlineTrafficMonitor | null = null
|
|
||||||
private mode: 'worker' | 'inline' | null = null
|
|
||||||
private listeners = new Set<
|
private listeners = new Set<
|
||||||
(snapshot: ITrafficWorkerSnapshotMessage) => void
|
(snapshot: ITrafficWorkerSnapshotMessage) => void
|
||||||
>()
|
>()
|
||||||
private pendingMessages: TrafficWorkerRequestMessage[] = []
|
|
||||||
private ready = false
|
|
||||||
private currentRange = WORKER_CONFIG.defaultRangeMinutes
|
private currentRange = WORKER_CONFIG.defaultRangeMinutes
|
||||||
|
|
||||||
start(rangeMinutes?: number) {
|
start(rangeMinutes?: number) {
|
||||||
@@ -155,101 +46,52 @@ class TrafficWorkerClient {
|
|||||||
|
|
||||||
this.currentRange = rangeMinutes ?? this.currentRange
|
this.currentRange = rangeMinutes ?? this.currentRange
|
||||||
|
|
||||||
if (this.ready) return
|
if (this.worker) return
|
||||||
|
|
||||||
const initMessage: TrafficWorkerRequestMessage = {
|
const initMessage: TrafficWorkerRequestMessage = {
|
||||||
type: 'init',
|
type: 'init',
|
||||||
config: {
|
config: { ...WORKER_CONFIG, defaultRangeMinutes: this.currentRange },
|
||||||
rawDataMinutes: WORKER_CONFIG.rawDataMinutes,
|
|
||||||
compressedDataMinutes: WORKER_CONFIG.compressedDataMinutes,
|
|
||||||
compressionRatio: WORKER_CONFIG.compressionRatio,
|
|
||||||
snapshotIntervalMs: WORKER_CONFIG.snapshotIntervalMs,
|
|
||||||
defaultRangeMinutes: this.currentRange,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof Worker !== 'undefined') {
|
this.worker = new Worker(
|
||||||
try {
|
new URL('../services/traffic-monitor-worker.ts', import.meta.url),
|
||||||
this.worker = new Worker(
|
{ type: 'module' },
|
||||||
new URL('../services/traffic-monitor-worker.ts', import.meta.url),
|
)
|
||||||
{ type: 'module' },
|
|
||||||
)
|
|
||||||
this.mode = 'worker'
|
|
||||||
|
|
||||||
this.worker.onmessage = (
|
this.worker.onmessage = (
|
||||||
event: MessageEvent<TrafficWorkerResponseMessage>,
|
event: MessageEvent<TrafficWorkerResponseMessage>,
|
||||||
) => {
|
) => {
|
||||||
const message = event.data
|
const message = event.data
|
||||||
if (message.type === 'snapshot') {
|
if (message.type === 'snapshot') {
|
||||||
if (import.meta.env.MODE === 'perf') {
|
if (import.meta.env.MODE === 'perf') {
|
||||||
document.documentElement.dataset.perfWorker = String(
|
document.documentElement.dataset.perfWorker = String(
|
||||||
message.lastTimestamp ?? 0,
|
message.lastTimestamp ?? 0,
|
||||||
)
|
)
|
||||||
document.documentElement.dataset.perfPoints = String(
|
document.documentElement.dataset.perfPoints = String(
|
||||||
message.dataPoints.length,
|
message.dataPoints.length,
|
||||||
)
|
)
|
||||||
const last = message.dataPoints.at(-1)
|
const last = message.dataPoints.at(-1)
|
||||||
document.documentElement.dataset.perfLastUp = String(
|
document.documentElement.dataset.perfLastUp = String(last?.up ?? 0)
|
||||||
last?.up ?? 0,
|
document.documentElement.dataset.perfLastDown = String(
|
||||||
)
|
last?.down ?? 0,
|
||||||
document.documentElement.dataset.perfLastDown = String(
|
)
|
||||||
last?.down ?? 0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
this.listeners.forEach((listener) => {
|
|
||||||
listener(message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.listeners.forEach((listener) => {
|
||||||
this.worker.onerror = (error) => {
|
listener(message)
|
||||||
debugLog(`[TrafficWorkerClient] Worker error: ${String(error)}`)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
this.ready = true
|
|
||||||
this.post(initMessage)
|
|
||||||
this.flushQueue()
|
|
||||||
return
|
|
||||||
} catch (error) {
|
|
||||||
debugLog(
|
|
||||||
`[TrafficWorkerClient] Worker initialization failed, falling back to inline sampler: ${String(error)}`,
|
|
||||||
)
|
|
||||||
this.worker = null
|
|
||||||
this.mode = null
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
debugLog(
|
|
||||||
'[TrafficWorkerClient] Worker not supported, using inline sampler',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startInline(initMessage)
|
this.worker.onerror = (error) => {
|
||||||
}
|
debugLog(`[TrafficWorkerClient] Worker error: ${String(error)}`)
|
||||||
|
}
|
||||||
|
|
||||||
private startInline(initMessage: TrafficWorkerRequestMessage) {
|
|
||||||
this.inlineMonitor = new InlineTrafficMonitor((snapshot) => {
|
|
||||||
this.listeners.forEach((listener) => {
|
|
||||||
listener(snapshot)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.mode = 'inline'
|
|
||||||
this.ready = true
|
|
||||||
this.post(initMessage)
|
this.post(initMessage)
|
||||||
this.flushQueue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this.worker) {
|
this.worker?.terminate()
|
||||||
this.worker.terminate()
|
|
||||||
}
|
|
||||||
if (this.inlineMonitor) {
|
|
||||||
this.inlineMonitor.stop()
|
|
||||||
}
|
|
||||||
this.worker = null
|
this.worker = null
|
||||||
this.inlineMonitor = null
|
|
||||||
this.mode = null
|
|
||||||
this.ready = false
|
|
||||||
this.pendingMessages = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSnapshot(listener: (snapshot: ITrafficWorkerSnapshotMessage) => void) {
|
onSnapshot(listener: (snapshot: ITrafficWorkerSnapshotMessage) => void) {
|
||||||
@@ -260,37 +102,11 @@ class TrafficWorkerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private post(message: TrafficWorkerRequestMessage) {
|
private post(message: TrafficWorkerRequestMessage) {
|
||||||
if (!this.ready) {
|
this.worker?.postMessage(message)
|
||||||
this.pendingMessages.push(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mode === 'worker' && this.worker) {
|
|
||||||
this.worker.postMessage(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mode === 'inline' && this.inlineMonitor) {
|
|
||||||
this.inlineMonitor.handle(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pendingMessages.push(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
private flushQueue() {
|
|
||||||
if (!this.ready || this.pendingMessages.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const queued = [...this.pendingMessages]
|
|
||||||
this.pendingMessages = []
|
|
||||||
queued.forEach((message) => {
|
|
||||||
this.post(message)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureStarted() {
|
private ensureStarted() {
|
||||||
if (!this.ready) {
|
if (!this.worker) {
|
||||||
this.start(this.currentRange)
|
this.start(this.currentRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export const useVerge = () => {
|
|||||||
| IVergeConfig
|
| IVergeConfig
|
||||||
| ((prev: IVergeConfig | undefined) => IVergeConfig | undefined)
|
| ((prev: IVergeConfig | undefined) => IVergeConfig | undefined)
|
||||||
| undefined,
|
| undefined,
|
||||||
_revalidate?: boolean,
|
|
||||||
) => {
|
) => {
|
||||||
if (updaterOrData === undefined) {
|
if (updaterOrData === undefined) {
|
||||||
void refetch()
|
void refetch()
|
||||||
|
|||||||
+6
-18
@@ -11,36 +11,24 @@ const useWindow = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useWindowControls = () => {
|
export const useWindowControls = () => {
|
||||||
const {
|
const { maximized, minimize, toggleMaximize, close, currentWindow } =
|
||||||
maximized,
|
useWindow()
|
||||||
minimize,
|
|
||||||
toggleMaximize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
|
||||||
} = useWindow()
|
|
||||||
return {
|
return {
|
||||||
maximized,
|
maximized,
|
||||||
minimize,
|
minimize,
|
||||||
toggleMaximize,
|
toggleMaximize,
|
||||||
close,
|
close,
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
currentWindow,
|
||||||
} satisfies Pick<
|
} satisfies Pick<
|
||||||
WindowContextType,
|
WindowContextType,
|
||||||
| 'maximized'
|
'maximized' | 'minimize' | 'toggleMaximize' | 'close' | 'currentWindow'
|
||||||
| 'minimize'
|
|
||||||
| 'toggleMaximize'
|
|
||||||
| 'close'
|
|
||||||
| 'toggleFullscreen'
|
|
||||||
| 'currentWindow'
|
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWindowDecorations = () => {
|
export const useWindowDecorations = () => {
|
||||||
const { decorated, toggleDecorations, refreshDecorated } = useWindow()
|
const { decorated, toggleDecorations } = useWindow()
|
||||||
return { decorated, toggleDecorations, refreshDecorated } satisfies Pick<
|
return { decorated, toggleDecorations } satisfies Pick<
|
||||||
WindowContextType,
|
WindowContextType,
|
||||||
'decorated' | 'toggleDecorations' | 'refreshDecorated'
|
'decorated' | 'toggleDecorations'
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-21
@@ -1,8 +1,7 @@
|
|||||||
import { Paper, ThemeProvider } from '@mui/material'
|
import { Paper, ThemeProvider } from '@mui/material'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
import { useCallback, useEffect, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { Outlet, useNavigate } from 'react-router'
|
import { Outlet, useNavigate } from 'react-router'
|
||||||
|
|
||||||
import { BaseErrorBoundary } from '@/components/base'
|
import { BaseErrorBoundary } from '@/components/base'
|
||||||
@@ -38,7 +37,6 @@ const OS = getSystem()
|
|||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const mode = useThemeMode()
|
const mode = useThemeMode()
|
||||||
const isDark = mode !== 'light'
|
const isDark = mode !== 'light'
|
||||||
const { t } = useTranslation()
|
|
||||||
const { theme } = useCustomTheme()
|
const { theme } = useCustomTheme()
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
const { language } = verge ?? {}
|
const { language } = verge ?? {}
|
||||||
@@ -46,7 +44,6 @@ const Layout = () => {
|
|||||||
const { switchLanguage } = useI18n()
|
const { switchLanguage } = useI18n()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const themeReady = useMemo(() => Boolean(theme), [theme])
|
const themeReady = useMemo(() => Boolean(theme), [theme])
|
||||||
const windowControlsRef = useRef<any>(null)
|
|
||||||
const { decorated } = useWindowDecorations()
|
const { decorated } = useWindowDecorations()
|
||||||
|
|
||||||
const customTitlebar = useMemo(
|
const customTitlebar = useMemo(
|
||||||
@@ -57,7 +54,7 @@ const Layout = () => {
|
|||||||
className="the_titlebar-drag-region"
|
className="the_titlebar-drag-region"
|
||||||
data-tauri-drag-region="true"
|
data-tauri-drag-region="true"
|
||||||
/>
|
/>
|
||||||
<WindowControls ref={windowControlsRef} />
|
<WindowControls />
|
||||||
</div>
|
</div>
|
||||||
) : null,
|
) : null,
|
||||||
[decorated],
|
[decorated],
|
||||||
@@ -69,12 +66,12 @@ const Layout = () => {
|
|||||||
(payload: [string, string]) => {
|
(payload: [string, string]) => {
|
||||||
const [status, msg] = payload
|
const [status, msg] = payload
|
||||||
try {
|
try {
|
||||||
handleNoticeMessage(status, msg, t, navigate)
|
handleNoticeMessage(status, msg, navigate)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[通知处理] 失败:', error)
|
console.error('[通知处理] 失败:', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[t, navigate],
|
[navigate],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEventBus(handleNotice)
|
useEventBus(handleNotice)
|
||||||
@@ -110,20 +107,6 @@ const Layout = () => {
|
|||||||
<NoticeManager position={verge?.notice_position} />
|
<NoticeManager position={verge?.notice_position} />
|
||||||
<ServiceMigrationDialog />
|
<ServiceMigrationDialog />
|
||||||
<SysproxyPrivilegeDialog />
|
<SysproxyPrivilegeDialog />
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
animation: 'fadeIn 0.5s',
|
|
||||||
WebkitAnimation: 'fadeIn 0.5s',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
{`
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
<Paper
|
<Paper
|
||||||
square
|
square
|
||||||
elevation={0}
|
elevation={0}
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ const ProfilePage = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState('')
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
const [profileDndRevision, setProfileDndRevision] = useState(0)
|
const [profileDndRevision, setProfileDndRevision] = useState(0)
|
||||||
const [activatings, setActivatings] = useState<string[]>([])
|
const [activatings, setActivatings] = useState<string[]>([])
|
||||||
const [switchTarget, setSwitchTarget] = useState<string | null>(null)
|
const [switchTarget, setSwitchTarget] = useState<string | null>(null)
|
||||||
@@ -257,7 +256,6 @@ const ProfilePage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setDisabled(false)
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -824,7 +822,7 @@ const ProfilePage = () => {
|
|||||||
if (event.key !== 'Enter' || event.nativeEvent.isComposing) {
|
if (event.key !== 'Enter' || event.nativeEvent.isComposing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!url || disabled || loading) {
|
if (!url || loading) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -857,7 +855,7 @@ const ProfilePage = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={!url || disabled}
|
disabled={!url}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
+1
-4
@@ -24,11 +24,8 @@ const RulesPage = () => {
|
|||||||
const [showScrollTop, setShowScrollTop] = useState(false)
|
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||||
const pageVisible = useVisibility()
|
const pageVisible = useVisibility()
|
||||||
|
|
||||||
// 在组件挂载时和页面获得焦点时刷新规则数据
|
// 页面可见时刷新规则数据:挂载时可见或从隐藏恢复可见各触发一次
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshRules()
|
|
||||||
refreshRuleProviders()
|
|
||||||
|
|
||||||
if (pageVisible) {
|
if (pageVisible) {
|
||||||
refreshRules()
|
refreshRules()
|
||||||
refreshRuleProviders()
|
refreshRuleProviders()
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ const TQ_MIHOMO = {
|
|||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
staleTime: 1500,
|
staleTime: 1500,
|
||||||
retry: 3,
|
retry: 3,
|
||||||
retryDelay: (attempt: number) => Math.min(200 * 2 ** attempt, 3000),
|
// SWR's default retry waits about 2^retryCount * errorRetryInterval with
|
||||||
|
// jitter, so 100 reproduces the previous 200 * 2^attempt backoff.
|
||||||
|
retryDelay: 100,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const TQ_DEFAULTS = {
|
const TQ_DEFAULTS = {
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ export interface WindowContextType {
|
|||||||
decorated: boolean | null
|
decorated: boolean | null
|
||||||
maximized: boolean | null
|
maximized: boolean | null
|
||||||
toggleDecorations: () => Promise<void>
|
toggleDecorations: () => Promise<void>
|
||||||
refreshDecorated: () => Promise<boolean>
|
|
||||||
minimize: () => Promise<void>
|
minimize: () => Promise<void>
|
||||||
close: () => Promise<void>
|
close: () => Promise<void>
|
||||||
toggleMaximize: () => Promise<void>
|
toggleMaximize: () => Promise<void>
|
||||||
toggleFullscreen: () => Promise<void>
|
|
||||||
currentWindow: ReturnType<typeof getCurrentWindow>
|
currentWindow: ReturnType<typeof getCurrentWindow>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,10 +61,6 @@ export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
}
|
}
|
||||||
}, [currentWindow])
|
}, [currentWindow])
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(async () => {
|
|
||||||
await currentWindow.setFullscreen(!(await currentWindow.isFullscreen()))
|
|
||||||
}, [currentWindow])
|
|
||||||
|
|
||||||
const refreshDecorated = useCallback(async () => {
|
const refreshDecorated = useCallback(async () => {
|
||||||
const val = await currentWindow.isDecorated()
|
const val = await currentWindow.isDecorated()
|
||||||
setDecorated(val)
|
setDecorated(val)
|
||||||
@@ -87,22 +83,18 @@ export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
decorated,
|
decorated,
|
||||||
maximized,
|
maximized,
|
||||||
toggleDecorations,
|
toggleDecorations,
|
||||||
refreshDecorated,
|
|
||||||
minimize,
|
minimize,
|
||||||
close,
|
close,
|
||||||
toggleMaximize,
|
toggleMaximize,
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
currentWindow,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
decorated,
|
decorated,
|
||||||
maximized,
|
maximized,
|
||||||
toggleDecorations,
|
toggleDecorations,
|
||||||
refreshDecorated,
|
|
||||||
minimize,
|
minimize,
|
||||||
close,
|
close,
|
||||||
toggleMaximize,
|
toggleMaximize,
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
currentWindow,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
+38
-37
@@ -35,7 +35,6 @@ interface IpInfo {
|
|||||||
interface ServiceConfig {
|
interface ServiceConfig {
|
||||||
url: string
|
url: string
|
||||||
mapping: (data: any) => IpInfo
|
mapping: (data: any) => IpInfo
|
||||||
timeout?: number // 保留timeout字段(如有需要)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可用的IP检测服务列表及字段映射
|
// 可用的IP检测服务列表及字段映射
|
||||||
@@ -156,48 +155,52 @@ export const getIpInfo = async (): Promise<
|
|||||||
for (const service of shuffledServices) {
|
for (const service of shuffledServices) {
|
||||||
debugLog(`尝试IP检测服务: ${service.url}`)
|
debugLog(`尝试IP检测服务: ${service.url}`)
|
||||||
|
|
||||||
const timeoutController = new AbortController()
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
timeoutController.abort()
|
|
||||||
}, service.timeout || serviceTimeout)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await asyncRetry(
|
return await asyncRetry(
|
||||||
async (bail) => {
|
async (bail) => {
|
||||||
console.debug('Fetching IP information:', service.url)
|
console.debug('Fetching IP information:', service.url)
|
||||||
|
|
||||||
const response = await fetch(service.url, {
|
const timeoutController = new AbortController()
|
||||||
method: 'GET',
|
const timeoutId = setTimeout(() => {
|
||||||
signal: timeoutController.signal, // AbortSignal.timeout(service.timeout || serviceTimeout),
|
timeoutController.abort()
|
||||||
connectTimeout: service.timeout || serviceTimeout,
|
}, serviceTimeout)
|
||||||
headers: {
|
|
||||||
'User-Agent': userAgent,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return bail(
|
|
||||||
new Error(
|
|
||||||
`IP 检测服务出错,状态码: ${response.status} from ${service.url}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: any
|
|
||||||
try {
|
try {
|
||||||
data = await response.json()
|
const response = await fetch(service.url, {
|
||||||
} catch {
|
method: 'GET',
|
||||||
return bail(new Error(`无法解析 JSON 响应 from ${service.url}`))
|
signal: timeoutController.signal,
|
||||||
}
|
connectTimeout: serviceTimeout,
|
||||||
|
headers: {
|
||||||
if (data && data.ip) {
|
'User-Agent': userAgent,
|
||||||
debugLog(`IP检测成功,使用服务: ${service.url}`)
|
},
|
||||||
return Object.assign(service.mapping(data), {
|
|
||||||
// use last fetch success timestamp
|
|
||||||
lastFetchTs: Date.now(),
|
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
return bail(new Error(`无效的响应格式 from ${service.url}`))
|
if (!response.ok) {
|
||||||
|
return bail(
|
||||||
|
new Error(
|
||||||
|
`IP 检测服务出错,状态码: ${response.status} from ${service.url}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: any
|
||||||
|
try {
|
||||||
|
data = await response.json()
|
||||||
|
} catch {
|
||||||
|
return bail(new Error(`无法解析 JSON 响应 from ${service.url}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.ip) {
|
||||||
|
debugLog(`IP检测成功,使用服务: ${service.url}`)
|
||||||
|
return Object.assign(service.mapping(data), {
|
||||||
|
// use last fetch success timestamp
|
||||||
|
lastFetchTs: Date.now(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return bail(new Error(`无效的响应格式 from ${service.url}`))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -210,8 +213,6 @@ export const getIpInfo = async (): Promise<
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLog(`IP检测服务失败: ${service.url}`, error)
|
debugLog(`IP检测服务失败: ${service.url}`, error)
|
||||||
lastError = error
|
lastError = error
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const mountBus = async () => {
|
|||||||
return createEventBus({
|
return createEventBus({
|
||||||
dispatch,
|
dispatch,
|
||||||
handleNotice: ([status, message]: [string, string]) => {
|
handleNotice: ([status, message]: [string, string]) => {
|
||||||
notice(status, message, (key) => key, vi.fn())
|
notice(status, message, vi.fn())
|
||||||
},
|
},
|
||||||
revalidateKeys: (keys) => void revalidateQueries(keys.map((key) => [key])),
|
revalidateKeys: (keys) => void revalidateQueries(keys.map((key) => [key])),
|
||||||
readPendingFailures: () => {},
|
readPendingFailures: () => {},
|
||||||
@@ -72,7 +72,7 @@ it('does not replay a recovered startup error after WebView recreation', async (
|
|||||||
kind: 'startFailed',
|
kind: 'startFailed',
|
||||||
detail: 'startup failed',
|
detail: 'startup failed',
|
||||||
})
|
})
|
||||||
first.handleNoticeMessage('core_start::error', '', (key) => key, vi.fn())
|
first.handleNoticeMessage('core_start::error', '', vi.fn())
|
||||||
await flush()
|
await flush()
|
||||||
expect(showNotice.error).toHaveBeenCalledOnce()
|
expect(showNotice.error).toHaveBeenCalledOnce()
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ it('does not replay a recovered startup error after WebView recreation', async (
|
|||||||
vi.mocked(getCoreStartupError).mockResolvedValue(null)
|
vi.mocked(getCoreStartupError).mockResolvedValue(null)
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
const recreated = await import('@/services/notice-handlers')
|
const recreated = await import('@/services/notice-handlers')
|
||||||
recreated.handleNoticeMessage('core_start::error', '', (key) => key, vi.fn())
|
recreated.handleNoticeMessage('core_start::error', '', vi.fn())
|
||||||
await flush()
|
await flush()
|
||||||
expect(showNotice.error).toHaveBeenCalledOnce()
|
expect(showNotice.error).toHaveBeenCalledOnce()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ class DelayManager {
|
|||||||
group: string,
|
group: string,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
): Promise<DelayUpdate> {
|
): Promise<DelayUpdate> {
|
||||||
|
// Entry points own the pending mark; measureDelay only writes results.
|
||||||
|
this.setDelay(member.ref.name, group, -2)
|
||||||
const update = await this.measureDelay(member, group, timeout)
|
const update = await this.measureDelay(member, group, timeout)
|
||||||
this.queueGroupNotification(group)
|
this.queueGroupNotification(group)
|
||||||
return update
|
return update
|
||||||
@@ -298,8 +300,6 @@ class DelayManager {
|
|||||||
`[DelayManager] 开始测试延迟,代理: ${name}, 组: ${group}, 超时: ${timeout}ms`,
|
`[DelayManager] 开始测试延迟,代理: ${name}, 组: ${group}, 超时: ${timeout}ms`,
|
||||||
)
|
)
|
||||||
|
|
||||||
this.setDelay(name, group, -2)
|
|
||||||
|
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -359,8 +359,6 @@ class DelayManager {
|
|||||||
const currName = currMember.ref.name
|
const currName = currMember.ref.name
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.setDelay(currName, group, -2)
|
|
||||||
|
|
||||||
// Stagger requests so a batch does not hit the core at once.
|
// Stagger requests so a batch does not hit the core at once.
|
||||||
if (index > 1) {
|
if (index > 1) {
|
||||||
await new Promise((resolve) =>
|
await new Promise((resolve) =>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { hideNotice, showNotice } from '@/services/notice-service'
|
|||||||
import { requestService } from '@/services/service-request'
|
import { requestService } from '@/services/service-request'
|
||||||
|
|
||||||
type NavigateFunction = (path: string, options?: any) => void
|
type NavigateFunction = (path: string, options?: any) => void
|
||||||
type TranslateFunction = (key: string) => string
|
|
||||||
|
|
||||||
let shownStartupError: string | null = null
|
let shownStartupError: string | null = null
|
||||||
// Reads can settle out of order; only the newest may act, and a reset counts as one.
|
// Reads can settle out of order; only the newest may act, and a reset counts as one.
|
||||||
@@ -241,7 +240,6 @@ const handlers: Record<
|
|||||||
export const handleNoticeMessage = (
|
export const handleNoticeMessage = (
|
||||||
status: string,
|
status: string,
|
||||||
msg: string,
|
msg: string,
|
||||||
t: TranslateFunction,
|
|
||||||
navigate: NavigateFunction,
|
navigate: NavigateFunction,
|
||||||
) => {
|
) => {
|
||||||
const handler = handlers[status as NoticeStatus]
|
const handler = handlers[status as NoticeStatus]
|
||||||
|
|||||||
@@ -148,15 +148,6 @@ function parseNoticeExtras(extras: NoticeExtra[]): ParsedNoticeExtras {
|
|||||||
raw = extra
|
raw = extra
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params && isPlainRecord(extra)) {
|
|
||||||
params = extra
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration === undefined && typeof extra === 'number') {
|
|
||||||
duration = extra
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type QueryOptions<T> = {
|
|||||||
placeholderData?: T | (() => T | undefined)
|
placeholderData?: T | (() => T | undefined)
|
||||||
staleTime?: number
|
staleTime?: number
|
||||||
retry?: number | false
|
retry?: number | false
|
||||||
retryDelay?: number | ((attempt: number) => number)
|
retryDelay?: number
|
||||||
refetchInterval?: number | false
|
refetchInterval?: number | false
|
||||||
refetchIntervalInBackground?: boolean
|
refetchIntervalInBackground?: boolean
|
||||||
revalidateOnMount?: boolean
|
revalidateOnMount?: boolean
|
||||||
@@ -90,25 +90,9 @@ export function useQuery<T>(options: QueryOptions<T>): QueryResult<T> {
|
|||||||
const swr = useSWR<T>(enabled ? queryKey : null, queryFn, {
|
const swr = useSWR<T>(enabled ? queryKey : null, queryFn, {
|
||||||
dedupingInterval: staleTime,
|
dedupingInterval: staleTime,
|
||||||
errorRetryCount: retry === false ? 0 : retry,
|
errorRetryCount: retry === false ? 0 : retry,
|
||||||
errorRetryInterval:
|
errorRetryInterval: retryDelay ?? swrConfig.errorRetryInterval,
|
||||||
typeof retryDelay === 'number'
|
|
||||||
? retryDelay
|
|
||||||
: swrConfig.errorRetryInterval,
|
|
||||||
fallbackData,
|
fallbackData,
|
||||||
keepPreviousData: placeholderData !== undefined,
|
keepPreviousData: placeholderData !== undefined,
|
||||||
onErrorRetry: (_error, _key, config, revalidate, { retryCount }) => {
|
|
||||||
const maxRetries = config.errorRetryCount
|
|
||||||
if (maxRetries !== undefined && retryCount > maxRetries) return
|
|
||||||
|
|
||||||
const interval =
|
|
||||||
typeof retryDelay === 'function'
|
|
||||||
? retryDelay(Math.max(retryCount - 1, 0))
|
|
||||||
: config.errorRetryInterval
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
revalidate({ retryCount, dedupe: true })
|
|
||||||
}, interval)
|
|
||||||
},
|
|
||||||
revalidateOnFocus: refetchOnWindowFocus,
|
revalidateOnFocus: refetchOnWindowFocus,
|
||||||
revalidateOnMount,
|
revalidateOnMount,
|
||||||
revalidateOnReconnect: refetchOnReconnect,
|
revalidateOnReconnect: refetchOnReconnect,
|
||||||
|
|||||||
Vendored
-24
@@ -105,30 +105,6 @@ type IProxyGroupItem = Omit<IProxyItem, 'all'> & {
|
|||||||
all: IProxyItem[]
|
all: IProxyItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProxyProviderItem {
|
|
||||||
name: string
|
|
||||||
type: string
|
|
||||||
proxies: IProxyItem[]
|
|
||||||
updatedAt: string
|
|
||||||
vehicleType: string
|
|
||||||
subscriptionInfo?: {
|
|
||||||
Upload: number
|
|
||||||
Download: number
|
|
||||||
Total: number
|
|
||||||
Expire: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IRuleProviderItem {
|
|
||||||
name: string
|
|
||||||
behavior: string
|
|
||||||
format: string
|
|
||||||
ruleCount: number
|
|
||||||
type: string
|
|
||||||
updatedAt: string
|
|
||||||
vehicleType: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ITrafficItem {
|
interface ITrafficItem {
|
||||||
up: number
|
up: number
|
||||||
down: number
|
down: number
|
||||||
|
|||||||
+1
-3
@@ -14,9 +14,7 @@ export default defineConfig({
|
|||||||
legacy({
|
legacy({
|
||||||
modernTargets: ['edge>=109', 'safari>=14'],
|
modernTargets: ['edge>=109', 'safari>=14'],
|
||||||
renderLegacyChunks: false,
|
renderLegacyChunks: false,
|
||||||
// Auto-detected from the emitted bundle, dependencies included,
|
modernPolyfills: ['es.object.has-own', 'web.structured-clone'],
|
||||||
// so post-Safari-14 APIs never depend on extending a manual list.
|
|
||||||
modernPolyfills: true,
|
|
||||||
additionalModernPolyfills: [
|
additionalModernPolyfills: [
|
||||||
path.resolve('./src/polyfills/matchMedia.js'),
|
path.resolve('./src/polyfills/matchMedia.js'),
|
||||||
path.resolve('./src/polyfills/WeakRef.js'),
|
path.resolve('./src/polyfills/WeakRef.js'),
|
||||||
|
|||||||
Reference in New Issue
Block a user