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": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "26.13.0",
|
||||
"version": "24.22.1",
|
||||
"pnpmVersion": "12.15.0"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -426,7 +426,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -524,7 +524,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6.1.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Restore pnpm cache
|
||||
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -331,7 +331,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
@@ -466,7 +466,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -547,7 +547,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -573,7 +573,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -638,7 +638,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
nodejs 26.13.0
|
||||
nodejs 24.22.1
|
||||
pnpm 12.15.0
|
||||
rust 1.99.1
|
||||
|
||||
Generated
+24
-25
@@ -1175,7 +1175,7 @@ dependencies = [
|
||||
"windows-service",
|
||||
"windows-sys 0.61.2",
|
||||
"winreg 0.56.0",
|
||||
"zip 9.1.2",
|
||||
"zip 9.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2034,7 +2034,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2293,7 +2293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2809,7 +2809,7 @@ dependencies = [
|
||||
"gobject-sys 0.21.5",
|
||||
"libc",
|
||||
"system-deps 7.0.8",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4051,9 +4051,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.23.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
|
||||
checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb"
|
||||
dependencies = [
|
||||
"sha2 0.11.1",
|
||||
]
|
||||
@@ -4234,7 +4234,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4401,7 +4401,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4878,7 +4878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.45.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5543,7 +5543,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6060,7 +6060,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6119,7 +6119,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6739,7 +6739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7805,10 +7805,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8474,7 +8474,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8553,9 +8553,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
version = "0.13.0"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e"
|
||||
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
@@ -8583,7 +8583,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9125,7 +9125,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9146,7 +9146,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
@@ -9721,7 +9721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10066,9 +10066,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.1.2"
|
||||
version = "9.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec2e3161de27091f18a831d9494e9c53378a34833af5c1dda35ad85849da18d6"
|
||||
checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
@@ -10087,7 +10087,6 @@ dependencies = [
|
||||
"sha1 0.11.0",
|
||||
"time",
|
||||
"typed-path",
|
||||
"windows-sys 0.61.2",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
- 修复 代理 在全局模式下无法展开节点组
|
||||
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
- macOS:修复 Bigsur 上首页测速和流量图无法显示的问题
|
||||
|
||||
**🐧 Linux**
|
||||
|
||||
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
||||
|
||||
+3
-3
@@ -79,7 +79,7 @@
|
||||
"react-dom": "19.3.1",
|
||||
"react-error-boundary": "6.1.6",
|
||||
"react-hook-form": "^7.88.0",
|
||||
"react-i18next": "17.1.9",
|
||||
"react-i18next": "17.1.8",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "^8.4.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
@@ -97,7 +97,7 @@
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tauri-apps/cli": "2.12.3",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/node": "^24.13.6",
|
||||
"@types/react": "19.3.0",
|
||||
"@types/react-dom": "19.3.0",
|
||||
"@types/validator": "^13.15.10",
|
||||
@@ -118,7 +118,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.5.7",
|
||||
"eslint-plugin-sukka": "^10.2.0",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"glob": "^14.0.0",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.12.0",
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"husky": "^9.1.7",
|
||||
|
||||
Generated
+41
-74
@@ -275,8 +275,8 @@ importers:
|
||||
specifier: ^7.88.0
|
||||
version: 7.88.0(react@19.3.1)
|
||||
react-i18next:
|
||||
specifier: 17.1.9
|
||||
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)
|
||||
specifier: 17.1.8
|
||||
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:
|
||||
specifier: 10.1.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
|
||||
version: 4.17.12
|
||||
'@types/node':
|
||||
specifier: ^26.0.0
|
||||
version: 26.6.5
|
||||
specifier: ^24.13.6
|
||||
version: 24.13.6
|
||||
'@types/react':
|
||||
specifier: 19.3.0
|
||||
version: 19.3.0
|
||||
@@ -337,10 +337,10 @@ importers:
|
||||
version: 13.15.10
|
||||
'@vitejs/plugin-legacy':
|
||||
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':
|
||||
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':
|
||||
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))
|
||||
@@ -387,8 +387,8 @@ importers:
|
||||
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))
|
||||
glob:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.3
|
||||
specifier: ^13.0.6
|
||||
version: 13.0.6
|
||||
globals:
|
||||
specifier: ^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)
|
||||
vite:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
specifier: 10.1.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==}
|
||||
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':
|
||||
resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
@@ -2324,8 +2321,8 @@ packages:
|
||||
'@types/node@22.20.5':
|
||||
resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==}
|
||||
|
||||
'@types/node@26.6.5':
|
||||
resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==}
|
||||
'@types/node@24.13.6':
|
||||
resolution: {integrity: sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==}
|
||||
|
||||
'@types/parse-json@4.0.2':
|
||||
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
||||
@@ -3597,10 +3594,6 @@ packages:
|
||||
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
glob@14.0.3:
|
||||
resolution: {integrity: sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
globals@17.12.0:
|
||||
resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4183,10 +4176,6 @@ packages:
|
||||
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
minimatch@11.0.2:
|
||||
resolution: {integrity: sha512-pWTgz5CN3tjqyZNSK2tePlSmu9IwM41q8yn8Usdqqwe9TIHh+9dEpLLjF4uApsfPEAZY71qtSiA2B2APEr+1Yw==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
minipass@7.1.3:
|
||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -4348,10 +4337,6 @@ packages:
|
||||
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4458,8 +4443,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
react-i18next@17.1.9:
|
||||
resolution: {integrity: sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==}
|
||||
react-i18next@17.1.8:
|
||||
resolution: {integrity: sha512-vGH+Tbhqbv5LdN9KvC9GEmQ6ATxJAjyxPEK0zSCSRlkeDqCPNxVtEOvDNRAz+51HTxc6QPwhnHaFD7ICu6pD1A==}
|
||||
peerDependencies:
|
||||
i18next: '>= 26.2.0'
|
||||
react: '>= 16.8.0'
|
||||
@@ -4880,8 +4865,8 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici-types@8.9.0:
|
||||
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
undici@6.28.1:
|
||||
resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==}
|
||||
@@ -6318,8 +6303,6 @@ snapshots:
|
||||
dependencies:
|
||||
minipass: 7.1.3
|
||||
|
||||
'@isaacs/nonrecursive-glob-match@1.0.2': {}
|
||||
|
||||
'@jest/diff-sequences@30.5.0': {}
|
||||
|
||||
'@jest/expect-utils@30.5.2':
|
||||
@@ -6330,7 +6313,7 @@ snapshots:
|
||||
|
||||
'@jest/pattern@30.5.0':
|
||||
dependencies:
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 24.13.6
|
||||
jest-regex-util: 30.5.0
|
||||
|
||||
'@jest/schemas@30.5.0':
|
||||
@@ -6343,7 +6326,7 @@ snapshots:
|
||||
'@jest/schemas': 30.5.0
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
'@types/istanbul-reports': 3.0.4
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 24.13.6
|
||||
'@types/yargs': 17.0.35
|
||||
chalk: 4.1.2
|
||||
|
||||
@@ -7034,9 +7017,9 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@26.6.5':
|
||||
'@types/node@24.13.6':
|
||||
dependencies:
|
||||
undici-types: 8.9.0
|
||||
undici-types: 7.18.2
|
||||
|
||||
'@types/parse-json@4.0.2': {}
|
||||
|
||||
@@ -7058,7 +7041,7 @@ snapshots:
|
||||
|
||||
'@types/split2@4.2.3':
|
||||
dependencies:
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 24.13.6
|
||||
|
||||
'@types/stack-utils@2.0.3': {}
|
||||
|
||||
@@ -7073,7 +7056,7 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 24.13.6
|
||||
|
||||
'@types/yargs-parser@21.0.3': {}
|
||||
|
||||
@@ -7244,7 +7227,7 @@ snapshots:
|
||||
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
|
||||
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:
|
||||
'@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
|
||||
systemjs: 6.15.1
|
||||
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:
|
||||
- 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:
|
||||
'@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:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
'@vitest/spy': 5.0.1
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 1.4.1
|
||||
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':
|
||||
dependencies:
|
||||
@@ -8453,12 +8436,6 @@ snapshots:
|
||||
minipass: 7.1.3
|
||||
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: {}
|
||||
|
||||
gopd@1.2.0: {}
|
||||
@@ -8708,7 +8685,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/expect-utils': 30.5.2
|
||||
'@jest/types': 30.5.1
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 24.13.6
|
||||
jest-util: 30.5.1
|
||||
|
||||
jest-regex-util@30.5.0: {}
|
||||
@@ -8716,7 +8693,7 @@ snapshots:
|
||||
jest-util@30.5.1:
|
||||
dependencies:
|
||||
'@jest/types': 30.5.1
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 24.13.6
|
||||
chalk: 4.1.2
|
||||
ci-info: 4.4.0
|
||||
graceful-fs: 4.2.11
|
||||
@@ -9273,11 +9250,6 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
minizlib@3.1.0:
|
||||
@@ -9478,11 +9450,6 @@ snapshots:
|
||||
lru-cache: 11.5.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: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
@@ -9562,7 +9529,7 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
'@babel/runtime': 7.29.7
|
||||
html-parse-stringify: 4.0.1
|
||||
@@ -10016,7 +9983,7 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici-types@8.9.0: {}
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
undici@6.28.1: {}
|
||||
|
||||
@@ -10134,18 +10101,18 @@ snapshots:
|
||||
'@types/unist': 3.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:
|
||||
'@rollup/pluginutils': 5.4.0
|
||||
'@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)
|
||||
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:
|
||||
- rollup
|
||||
- supports-color
|
||||
- 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:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.7
|
||||
@@ -10153,7 +10120,7 @@ snapshots:
|
||||
rolldown: 1.2.9
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 24.13.6
|
||||
esbuild: 0.28.2
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.7.0
|
||||
@@ -10161,10 +10128,10 @@ snapshots:
|
||||
terser: 5.51.2
|
||||
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:
|
||||
'@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
|
||||
es-module-lexer: 2.3.2
|
||||
expect-type: 1.4.0
|
||||
@@ -10175,10 +10142,10 @@ snapshots:
|
||||
tinybench: 6.1.4
|
||||
tinyexec: 1.3.0
|
||||
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
|
||||
optionalDependencies:
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 24.13.6
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
|
||||
@@ -21,3 +21,27 @@ These rules supplement the repository's root `AGENTS.md`.
|
||||
- **Command ownership.** `services` owns backend command invocation and transport
|
||||
wrappers. Components and hooks use those boundaries; plugin mutations follow
|
||||
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
|
||||
helperText?: ReactNode
|
||||
placeholder?: string
|
||||
rows?: number
|
||||
separator?: string
|
||||
splitPattern?: RegExp
|
||||
defaultMode?: BaseSplitChipEditorMode
|
||||
showModeToggle?: boolean
|
||||
ariaLabel?: string
|
||||
addLabel?: ReactNode
|
||||
emptyLabel?: ReactNode
|
||||
modeLabels?: Partial<Record<BaseSplitChipEditorMode, ReactNode>>
|
||||
renderHeader?: (modeToggle: ReactNode) => ReactNode
|
||||
}
|
||||
|
||||
const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/
|
||||
|
||||
const splitValue = (value: string, splitPattern: RegExp) =>
|
||||
const splitValue = (value: string) =>
|
||||
value
|
||||
.split(splitPattern)
|
||||
.split(DEFAULT_SPLIT_PATTERN)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -49,35 +42,25 @@ export const BaseSplitChipEditor = ({
|
||||
error = false,
|
||||
helperText,
|
||||
placeholder,
|
||||
rows = 4,
|
||||
separator = ',',
|
||||
splitPattern = DEFAULT_SPLIT_PATTERN,
|
||||
defaultMode = 'visual',
|
||||
showModeToggle = true,
|
||||
ariaLabel,
|
||||
addLabel,
|
||||
emptyLabel,
|
||||
modeLabels,
|
||||
renderHeader,
|
||||
}: BaseSplitChipEditorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [mode, setMode] = useState<BaseSplitChipEditorMode>(defaultMode)
|
||||
const [mode, setMode] = useState<BaseSplitChipEditorMode>('visual')
|
||||
const [draft, setDraft] = useState('')
|
||||
|
||||
const resolvedLabels = useMemo(
|
||||
() => ({
|
||||
visual: modeLabels?.visual ?? t('shared.editorModes.visualization'),
|
||||
advanced: modeLabels?.advanced ?? t('shared.editorModes.advanced'),
|
||||
add: addLabel ?? t('shared.actions.new'),
|
||||
empty: emptyLabel ?? t('shared.statuses.empty'),
|
||||
visual: t('shared.editorModes.visualization'),
|
||||
advanced: t('shared.editorModes.advanced'),
|
||||
add: t('shared.actions.new'),
|
||||
empty: t('shared.statuses.empty'),
|
||||
}),
|
||||
[t, modeLabels, addLabel, emptyLabel],
|
||||
[t],
|
||||
)
|
||||
|
||||
const values = useMemo(
|
||||
() => splitValue(value, splitPattern),
|
||||
[value, splitPattern],
|
||||
)
|
||||
const values = useMemo(() => splitValue(value), [value])
|
||||
|
||||
const items = useMemo(() => {
|
||||
const counts = new Map<string, number>()
|
||||
@@ -92,7 +75,7 @@ export const BaseSplitChipEditor = ({
|
||||
}, [values])
|
||||
|
||||
const handleAddDraft = () => {
|
||||
const nextValues = splitValue(draft, splitPattern)
|
||||
const nextValues = splitValue(draft)
|
||||
if (!nextValues.length) {
|
||||
return
|
||||
}
|
||||
@@ -113,7 +96,7 @@ export const BaseSplitChipEditor = ({
|
||||
const resolvedAriaLabel =
|
||||
ariaLabel ?? (typeof toggleLabel === 'string' ? toggleLabel : undefined)
|
||||
|
||||
const modeToggle = showModeToggle ? (
|
||||
const modeToggle = (
|
||||
<Tooltip title={toggleLabel}>
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -128,7 +111,7 @@ export const BaseSplitChipEditor = ({
|
||||
<ToggleIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -202,7 +185,7 @@ export const BaseSplitChipEditor = ({
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
multiline
|
||||
rows={rows}
|
||||
rows={4}
|
||||
sx={{ width: '100%' }}
|
||||
value={value}
|
||||
helperText={helperText}
|
||||
|
||||
@@ -596,7 +596,7 @@ export const CurrentProxyCard = () => {
|
||||
[getProfileStorageKey],
|
||||
)
|
||||
|
||||
const { handleSelectChange } = useProxySelection({
|
||||
const { select } = useProxySelection({
|
||||
onSuccess: () => {
|
||||
refreshProxy()
|
||||
},
|
||||
@@ -766,15 +766,9 @@ export const CurrentProxyCard = () => {
|
||||
const previousProxy = selectedGroup.now
|
||||
const nextName = option.member.ref.name
|
||||
// The profile selection is the durable source across core restarts and run modes.
|
||||
handleSelectChange(
|
||||
selectedGroup.name,
|
||||
previousProxy!,
|
||||
selectedGroup.fixed!,
|
||||
)({
|
||||
target: { value: nextName },
|
||||
})
|
||||
select(selectedGroup.name, nextName, previousProxy!, selectedGroup.fixed!)
|
||||
},
|
||||
[handleSelectChange, isDirectMode, selectedGroup, unsortedProxyOptions],
|
||||
[isDirectMode, select, selectedGroup, unsortedProxyOptions],
|
||||
)
|
||||
|
||||
const goToProxies = useCallback(() => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useTrafficGraphDataEnhanced } from '@/hooks/use-traffic-monitor'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import parseTraffic from '@/utils/parse-traffic'
|
||||
import {
|
||||
formatTrafficHourMinute,
|
||||
@@ -23,14 +22,7 @@ import {
|
||||
isSameTrafficData,
|
||||
} from '@/utils/traffic-sampler'
|
||||
|
||||
interface ITrafficItem {
|
||||
up: number
|
||||
down: number
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
export interface EnhancedCanvasTrafficGraphRef {
|
||||
appendData: (data: ITrafficItem) => void
|
||||
toggleStyle: () => void
|
||||
}
|
||||
|
||||
@@ -142,7 +134,6 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
[],
|
||||
)
|
||||
const debounceTimeoutRef = useRef<number | null>(null)
|
||||
const [currentFPS, setCurrentFPS] = useState(GRAPH_CONFIG.targetFPS)
|
||||
|
||||
const colors = useMemo(
|
||||
() => ({
|
||||
@@ -183,8 +174,6 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
if (displayData.length === 0) {
|
||||
lastDataTimestampRef.current = 0
|
||||
dataStaleRef.current = false
|
||||
// eslint-disable-next-line @eslint-react/set-state-in-effect
|
||||
setCurrentFPS(GRAPH_CONFIG.targetFPS)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,18 +189,11 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
}
|
||||
}, [displayData])
|
||||
|
||||
const handleFocusStateChange = useCallback(
|
||||
(focused: boolean) => {
|
||||
isWindowFocusedRef.current = focused
|
||||
const handleFocusStateChange = useCallback((focused: boolean) => {
|
||||
isWindowFocusedRef.current = focused
|
||||
|
||||
if (focused || !pause_render_traffic_stats_on_blur) {
|
||||
setCurrentFPS(GRAPH_CONFIG.targetFPS)
|
||||
}
|
||||
|
||||
scheduleDrawGraphRef.current()
|
||||
},
|
||||
[pause_render_traffic_stats_on_blur],
|
||||
)
|
||||
scheduleDrawGraphRef.current()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
@@ -684,19 +666,13 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(getX(0), getY(0))
|
||||
|
||||
if (chartStyle === 'bezier') {
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const currentX = getX(i)
|
||||
const currentY = getY(i)
|
||||
const nextIndex = Math.min(i + 1, lastIndex)
|
||||
const controlX = (currentX + getX(nextIndex)) / 2
|
||||
const controlY = (currentY + getY(nextIndex)) / 2
|
||||
ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
|
||||
}
|
||||
} else {
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
ctx.lineTo(getX(i), getY(i))
|
||||
}
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const currentX = getX(i)
|
||||
const currentY = getY(i)
|
||||
const nextIndex = Math.min(i + 1, lastIndex)
|
||||
const controlX = (currentX + getX(nextIndex)) / 2
|
||||
const controlY = (currentY + getY(nextIndex)) / 2
|
||||
ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
|
||||
}
|
||||
|
||||
ctx.lineTo(getX(lastIndex), height - padding.bottom)
|
||||
@@ -984,21 +960,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
setChartStyle((prev) => (prev === 'bezier' ? 'line' : 'bezier'))
|
||||
}, [])
|
||||
|
||||
const appendData = useCallback((data: ITrafficItem) => {
|
||||
debugLog(
|
||||
'[EnhancedCanvasTrafficGraphV2] appendData called (using global data):',
|
||||
data,
|
||||
)
|
||||
}, [])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
appendData,
|
||||
toggleStyle,
|
||||
}),
|
||||
[appendData, toggleStyle],
|
||||
)
|
||||
useImperativeHandle(ref, () => ({ toggleStyle }), [toggleStyle])
|
||||
|
||||
const getTimeRangeText = useCallback(() => {
|
||||
return t('home.components.traffic.patterns.minutes', {
|
||||
@@ -1142,7 +1104,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
{t('home.components.traffic.diagnostics', {
|
||||
points: displayData.length,
|
||||
compressed: samplerStats.compressedBufferSize,
|
||||
fps: currentFPS,
|
||||
fps: GRAPH_CONFIG.targetFPS,
|
||||
})}
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -3,10 +3,14 @@ import {
|
||||
ArrowUpwardRounded,
|
||||
CloudDownloadRounded,
|
||||
CloudUploadRounded,
|
||||
ErrorOutlineRounded,
|
||||
LinkRounded,
|
||||
MemoryRounded,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
PaletteColor,
|
||||
Paper,
|
||||
@@ -15,9 +19,9 @@ import {
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { ReactNode, memo, useMemo, useRef } from 'react'
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { TrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
|
||||
import { useConnectionSummaryData } from '@/hooks/use-connection-data'
|
||||
import { useMemoryData } from '@/hooks/use-memory-data'
|
||||
import { useTrafficData } from '@/hooks/use-traffic-data'
|
||||
@@ -127,6 +131,50 @@ const CompactStatCard = memo(
|
||||
// 添加显示名称
|
||||
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 = () => {
|
||||
const { t } = useTranslation()
|
||||
const theme = useTheme()
|
||||
@@ -254,10 +302,11 @@ export const EnhancedTrafficStats = () => {
|
||||
}, [t, parsedData, displayMemory])
|
||||
|
||||
return (
|
||||
<TrafficErrorBoundary
|
||||
onError={(error, errorInfo) => {
|
||||
console.error('[EnhancedTrafficStats] 组件错误:', error, errorInfo)
|
||||
}}
|
||||
<ErrorBoundary
|
||||
FallbackComponent={TrafficFallback}
|
||||
onError={(error, errorInfo) =>
|
||||
console.error('[EnhancedTrafficStats]', error, errorInfo)
|
||||
}
|
||||
>
|
||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||
{trafficGraph && (
|
||||
@@ -273,6 +322,6 @@ export const EnhancedTrafficStats = () => {
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</TrafficErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,16 +86,11 @@ export const SystemInfoCard = () => {
|
||||
}, [verge, patchVerge])
|
||||
|
||||
const handleRunningModeClick = useCallback(async () => {
|
||||
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
|
||||
if (isSidecarMode) {
|
||||
await installServiceAndRestartCore()
|
||||
await mutateSystemState()
|
||||
}
|
||||
}, [
|
||||
isSidecarMode,
|
||||
isAdminMode,
|
||||
installServiceAndRestartCore,
|
||||
mutateSystemState,
|
||||
])
|
||||
}, [isSidecarMode, installServiceAndRestartCore, mutateSystemState])
|
||||
|
||||
const onCheckUpdate = useLockFn(async () => {
|
||||
try {
|
||||
@@ -122,18 +117,16 @@ export const SystemInfoCard = () => {
|
||||
|
||||
const runningModeStyle = useMemo(
|
||||
() => ({
|
||||
cursor:
|
||||
isSidecarMode || (isAdminMode && isSidecarMode) ? 'pointer' : 'default',
|
||||
textDecoration:
|
||||
isSidecarMode || (isAdminMode && isSidecarMode) ? 'underline' : 'none',
|
||||
cursor: isSidecarMode ? 'pointer' : 'default',
|
||||
textDecoration: isSidecarMode ? 'underline' : 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
'&:hover': {
|
||||
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
|
||||
opacity: isSidecarMode ? 0.7 : 1,
|
||||
},
|
||||
}),
|
||||
[isSidecarMode, isAdminMode],
|
||||
[isSidecarMode],
|
||||
)
|
||||
|
||||
const getModeIcon = () => {
|
||||
|
||||
@@ -87,7 +87,7 @@ export const TestCard = () => {
|
||||
x.uid === uid ? { ...x, ...patch } : x,
|
||||
)
|
||||
|
||||
mutateVerge({ ...verge, test_list: newList }, false)
|
||||
mutateVerge({ ...verge, test_list: newList })
|
||||
},
|
||||
[testList, verge, mutateVerge],
|
||||
)
|
||||
@@ -96,7 +96,7 @@ export const TestCard = () => {
|
||||
(uid: string) => {
|
||||
const newList = testList.filter((x) => x.uid !== uid)
|
||||
patchVerge({ test_list: newList })
|
||||
mutateVerge({ ...verge, test_list: newList }, false)
|
||||
mutateVerge({ ...verge, test_list: newList })
|
||||
},
|
||||
[testList, verge, patchVerge, mutateVerge],
|
||||
)
|
||||
@@ -129,7 +129,7 @@ export const TestCard = () => {
|
||||
newList.splice(newIndex, 0, removed)
|
||||
|
||||
// 优化:先本地更新,再异步 patch,避免UI卡死
|
||||
mutateVerge({ ...verge, test_list: newList }, false)
|
||||
mutateVerge({ ...verge, test_list: newList })
|
||||
const patchFn = () => {
|
||||
try {
|
||||
patchVerge({ test_list: newList })
|
||||
|
||||
@@ -35,10 +35,7 @@ export const LayoutSidebar = (props: LayoutSidebarProps) => {
|
||||
|
||||
const handleMenuOrderOptimisticUpdate = useCallback(
|
||||
(order: string[]) => {
|
||||
mutateVerge(
|
||||
(prev) => (prev ? { ...prev, menu_order: order } : prev),
|
||||
false,
|
||||
)
|
||||
mutateVerge((prev) => (prev ? { ...prev, menu_order: order } : prev))
|
||||
},
|
||||
[mutateVerge],
|
||||
)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import {
|
||||
ArrowDownwardRounded,
|
||||
ArrowUpwardRounded,
|
||||
ErrorOutlineRounded,
|
||||
MemoryRounded,
|
||||
} from '@mui/icons-material'
|
||||
import type { BoxProps, SvgIconProps, TypographyProps } from '@mui/material'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { LightweightTrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
|
||||
import { useMemoryData } from '@/hooks/use-memory-data'
|
||||
import { useTrafficData } from '@/hooks/use-traffic-data'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
@@ -17,6 +18,30 @@ import parseTraffic from '@/utils/parse-traffic'
|
||||
|
||||
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 = () => {
|
||||
const { t } = useTranslation()
|
||||
const { verge } = useVerge()
|
||||
@@ -75,7 +100,12 @@ export const LayoutTraffic = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<LightweightTrafficErrorBoundary>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={TrafficUnavailableFallback}
|
||||
onError={(error, errorInfo) =>
|
||||
console.error('[LayoutTraffic]', error, errorInfo)
|
||||
}
|
||||
>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
{trafficGraph && pageVisible && (
|
||||
<div
|
||||
@@ -130,7 +160,6 @@ export const LayoutTraffic = () => {
|
||||
cursor: 'auto',
|
||||
}}
|
||||
color={'disabled'}
|
||||
onClick={async () => {}}
|
||||
>
|
||||
<MemoryRounded {...iconStyle} />
|
||||
<Typography {...valStyle}>{inuse}</Typography>
|
||||
@@ -139,6 +168,6 @@ export const LayoutTraffic = () => {
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</LightweightTrafficErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Close, CropSquare, FilterNone, Minimize } from '@mui/icons-material'
|
||||
import { Box, IconButton } from '@mui/material'
|
||||
import {
|
||||
forwardRef,
|
||||
type PointerEvent,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
} from 'react'
|
||||
import { type PointerEvent, useCallback } from 'react'
|
||||
|
||||
import { useWindowControls } from '@/hooks/use-window'
|
||||
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 {
|
||||
currentWindow,
|
||||
maximized,
|
||||
minimize,
|
||||
close,
|
||||
toggleFullscreen,
|
||||
toggleMaximize,
|
||||
} = useWindowControls()
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
currentWindow,
|
||||
maximized,
|
||||
minimize,
|
||||
close,
|
||||
toggleFullscreen,
|
||||
toggleMaximize,
|
||||
}),
|
||||
[
|
||||
currentWindow,
|
||||
maximized,
|
||||
minimize,
|
||||
close,
|
||||
toggleFullscreen,
|
||||
toggleMaximize,
|
||||
],
|
||||
)
|
||||
const { maximized, minimize, close, toggleMaximize } = useWindowControls()
|
||||
|
||||
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
||||
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
|
||||
@@ -186,4 +154,4 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
}
|
||||
}, [handleScroll])
|
||||
|
||||
const { handleProxyGroupChange } = useProxySelection({
|
||||
const { select } = useProxySelection({
|
||||
onSuccess: () => {
|
||||
onProxies()
|
||||
},
|
||||
@@ -477,9 +477,9 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) 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) => {
|
||||
|
||||
@@ -30,10 +30,8 @@ export const useProxyGroupHeaderLayout = () => {
|
||||
if (canceled || !operation.source || !operation.target) return
|
||||
|
||||
const position = toolsOnLeft ? 'right' : 'left'
|
||||
mutateVerge(
|
||||
(prev) =>
|
||||
prev ? { ...prev, proxy_group_tools_position: position } : prev,
|
||||
false,
|
||||
mutateVerge((prev) =>
|
||||
prev ? { ...prev, proxy_group_tools_position: position } : prev,
|
||||
)
|
||||
try {
|
||||
await patchVerge({ proxy_group_tools_position: position })
|
||||
|
||||
@@ -113,17 +113,15 @@ export const BackupConfigViewer = memo(
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
await mutateVerge(
|
||||
(current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
webdav_url: trimmedUrl,
|
||||
webdav_username: trimmedUsername,
|
||||
webdav_password: data.password,
|
||||
}
|
||||
: current,
|
||||
false,
|
||||
await mutateVerge((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
webdav_url: trimmedUrl,
|
||||
webdav_username: trimmedUsername,
|
||||
webdav_password: data.password,
|
||||
}
|
||||
: current,
|
||||
)
|
||||
setWebdavStatus(signature, 'unknown')
|
||||
showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
|
||||
|
||||
@@ -107,7 +107,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
showNotice.error(err)
|
||||
}
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false)
|
||||
mutateVerge({ ...verge, ...patch })
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,44 +15,33 @@ import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
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) => {
|
||||
const { t } = useTranslation()
|
||||
const { verge, patchVerge } = useVerge()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [values, setValues] = useState({
|
||||
appLogLevel: 'warn',
|
||||
appLogMaxSize: 8,
|
||||
appLogMaxCount: 12,
|
||||
autoCloseConnection: true,
|
||||
autoCheckUpdate: true,
|
||||
enableBuiltinEnhanced: true,
|
||||
proxyLayoutColumn: 6,
|
||||
enableAutoDelayDetection: false,
|
||||
autoDelayDetectionIntervalMinutes: 5,
|
||||
defaultLatencyTest: '',
|
||||
autoLogClean: 2,
|
||||
defaultLatencyTimeout: 10000,
|
||||
})
|
||||
const [values, setValues] = useState(() => buildInitialValues(null))
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setOpen(true)
|
||||
setValues({
|
||||
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,
|
||||
})
|
||||
setValues(buildInitialValues(verge))
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}))
|
||||
|
||||
@@ -87,6 +87,17 @@ const splitBypass = (value?: string) =>
|
||||
.map((item) => item.trim())
|
||||
.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) => {
|
||||
const { t } = useTranslation()
|
||||
const systemName = getSystem()
|
||||
@@ -116,16 +127,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
proxy_host,
|
||||
} = verge ?? {}
|
||||
|
||||
const [value, setValue] = useState({
|
||||
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 [value, setValue] = useState(() => buildInitialValues(null))
|
||||
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
|
||||
null,
|
||||
)
|
||||
@@ -192,16 +194,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setOpen(true)
|
||||
setValue({
|
||||
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',
|
||||
})
|
||||
setValue(buildInitialValues(verge))
|
||||
fetchNetworkInterfaces()
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
@@ -339,7 +332,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
Promise.resolve().then(async () => {
|
||||
try {
|
||||
if (Object.keys(patch).length > 0) {
|
||||
mutateVerge({ ...verge, ...patch }, false)
|
||||
mutateVerge({ ...verge, ...patch })
|
||||
}
|
||||
if (Object.keys(patch).length > 0) {
|
||||
await patchVerge(patch)
|
||||
|
||||
@@ -76,34 +76,28 @@ const proxyOptionToken = ({ memberIndex, member }: TunnelProxyOption) =>
|
||||
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) => {
|
||||
const { t } = useTranslation()
|
||||
const { clash, mutateClash, patchClash } = useClash()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [values, setValues] = useState<TunnelFormValues>({
|
||||
localAddr: '',
|
||||
localPort: '',
|
||||
targetAddr: '',
|
||||
targetPort: '',
|
||||
network: 'tcp+udp',
|
||||
group: '',
|
||||
proxy: null,
|
||||
})
|
||||
const [values, setValues] = useState<TunnelFormValues>(buildInitialValues)
|
||||
const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setValues(() => ({
|
||||
localAddr: '',
|
||||
localPort: '',
|
||||
targetAddr: '',
|
||||
targetPort: '',
|
||||
network: 'tcp+udp',
|
||||
group: '',
|
||||
proxy: null,
|
||||
}))
|
||||
setValues(buildInitialValues())
|
||||
setDraftTunnels(() => clash?.tunnels ?? [])
|
||||
setOpen(true)
|
||||
// 如果没有隧道,则自动展开
|
||||
|
||||
@@ -50,21 +50,21 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
const handleAdd = useLockFn(async (value: string) => {
|
||||
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 })
|
||||
})
|
||||
|
||||
const handleChange = useLockFn(async (index: number, value?: string) => {
|
||||
const newList = [...webUIList]
|
||||
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 })
|
||||
})
|
||||
|
||||
const handleDelete = useLockFn(async (index: number) => {
|
||||
const newList = [...webUIList]
|
||||
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 })
|
||||
})
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useRef } from 'react'
|
||||
import React from 'react'
|
||||
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 { useVerge } from '@/hooks/use-verge'
|
||||
|
||||
import { GuardState } from './mods/guard-state'
|
||||
import { SettingList, SettingItem } from './mods/setting-comp'
|
||||
import { SysproxyViewer } from './mods/sysproxy-viewer'
|
||||
import { TunViewer } from './mods/tun-viewer'
|
||||
|
||||
interface Props {
|
||||
onError?: (err: Error) => void
|
||||
@@ -21,22 +19,16 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
|
||||
const { enable_auto_launch, enable_silent_start } = verge ?? {}
|
||||
|
||||
const sysproxyRef = useRef<DialogRef>(null)
|
||||
const tunRef = useRef<DialogRef>(null)
|
||||
|
||||
const onSwitchFormat = (
|
||||
_e: React.ChangeEvent<HTMLInputElement>,
|
||||
value: boolean,
|
||||
) => value
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false)
|
||||
mutateVerge({ ...verge, ...patch })
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingList title={t('settings.sections.system.title')}>
|
||||
<SysproxyViewer ref={sysproxyRef} />
|
||||
<TunViewer ref={tunRef} />
|
||||
|
||||
<ProxyControlSwitches
|
||||
label={t('settings.sections.system.toggles.tunMode')}
|
||||
onError={onError}
|
||||
@@ -58,8 +50,6 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
}}
|
||||
onGuard={async (e) => {
|
||||
try {
|
||||
// 先触发UI更新立即看到反馈
|
||||
onChangeData({ enable_auto_launch: e })
|
||||
await patchVerge({ enable_auto_launch: e })
|
||||
return Promise.resolve()
|
||||
} catch (error) {
|
||||
|
||||
@@ -72,7 +72,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
const backupRef = useRef<DialogRef>(null)
|
||||
|
||||
const onChangeData = (patch: any) => {
|
||||
mutateVerge({ ...verge, ...patch }, false)
|
||||
mutateVerge({ ...verge, ...patch })
|
||||
}
|
||||
|
||||
const onCopyClashEnv = useCallback(async () => {
|
||||
|
||||
@@ -163,7 +163,7 @@ const ProxyControlSwitches = ({
|
||||
})
|
||||
return false
|
||||
}
|
||||
mutateVerge({ ...verge, enable_tun_mode: value }, false)
|
||||
mutateVerge({ ...verge, enable_tun_mode: value })
|
||||
await patchVerge({ enable_tun_mode: value })
|
||||
}
|
||||
|
||||
@@ -229,8 +229,8 @@ const ProxyControlSwitches = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<SysproxyViewer ref={sysproxyRef} />
|
||||
<TunViewer ref={tunRef} />
|
||||
{isSystemProxyMode && <SysproxyViewer ref={sysproxyRef} />}
|
||||
{isTunMode && <TunViewer ref={tunRef} />}
|
||||
</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 { useMemo } from 'react'
|
||||
|
||||
import { downloadIconCache } from '@/services/cmds'
|
||||
import { useQuery } from '@/services/query-client'
|
||||
@@ -23,9 +22,8 @@ export const useIconCache = ({
|
||||
const iconValue = icon?.trim() ?? ''
|
||||
const cacheKeyValue = cacheKey?.trim() ?? ''
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
return enabled && iconValue.startsWith('http') && cacheKeyValue !== ''
|
||||
}, [enabled, iconValue, cacheKeyValue])
|
||||
const isEnabled =
|
||||
enabled && iconValue.startsWith('http') && cacheKeyValue !== ''
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['icon-cache', iconValue, cacheKeyValue],
|
||||
|
||||
@@ -54,8 +54,7 @@ export const useLogData = () => {
|
||||
const hasLoadedInitialLogsRef = useRef(false)
|
||||
|
||||
const { response, refresh, setData } = useMihomoWsSubscription<ILogItem[]>({
|
||||
storageKey: 'mihomo_logs_date',
|
||||
buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null),
|
||||
subscriptionKey: enableLog ? 'getClashLog' : null,
|
||||
fallbackData: [],
|
||||
connect: () => MihomoWebSocket.connect_logs(logLevel),
|
||||
setupHandlers: ({ next, scheduleReconnect, isMounted }) => {
|
||||
@@ -77,14 +76,13 @@ export const useLogData = () => {
|
||||
}
|
||||
const pendingLogs = buffer.splice(0, buffer.length)
|
||||
flushTimeStr = null
|
||||
next(null, (current) => appendLogs(current, pendingLogs))
|
||||
next((current) => appendLogs(current, pendingLogs))
|
||||
flushTimer = null
|
||||
}
|
||||
|
||||
return {
|
||||
handleMessage: (data) => {
|
||||
if (data.startsWith('Websocket error')) {
|
||||
next(data)
|
||||
void scheduleReconnect()
|
||||
return
|
||||
}
|
||||
@@ -108,9 +106,7 @@ export const useLogData = () => {
|
||||
if (!flushTimer) {
|
||||
flushTimer = setTimeout(flush, FLUSH_DELAY_MS)
|
||||
}
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
async onConnected() {
|
||||
if (hasLoadedInitialLogsRef.current) {
|
||||
@@ -119,7 +115,7 @@ export const useLogData = () => {
|
||||
const logs = await getClashLogs()
|
||||
hasLoadedInitialLogsRef.current = true
|
||||
if (isMounted()) {
|
||||
next(null, (current) => {
|
||||
next((current) => {
|
||||
if (!current || current.length === 0) {
|
||||
return clampLogs(filterLogsByLevel(logs, allowedTypes))
|
||||
}
|
||||
|
||||
@@ -33,15 +33,13 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
|
||||
const enabled = options?.enabled ?? true
|
||||
|
||||
const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({
|
||||
storageKey: 'mihomo_memory_date',
|
||||
buildSubscriptKey: (date) => (enabled ? `getClashMemory-${date}` : null),
|
||||
subscriptionKey: enabled ? 'getClashMemory' : null,
|
||||
fallbackData: FALLBACK_MEMORY_USAGE,
|
||||
connect: () => MihomoWebSocket.connect_memory(),
|
||||
throttleMs: 500,
|
||||
setupHandlers: ({ next, scheduleReconnect }) => ({
|
||||
handleMessage: (data) => {
|
||||
if (data.startsWith('Websocket error')) {
|
||||
next(data, FALLBACK_MEMORY_USAGE)
|
||||
void scheduleReconnect()
|
||||
return
|
||||
}
|
||||
@@ -51,10 +49,8 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
|
||||
if (shouldSkipDuplicateMemory(parsed)) {
|
||||
return
|
||||
}
|
||||
next(null, parsed)
|
||||
} catch (error) {
|
||||
next(error, FALLBACK_MEMORY_USAGE)
|
||||
}
|
||||
next(parsed)
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import {
|
||||
removeCacheData,
|
||||
setCacheData,
|
||||
useQuery,
|
||||
} from '@/services/query-client'
|
||||
import { setCacheData, useQuery } from '@/services/query-client'
|
||||
|
||||
const RECONNECT_DELAY_MS = 1000
|
||||
|
||||
@@ -31,7 +26,6 @@ interface SharedSubscriptionEntry {
|
||||
|
||||
const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>()
|
||||
const subscriptionSnapshots = new Map<string, unknown>()
|
||||
const initialSubscriptionDate = Date.now()
|
||||
|
||||
const getSubscriptionSnapshot = <T>(key: string) =>
|
||||
subscriptionSnapshots.get(key) as T | undefined
|
||||
@@ -136,15 +130,13 @@ const createSharedSubscriptionEntry = (
|
||||
return entry
|
||||
}
|
||||
|
||||
type Updater<T> = (current?: T) => T
|
||||
|
||||
/**
|
||||
* Mirrors SWR's MutatorCallback: consumers can pass either a plain value or a
|
||||
* functional updater `(current?: T) => T`. The functional form is resolved
|
||||
* against the current subscription snapshot before updating SWR.
|
||||
* Pass either a plain value or a functional updater; the updater is resolved
|
||||
* against the current subscription snapshot before the cache is written.
|
||||
*/
|
||||
type NextFn<T> = (
|
||||
error?: any,
|
||||
data?: T | ((current?: T) => T | undefined),
|
||||
) => void
|
||||
type NextFn<T> = (data: T | Updater<T>) => void
|
||||
|
||||
interface HandlerContext<T> {
|
||||
next: NextFn<T>
|
||||
@@ -159,8 +151,7 @@ interface HandlerResult {
|
||||
}
|
||||
|
||||
interface UseMihomoWsSubscriptionOptions<T> {
|
||||
storageKey: string
|
||||
buildSubscriptKey: (date: number) => string | null
|
||||
subscriptionKey: string | null
|
||||
fallbackData: T
|
||||
connect: () => Promise<MihomoWebSocket>
|
||||
/**
|
||||
@@ -178,18 +169,12 @@ interface UseMihomoWsSubscriptionOptions<T> {
|
||||
export const useMihomoWsSubscription = <T>(
|
||||
options: UseMihomoWsSubscriptionOptions<T>,
|
||||
) => {
|
||||
const {
|
||||
storageKey,
|
||||
buildSubscriptKey,
|
||||
fallbackData,
|
||||
connect,
|
||||
throttleMs,
|
||||
setupHandlers,
|
||||
} = options
|
||||
const { subscriptionKey, fallbackData, connect, throttleMs, setupHandlers } =
|
||||
options
|
||||
|
||||
const [date, setDate] = useLocalStorage(storageKey, initialSubscriptionDate)
|
||||
const subscriptKey = buildSubscriptKey(date)
|
||||
const subscriptionCacheKey = subscriptKey ? `$sub$${subscriptKey}` : null
|
||||
const subscriptionCacheKey = subscriptionKey
|
||||
? `$sub$${subscriptionKey}`
|
||||
: null
|
||||
const lastSubscriptionCacheKeyRef = useRef<string | null>(null)
|
||||
if (subscriptionCacheKey) {
|
||||
lastSubscriptionCacheKeyRef.current = subscriptionCacheKey
|
||||
@@ -197,20 +182,9 @@ export const useMihomoWsSubscription = <T>(
|
||||
const responseCacheKey =
|
||||
subscriptionCacheKey ?? lastSubscriptionCacheKeyRef.current
|
||||
|
||||
const resolveNextData = useCallback(
|
||||
(
|
||||
data: T | ((current?: T) => T | undefined) | undefined,
|
||||
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],
|
||||
)
|
||||
// Bumping the epoch re-runs the subscription effect, rebuilding the shared
|
||||
// entry (and its captured `connect`/handler closures) under the same key.
|
||||
const [refreshEpoch, setRefreshEpoch] = useState(0)
|
||||
|
||||
const response = useQuery<T>({
|
||||
queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'],
|
||||
@@ -238,44 +212,35 @@ export const useMihomoWsSubscription = <T>(
|
||||
let throttleCleanup: (() => void) | undefined
|
||||
let wrappedNext: NextFn<T>
|
||||
|
||||
const baseNext: NextFn<T> = (error, data) => {
|
||||
if (error !== undefined && error !== null) {
|
||||
return
|
||||
}
|
||||
if (data === undefined) return
|
||||
const resolved = resolveNextData(data, subscriptionCacheKey)
|
||||
const baseNext: NextFn<T> = (data) => {
|
||||
const resolved =
|
||||
typeof data === 'function'
|
||||
? (data as Updater<T>)(
|
||||
getSubscriptionSnapshot<T>(subscriptionCacheKey),
|
||||
)
|
||||
: (data as T)
|
||||
writeSubscriptionSnapshot(subscriptionCacheKey, resolved)
|
||||
}
|
||||
|
||||
if (throttleMs && throttleMs > 0) {
|
||||
let pendingData: T | ((current?: T) => T | undefined) | undefined
|
||||
let hasPending = false
|
||||
let pendingData: T | Updater<T> | undefined
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const flush = () => {
|
||||
timerId = null
|
||||
if (hasPending) {
|
||||
if (pendingData !== undefined) {
|
||||
const data = pendingData
|
||||
pendingData = undefined
|
||||
hasPending = false
|
||||
baseNext(undefined, data)
|
||||
baseNext(data)
|
||||
}
|
||||
}
|
||||
|
||||
wrappedNext = (
|
||||
error?: any,
|
||||
data?: T | ((current?: T) => T | undefined),
|
||||
) => {
|
||||
if (error !== undefined && error !== null) {
|
||||
baseNext(error, data)
|
||||
return
|
||||
}
|
||||
wrappedNext = (data) => {
|
||||
if (!timerId) {
|
||||
baseNext(undefined, data)
|
||||
baseNext(data)
|
||||
timerId = setTimeout(flush, throttleMs)
|
||||
} else {
|
||||
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-hooks/exhaustive-deps, @eslint-react/exhaustive-deps
|
||||
}, [subscriptionCacheKey])
|
||||
}, [subscriptionCacheKey, refreshEpoch])
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (subscriptionCacheKey) {
|
||||
subscriptionSnapshots.delete(subscriptionCacheKey)
|
||||
void removeCacheData([subscriptionCacheKey])
|
||||
writeSubscriptionSnapshot(subscriptionCacheKey, fallbackData)
|
||||
}
|
||||
setDate(Date.now())
|
||||
}, [subscriptionCacheKey, setDate])
|
||||
setRefreshEpoch((epoch) => epoch + 1)
|
||||
}, [subscriptionCacheKey, fallbackData])
|
||||
|
||||
const setData = useCallback(
|
||||
(data: T) => {
|
||||
|
||||
@@ -35,8 +35,8 @@ it('applies and records independent group selections while another group is pend
|
||||
const secondSuccess = vi.fn()
|
||||
const first = useProxySelection({ onSuccess: firstSuccess })
|
||||
const second = useProxySelection({ onSuccess: secondSuccess })
|
||||
first.changeProxy('Group A', 'Node A')
|
||||
second.changeProxy('Group B', 'Node B')
|
||||
first.select('Group A', 'Node A')
|
||||
second.select('Group B', 'Node B')
|
||||
await vi.waitFor(() =>
|
||||
expect(record).toHaveBeenCalledWith('Group B', 'Node B'),
|
||||
)
|
||||
|
||||
@@ -35,7 +35,6 @@ const cleanupConnections = async (previousProxy: string) => {
|
||||
interface ProxySelectionOptions {
|
||||
onSuccess?: () => void
|
||||
onError?: (error: any) => void
|
||||
enableConnectionCleanup?: boolean
|
||||
}
|
||||
|
||||
interface ProxyChangeRequest {
|
||||
@@ -53,7 +52,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
||||
const pendingRequestRef = useRef<ProxyChangeRequest | null>(null)
|
||||
const isProcessingRef = useRef(false)
|
||||
|
||||
const { onSuccess, onError, enableConnectionCleanup = true } = options
|
||||
const { onSuccess, onError } = options
|
||||
|
||||
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)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -112,7 +111,6 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
||||
[
|
||||
autoCloseConnection,
|
||||
forgetSelection,
|
||||
enableConnectionCleanup,
|
||||
onError,
|
||||
onSuccess,
|
||||
recordSelection,
|
||||
@@ -138,7 +136,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
||||
}
|
||||
}, [executeChange])
|
||||
|
||||
const changeProxy = useCallback(
|
||||
const select = useCallback(
|
||||
(
|
||||
groupName: string,
|
||||
proxyName: string,
|
||||
@@ -156,27 +154,5 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
||||
[flushChangeQueue],
|
||||
)
|
||||
|
||||
const handleSelectChange = useCallback(
|
||||
(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,
|
||||
}
|
||||
return { select }
|
||||
}
|
||||
|
||||
@@ -52,9 +52,8 @@ export const useSystemProxyState = () => {
|
||||
const toggleSystemProxy = async (enabled: boolean) => {
|
||||
// Roll failed optimistic writes back to the latest confirmed state.
|
||||
let confirmed = verge?.enable_system_proxy ?? false
|
||||
mutateVerge(
|
||||
(prev) => (prev ? { ...prev, enable_system_proxy: enabled } : prev),
|
||||
false,
|
||||
mutateVerge((prev) =>
|
||||
prev ? { ...prev, enable_system_proxy: enabled } : prev,
|
||||
)
|
||||
pendingRef.current = enabled
|
||||
|
||||
@@ -78,9 +77,8 @@ export const useSystemProxyState = () => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
mutateVerge(
|
||||
(prev) => (prev ? { ...prev, enable_system_proxy: confirmed } : prev),
|
||||
false,
|
||||
mutateVerge((prev) =>
|
||||
prev ? { ...prev, enable_system_proxy: confirmed } : prev,
|
||||
)
|
||||
// Queued requests were based on a state that never landed.
|
||||
pendingRef.current = null
|
||||
|
||||
@@ -41,7 +41,8 @@ export function useAppUptime() {
|
||||
const { data: uptime = 0 } = useQuery({
|
||||
queryKey: appUptimeQueryKey,
|
||||
queryFn: getAppUptime,
|
||||
staleTime: 5000,
|
||||
// The wrapper maps staleTime to SWR's dedupingInterval, which drops
|
||||
// refetchInterval ticks while it exceeds the interval.
|
||||
refetchInterval: 3000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
|
||||
@@ -32,15 +32,13 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
|
||||
graphData: { appendData },
|
||||
} = useTrafficMonitorEnhanced({ subscribe: false, enabled })
|
||||
const { response, refresh } = useMihomoWsSubscription<ITrafficItem>({
|
||||
storageKey: 'mihomo_traffic_date',
|
||||
buildSubscriptKey: (date) => (enabled ? `getClashTraffic-${date}` : null),
|
||||
subscriptionKey: enabled ? 'getClashTraffic' : null,
|
||||
fallbackData: FALLBACK_TRAFFIC,
|
||||
connect: () => MihomoWebSocket.connect_traffic(),
|
||||
throttleMs: 200,
|
||||
setupHandlers: ({ next, scheduleReconnect }) => ({
|
||||
handleMessage: (data) => {
|
||||
if (data.startsWith('Websocket error')) {
|
||||
next(data, FALLBACK_TRAFFIC)
|
||||
void scheduleReconnect()
|
||||
return
|
||||
}
|
||||
@@ -51,10 +49,8 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
|
||||
return
|
||||
}
|
||||
appendData(parsed)
|
||||
next(null, parsed)
|
||||
} catch (error) {
|
||||
next(error, FALLBACK_TRAFFIC)
|
||||
}
|
||||
next(parsed)
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Traffic } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { useVisibility } from '@/hooks/use-visibility'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { TrafficDataSampler, isSameTrafficData } from '@/utils/traffic-sampler'
|
||||
|
||||
// 引用计数管理器
|
||||
class ReferenceCounter {
|
||||
@@ -32,119 +31,11 @@ const WORKER_CONFIG = {
|
||||
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 {
|
||||
private worker: Worker | null = null
|
||||
private inlineMonitor: InlineTrafficMonitor | null = null
|
||||
private mode: 'worker' | 'inline' | null = null
|
||||
private listeners = new Set<
|
||||
(snapshot: ITrafficWorkerSnapshotMessage) => void
|
||||
>()
|
||||
private pendingMessages: TrafficWorkerRequestMessage[] = []
|
||||
private ready = false
|
||||
private currentRange = WORKER_CONFIG.defaultRangeMinutes
|
||||
|
||||
start(rangeMinutes?: number) {
|
||||
@@ -155,101 +46,52 @@ class TrafficWorkerClient {
|
||||
|
||||
this.currentRange = rangeMinutes ?? this.currentRange
|
||||
|
||||
if (this.ready) return
|
||||
if (this.worker) return
|
||||
|
||||
const initMessage: TrafficWorkerRequestMessage = {
|
||||
type: 'init',
|
||||
config: {
|
||||
rawDataMinutes: WORKER_CONFIG.rawDataMinutes,
|
||||
compressedDataMinutes: WORKER_CONFIG.compressedDataMinutes,
|
||||
compressionRatio: WORKER_CONFIG.compressionRatio,
|
||||
snapshotIntervalMs: WORKER_CONFIG.snapshotIntervalMs,
|
||||
defaultRangeMinutes: this.currentRange,
|
||||
},
|
||||
config: { ...WORKER_CONFIG, defaultRangeMinutes: this.currentRange },
|
||||
}
|
||||
|
||||
if (typeof Worker !== 'undefined') {
|
||||
try {
|
||||
this.worker = new Worker(
|
||||
new URL('../services/traffic-monitor-worker.ts', import.meta.url),
|
||||
{ type: 'module' },
|
||||
)
|
||||
this.mode = 'worker'
|
||||
this.worker = new Worker(
|
||||
new URL('../services/traffic-monitor-worker.ts', import.meta.url),
|
||||
{ type: 'module' },
|
||||
)
|
||||
|
||||
this.worker.onmessage = (
|
||||
event: MessageEvent<TrafficWorkerResponseMessage>,
|
||||
) => {
|
||||
const message = event.data
|
||||
if (message.type === 'snapshot') {
|
||||
if (import.meta.env.MODE === 'perf') {
|
||||
document.documentElement.dataset.perfWorker = String(
|
||||
message.lastTimestamp ?? 0,
|
||||
)
|
||||
document.documentElement.dataset.perfPoints = String(
|
||||
message.dataPoints.length,
|
||||
)
|
||||
const last = message.dataPoints.at(-1)
|
||||
document.documentElement.dataset.perfLastUp = String(
|
||||
last?.up ?? 0,
|
||||
)
|
||||
document.documentElement.dataset.perfLastDown = String(
|
||||
last?.down ?? 0,
|
||||
)
|
||||
}
|
||||
this.listeners.forEach((listener) => {
|
||||
listener(message)
|
||||
})
|
||||
}
|
||||
this.worker.onmessage = (
|
||||
event: MessageEvent<TrafficWorkerResponseMessage>,
|
||||
) => {
|
||||
const message = event.data
|
||||
if (message.type === 'snapshot') {
|
||||
if (import.meta.env.MODE === 'perf') {
|
||||
document.documentElement.dataset.perfWorker = String(
|
||||
message.lastTimestamp ?? 0,
|
||||
)
|
||||
document.documentElement.dataset.perfPoints = String(
|
||||
message.dataPoints.length,
|
||||
)
|
||||
const last = message.dataPoints.at(-1)
|
||||
document.documentElement.dataset.perfLastUp = String(last?.up ?? 0)
|
||||
document.documentElement.dataset.perfLastDown = String(
|
||||
last?.down ?? 0,
|
||||
)
|
||||
}
|
||||
|
||||
this.worker.onerror = (error) => {
|
||||
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
|
||||
this.listeners.forEach((listener) => {
|
||||
listener(message)
|
||||
})
|
||||
}
|
||||
} 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.flushQueue()
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.worker) {
|
||||
this.worker.terminate()
|
||||
}
|
||||
if (this.inlineMonitor) {
|
||||
this.inlineMonitor.stop()
|
||||
}
|
||||
this.worker?.terminate()
|
||||
this.worker = null
|
||||
this.inlineMonitor = null
|
||||
this.mode = null
|
||||
this.ready = false
|
||||
this.pendingMessages = []
|
||||
}
|
||||
|
||||
onSnapshot(listener: (snapshot: ITrafficWorkerSnapshotMessage) => void) {
|
||||
@@ -260,37 +102,11 @@ class TrafficWorkerClient {
|
||||
}
|
||||
|
||||
private post(message: TrafficWorkerRequestMessage) {
|
||||
if (!this.ready) {
|
||||
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)
|
||||
})
|
||||
this.worker?.postMessage(message)
|
||||
}
|
||||
|
||||
private ensureStarted() {
|
||||
if (!this.ready) {
|
||||
if (!this.worker) {
|
||||
this.start(this.currentRange)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ export const useVerge = () => {
|
||||
| IVergeConfig
|
||||
| ((prev: IVergeConfig | undefined) => IVergeConfig | undefined)
|
||||
| undefined,
|
||||
_revalidate?: boolean,
|
||||
) => {
|
||||
if (updaterOrData === undefined) {
|
||||
void refetch()
|
||||
|
||||
+6
-18
@@ -11,36 +11,24 @@ const useWindow = () => {
|
||||
}
|
||||
|
||||
export const useWindowControls = () => {
|
||||
const {
|
||||
maximized,
|
||||
minimize,
|
||||
toggleMaximize,
|
||||
close,
|
||||
toggleFullscreen,
|
||||
currentWindow,
|
||||
} = useWindow()
|
||||
const { maximized, minimize, toggleMaximize, close, currentWindow } =
|
||||
useWindow()
|
||||
return {
|
||||
maximized,
|
||||
minimize,
|
||||
toggleMaximize,
|
||||
close,
|
||||
toggleFullscreen,
|
||||
currentWindow,
|
||||
} satisfies Pick<
|
||||
WindowContextType,
|
||||
| 'maximized'
|
||||
| 'minimize'
|
||||
| 'toggleMaximize'
|
||||
| 'close'
|
||||
| 'toggleFullscreen'
|
||||
| 'currentWindow'
|
||||
'maximized' | 'minimize' | 'toggleMaximize' | 'close' | 'currentWindow'
|
||||
>
|
||||
}
|
||||
|
||||
export const useWindowDecorations = () => {
|
||||
const { decorated, toggleDecorations, refreshDecorated } = useWindow()
|
||||
return { decorated, toggleDecorations, refreshDecorated } satisfies Pick<
|
||||
const { decorated, toggleDecorations } = useWindow()
|
||||
return { decorated, toggleDecorations } satisfies Pick<
|
||||
WindowContextType,
|
||||
'decorated' | 'toggleDecorations' | 'refreshDecorated'
|
||||
'decorated' | 'toggleDecorations'
|
||||
>
|
||||
}
|
||||
|
||||
+4
-21
@@ -1,8 +1,7 @@
|
||||
import { Paper, ThemeProvider } from '@mui/material'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { Outlet, useNavigate } from 'react-router'
|
||||
|
||||
import { BaseErrorBoundary } from '@/components/base'
|
||||
@@ -38,7 +37,6 @@ const OS = getSystem()
|
||||
const Layout = () => {
|
||||
const mode = useThemeMode()
|
||||
const isDark = mode !== 'light'
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useCustomTheme()
|
||||
const { verge } = useVerge()
|
||||
const { language } = verge ?? {}
|
||||
@@ -46,7 +44,6 @@ const Layout = () => {
|
||||
const { switchLanguage } = useI18n()
|
||||
const navigate = useNavigate()
|
||||
const themeReady = useMemo(() => Boolean(theme), [theme])
|
||||
const windowControlsRef = useRef<any>(null)
|
||||
const { decorated } = useWindowDecorations()
|
||||
|
||||
const customTitlebar = useMemo(
|
||||
@@ -57,7 +54,7 @@ const Layout = () => {
|
||||
className="the_titlebar-drag-region"
|
||||
data-tauri-drag-region="true"
|
||||
/>
|
||||
<WindowControls ref={windowControlsRef} />
|
||||
<WindowControls />
|
||||
</div>
|
||||
) : null,
|
||||
[decorated],
|
||||
@@ -69,12 +66,12 @@ const Layout = () => {
|
||||
(payload: [string, string]) => {
|
||||
const [status, msg] = payload
|
||||
try {
|
||||
handleNoticeMessage(status, msg, t, navigate)
|
||||
handleNoticeMessage(status, msg, navigate)
|
||||
} catch (error) {
|
||||
console.error('[通知处理] 失败:', error)
|
||||
}
|
||||
},
|
||||
[t, navigate],
|
||||
[navigate],
|
||||
)
|
||||
|
||||
useEventBus(handleNotice)
|
||||
@@ -110,20 +107,6 @@ const Layout = () => {
|
||||
<NoticeManager position={verge?.notice_position} />
|
||||
<ServiceMigrationDialog />
|
||||
<SysproxyPrivilegeDialog />
|
||||
<div
|
||||
style={{
|
||||
animation: 'fadeIn 0.5s',
|
||||
WebkitAnimation: 'fadeIn 0.5s',
|
||||
}}
|
||||
/>
|
||||
<style>
|
||||
{`
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<Paper
|
||||
square
|
||||
elevation={0}
|
||||
|
||||
@@ -83,7 +83,6 @@ const ProfilePage = () => {
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
const [url, setUrl] = useState('')
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const [profileDndRevision, setProfileDndRevision] = useState(0)
|
||||
const [activatings, setActivatings] = useState<string[]>([])
|
||||
const [switchTarget, setSwitchTarget] = useState<string | null>(null)
|
||||
@@ -257,7 +256,6 @@ const ProfilePage = () => {
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
setDisabled(false)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -824,7 +822,7 @@ const ProfilePage = () => {
|
||||
if (event.key !== 'Enter' || event.nativeEvent.isComposing) {
|
||||
return
|
||||
}
|
||||
if (!url || disabled || loading) {
|
||||
if (!url || loading) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
@@ -857,7 +855,7 @@ const ProfilePage = () => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={!url || disabled}
|
||||
disabled={!url}
|
||||
loading={loading}
|
||||
variant="contained"
|
||||
size="small"
|
||||
|
||||
+1
-4
@@ -24,11 +24,8 @@ const RulesPage = () => {
|
||||
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||
const pageVisible = useVisibility()
|
||||
|
||||
// 在组件挂载时和页面获得焦点时刷新规则数据
|
||||
// 页面可见时刷新规则数据:挂载时可见或从隐藏恢复可见各触发一次
|
||||
useEffect(() => {
|
||||
refreshRules()
|
||||
refreshRuleProviders()
|
||||
|
||||
if (pageVisible) {
|
||||
refreshRules()
|
||||
refreshRuleProviders()
|
||||
|
||||
@@ -26,7 +26,9 @@ const TQ_MIHOMO = {
|
||||
refetchOnReconnect: false,
|
||||
staleTime: 1500,
|
||||
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
|
||||
|
||||
const TQ_DEFAULTS = {
|
||||
|
||||
@@ -5,11 +5,9 @@ export interface WindowContextType {
|
||||
decorated: boolean | null
|
||||
maximized: boolean | null
|
||||
toggleDecorations: () => Promise<void>
|
||||
refreshDecorated: () => Promise<boolean>
|
||||
minimize: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
toggleMaximize: () => Promise<void>
|
||||
toggleFullscreen: () => Promise<void>
|
||||
currentWindow: ReturnType<typeof getCurrentWindow>
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,6 @@ export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
}
|
||||
}, [currentWindow])
|
||||
|
||||
const toggleFullscreen = useCallback(async () => {
|
||||
await currentWindow.setFullscreen(!(await currentWindow.isFullscreen()))
|
||||
}, [currentWindow])
|
||||
|
||||
const refreshDecorated = useCallback(async () => {
|
||||
const val = await currentWindow.isDecorated()
|
||||
setDecorated(val)
|
||||
@@ -87,22 +83,18 @@ export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
decorated,
|
||||
maximized,
|
||||
toggleDecorations,
|
||||
refreshDecorated,
|
||||
minimize,
|
||||
close,
|
||||
toggleMaximize,
|
||||
toggleFullscreen,
|
||||
currentWindow,
|
||||
}),
|
||||
[
|
||||
decorated,
|
||||
maximized,
|
||||
toggleDecorations,
|
||||
refreshDecorated,
|
||||
minimize,
|
||||
close,
|
||||
toggleMaximize,
|
||||
toggleFullscreen,
|
||||
currentWindow,
|
||||
],
|
||||
)
|
||||
|
||||
+38
-37
@@ -35,7 +35,6 @@ interface IpInfo {
|
||||
interface ServiceConfig {
|
||||
url: string
|
||||
mapping: (data: any) => IpInfo
|
||||
timeout?: number // 保留timeout字段(如有需要)
|
||||
}
|
||||
|
||||
// 可用的IP检测服务列表及字段映射
|
||||
@@ -156,48 +155,52 @@ export const getIpInfo = async (): Promise<
|
||||
for (const service of shuffledServices) {
|
||||
debugLog(`尝试IP检测服务: ${service.url}`)
|
||||
|
||||
const timeoutController = new AbortController()
|
||||
const timeoutId = setTimeout(() => {
|
||||
timeoutController.abort()
|
||||
}, service.timeout || serviceTimeout)
|
||||
|
||||
try {
|
||||
return await asyncRetry(
|
||||
async (bail) => {
|
||||
console.debug('Fetching IP information:', service.url)
|
||||
|
||||
const response = await fetch(service.url, {
|
||||
method: 'GET',
|
||||
signal: timeoutController.signal, // AbortSignal.timeout(service.timeout || serviceTimeout),
|
||||
connectTimeout: service.timeout || serviceTimeout,
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
})
|
||||
const timeoutController = new AbortController()
|
||||
const timeoutId = setTimeout(() => {
|
||||
timeoutController.abort()
|
||||
}, serviceTimeout)
|
||||
|
||||
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(),
|
||||
const response = await fetch(service.url, {
|
||||
method: 'GET',
|
||||
signal: timeoutController.signal,
|
||||
connectTimeout: serviceTimeout,
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
})
|
||||
} 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) {
|
||||
debugLog(`IP检测服务失败: ${service.url}`, error)
|
||||
lastError = error
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const mountBus = async () => {
|
||||
return createEventBus({
|
||||
dispatch,
|
||||
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])),
|
||||
readPendingFailures: () => {},
|
||||
@@ -72,7 +72,7 @@ it('does not replay a recovered startup error after WebView recreation', async (
|
||||
kind: 'startFailed',
|
||||
detail: 'startup failed',
|
||||
})
|
||||
first.handleNoticeMessage('core_start::error', '', (key) => key, vi.fn())
|
||||
first.handleNoticeMessage('core_start::error', '', vi.fn())
|
||||
await flush()
|
||||
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.resetModules()
|
||||
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()
|
||||
expect(showNotice.error).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
@@ -277,6 +277,8 @@ class DelayManager {
|
||||
group: string,
|
||||
timeout: number,
|
||||
): 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)
|
||||
this.queueGroupNotification(group)
|
||||
return update
|
||||
@@ -298,8 +300,6 @@ class DelayManager {
|
||||
`[DelayManager] 开始测试延迟,代理: ${name}, 组: ${group}, 超时: ${timeout}ms`,
|
||||
)
|
||||
|
||||
this.setDelay(name, group, -2)
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
@@ -359,8 +359,6 @@ class DelayManager {
|
||||
const currName = currMember.ref.name
|
||||
|
||||
try {
|
||||
this.setDelay(currName, group, -2)
|
||||
|
||||
// Stagger requests so a batch does not hit the core at once.
|
||||
if (index > 1) {
|
||||
await new Promise((resolve) =>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { hideNotice, showNotice } from '@/services/notice-service'
|
||||
import { requestService } from '@/services/service-request'
|
||||
|
||||
type NavigateFunction = (path: string, options?: any) => void
|
||||
type TranslateFunction = (key: string) => string
|
||||
|
||||
let shownStartupError: string | null = null
|
||||
// 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 = (
|
||||
status: string,
|
||||
msg: string,
|
||||
t: TranslateFunction,
|
||||
navigate: NavigateFunction,
|
||||
) => {
|
||||
const handler = handlers[status as NoticeStatus]
|
||||
|
||||
@@ -148,15 +148,6 @@ function parseNoticeExtras(extras: NoticeExtra[]): ParsedNoticeExtras {
|
||||
raw = extra
|
||||
continue
|
||||
}
|
||||
|
||||
if (!params && isPlainRecord(extra)) {
|
||||
params = extra
|
||||
continue
|
||||
}
|
||||
|
||||
if (duration === undefined && typeof extra === 'number') {
|
||||
duration = extra
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -18,7 +18,7 @@ type QueryOptions<T> = {
|
||||
placeholderData?: T | (() => T | undefined)
|
||||
staleTime?: number
|
||||
retry?: number | false
|
||||
retryDelay?: number | ((attempt: number) => number)
|
||||
retryDelay?: number
|
||||
refetchInterval?: number | false
|
||||
refetchIntervalInBackground?: boolean
|
||||
revalidateOnMount?: boolean
|
||||
@@ -90,25 +90,9 @@ export function useQuery<T>(options: QueryOptions<T>): QueryResult<T> {
|
||||
const swr = useSWR<T>(enabled ? queryKey : null, queryFn, {
|
||||
dedupingInterval: staleTime,
|
||||
errorRetryCount: retry === false ? 0 : retry,
|
||||
errorRetryInterval:
|
||||
typeof retryDelay === 'number'
|
||||
? retryDelay
|
||||
: swrConfig.errorRetryInterval,
|
||||
errorRetryInterval: retryDelay ?? swrConfig.errorRetryInterval,
|
||||
fallbackData,
|
||||
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,
|
||||
revalidateOnMount,
|
||||
revalidateOnReconnect: refetchOnReconnect,
|
||||
|
||||
Vendored
-24
@@ -105,30 +105,6 @@ type IProxyGroupItem = Omit<IProxyItem, 'all'> & {
|
||||
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 {
|
||||
up: number
|
||||
down: number
|
||||
|
||||
+1
-3
@@ -14,9 +14,7 @@ export default defineConfig({
|
||||
legacy({
|
||||
modernTargets: ['edge>=109', 'safari>=14'],
|
||||
renderLegacyChunks: false,
|
||||
// Auto-detected from the emitted bundle, dependencies included,
|
||||
// so post-Safari-14 APIs never depend on extending a manual list.
|
||||
modernPolyfills: true,
|
||||
modernPolyfills: ['es.object.has-own', 'web.structured-clone'],
|
||||
additionalModernPolyfills: [
|
||||
path.resolve('./src/polyfills/matchMedia.js'),
|
||||
path.resolve('./src/polyfills/WeakRef.js'),
|
||||
|
||||
Reference in New Issue
Block a user