mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-07 01:22:15 +08:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
438fc20142 | ||
|
|
7fd537af08 | ||
|
|
38faf20f90 | ||
|
|
87d9f95990 | ||
|
|
b9ab3b2015 | ||
|
|
a45c681389 | ||
|
|
a15ff6b448 | ||
|
|
dae542f772 | ||
|
|
39ba1e64a6 | ||
|
|
27da92b5ff | ||
|
|
f076d4cd5d | ||
|
|
97dc1622ee | ||
|
|
f65a7f1348 | ||
|
|
14abc4c4ee | ||
|
|
84bbe790e8 | ||
|
|
e4d82a1b3f | ||
|
|
9fb46607b5 | ||
|
|
943e18e8ca | ||
|
|
8be05c178b | ||
|
|
01fbc5714f | ||
|
|
5d41f99eb1 | ||
|
|
1105eedeb6 | ||
|
|
891757ede6 | ||
|
|
2b7b7cc0e7 | ||
|
|
40fd1d5012 | ||
|
|
8039feb270 | ||
|
|
9a904af3aa | ||
|
|
d7964e6d80 | ||
|
|
6aeae2f970 |
@@ -5,7 +5,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "26.13.0",
|
||||
"pnpmVersion": "12.16.1"
|
||||
"pnpmVersion": "12.18.0"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.99.1",
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
run: bash ./scripts/extract_update_logs.sh
|
||||
shell: bash
|
||||
|
||||
- uses: pnpm/action-setup@v6.1.0
|
||||
- uses: pnpm/action-setup@v6.1.1
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
@@ -157,15 +157,12 @@ jobs:
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
@@ -177,7 +174,7 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- uses: pnpm/action-setup@v6.1.0
|
||||
- uses: pnpm/action-setup@v6.1.1
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
@@ -188,14 +185,6 @@ jobs:
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
restore-keys: |
|
||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -204,12 +193,6 @@ jobs:
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
- name: Tauri build for Windows-macOS-Linux
|
||||
uses: tauri-apps/tauri-action@v1
|
||||
env:
|
||||
@@ -260,22 +243,19 @@ jobs:
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6.1.0
|
||||
uses: pnpm/action-setup@v6.1.1
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
@@ -285,14 +265,6 @@ jobs:
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
restore-keys: |
|
||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -345,12 +317,6 @@ jobs:
|
||||
gcc-arm-linux-gnueabihf \
|
||||
g++-arm-linux-gnueabihf
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
- name: Tauri Build for Linux
|
||||
run: |
|
||||
export PKG_CONFIG_ALLOW_CROSS=1
|
||||
@@ -404,22 +370,25 @@ jobs:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}-fixed-webview2'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6.1.0
|
||||
uses: pnpm/action-setup@v6.1.1
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
@@ -429,14 +398,6 @@ jobs:
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
restore-keys: |
|
||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -452,12 +413,6 @@ jobs:
|
||||
Remove-Item .\src-tauri\tauri.windows.conf.json
|
||||
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
- name: Tauri build for Windows
|
||||
id: build
|
||||
uses: tauri-apps/tauri-action@v1
|
||||
@@ -526,7 +481,7 @@ jobs:
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6.1.0
|
||||
- uses: pnpm/action-setup@v6.1.1
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
@@ -77,11 +77,12 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@1.99.1
|
||||
|
||||
- name: Rust Cache
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
@@ -106,15 +107,6 @@ jobs:
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
restore-keys: |
|
||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Pnpm install and check
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
|
||||
@@ -4,13 +4,13 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/**"
|
||||
- "scripts/**"
|
||||
- "**/*.js"
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.css"
|
||||
- "**/*.scss"
|
||||
- "**/*.json"
|
||||
- "**/*.md"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "vite.config.*"
|
||||
@@ -39,16 +39,16 @@ jobs:
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Restore pnpm cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: "pnpm-shared-stable-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}"
|
||||
restore-keys: |
|
||||
pnpm-shared-stable-${{ runner.os }}-
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Check generated i18n types are up to date
|
||||
run: |
|
||||
pnpm i18n:types
|
||||
if ! git diff --exit-code -- src/types/generated; then
|
||||
echo "::error::src/types/generated is stale; run pnpm i18n:types and commit the result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run Prettier check
|
||||
run: pnpm format:check
|
||||
|
||||
|
||||
@@ -206,15 +206,12 @@ jobs:
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
@@ -226,27 +223,22 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v1.0.0
|
||||
env:
|
||||
@@ -319,30 +311,28 @@ jobs:
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -392,12 +382,6 @@ jobs:
|
||||
gcc-arm-linux-gnueabihf \
|
||||
g++-arm-linux-gnueabihf
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
- name: Build for Linux
|
||||
run: |
|
||||
export PKG_CONFIG_ALLOW_CROSS=1
|
||||
@@ -458,30 +442,28 @@ jobs:
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||
shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}-fixed-webview2'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -494,12 +476,6 @@ jobs:
|
||||
Remove-Item .\src-tauri\tauri.windows.conf.json
|
||||
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v1.0.0
|
||||
env:
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
nodejs 26.13.0
|
||||
pnpm 12.16.1
|
||||
pnpm 12.18.0
|
||||
rust 1.99.1
|
||||
|
||||
Generated
+189
-153
@@ -150,7 +150,7 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"parking_lot 0.12.5",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.60.2",
|
||||
"wl-clipboard-rs",
|
||||
@@ -159,9 +159,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.2"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
|
||||
checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
@@ -1081,7 +1081,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "2.5.10"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -1089,7 +1089,6 @@ dependencies = [
|
||||
"async-trait",
|
||||
"backon",
|
||||
"base64 0.23.1",
|
||||
"bitflags 2.13.2",
|
||||
"boa_engine",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -1100,7 +1099,6 @@ dependencies = [
|
||||
"clash-verge-media-unlock",
|
||||
"clash-verge-signal",
|
||||
"clash_verge_service_ipc",
|
||||
"criterion",
|
||||
"dark-light",
|
||||
"deelevate",
|
||||
"dirs 7.0.0",
|
||||
@@ -1111,16 +1109,14 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"nanoid",
|
||||
"network-interface",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"open",
|
||||
"parking_lot",
|
||||
"parking_lot 0.13.0",
|
||||
"percent-encoding",
|
||||
"regex",
|
||||
"reqwest 0.13.5",
|
||||
"reqwest_dav",
|
||||
"rfd",
|
||||
@@ -1150,7 +1146,6 @@ dependencies = [
|
||||
"tauri-plugin-mihomo",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-wdio-webdriver",
|
||||
@@ -1158,7 +1153,6 @@ dependencies = [
|
||||
"tauri-specta",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"warp",
|
||||
@@ -1176,7 +1170,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"criterion",
|
||||
"parking_lot",
|
||||
"parking_lot 0.13.0",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -1199,7 +1193,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"flexi_logger",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"parking_lot 0.13.0",
|
||||
"tracing-estuary",
|
||||
]
|
||||
|
||||
@@ -1256,8 +1250,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "facfbc410a385e0b0cbca8179dd9e363ed70e8455960bf7545137e90ca753b66"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
"lock_api 0.4.14",
|
||||
"parking_lot_core 0.9.12",
|
||||
"polonius-the-crab",
|
||||
]
|
||||
|
||||
@@ -1334,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1768,8 +1762,18 @@ version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.23.0",
|
||||
"darling_macro 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
|
||||
dependencies = [
|
||||
"darling_core 0.24.1",
|
||||
"darling_macro 0.24.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1785,17 +1789,41 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.23.0",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
||||
dependencies = [
|
||||
"darling_core 0.24.1",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.2.1"
|
||||
@@ -1805,9 +1833,9 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"lock_api 0.4.14",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
"parking_lot_core 0.9.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1885,9 +1913,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "devtools-core"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c53d416e473377276435d49d731a75d253b96ee4339831cd85653702b94d8298"
|
||||
checksum = "c9e85926bda763dd434fcfbcfb24af4b2c1a8c3e3c34bcd5d844c48f655cca51"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"devtools-wire-format",
|
||||
@@ -2015,7 +2043,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2274,7 +2302,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]]
|
||||
@@ -2790,7 +2818,7 @@ dependencies = [
|
||||
"gobject-sys 0.21.5",
|
||||
"libc",
|
||||
"system-deps 7.0.8",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3344,7 +3372,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.61.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3358,12 +3386,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ico"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
|
||||
checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"png 0.17.16",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3549,7 +3577,7 @@ dependencies = [
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png 0.18.1",
|
||||
"png",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
@@ -3858,7 +3886,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"libc",
|
||||
"parking_lot",
|
||||
"parking_lot 0.12.5",
|
||||
"path-tree",
|
||||
"pin-project-lite",
|
||||
"rand 0.10.2",
|
||||
@@ -4019,10 +4047,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
name = "lock_api"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
checksum = "65d91b22da179613dcf4d6ec8349563f52a363f6849e03b00dd0a185ec4a59af"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
@@ -4032,9 +4069,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
|
||||
checksum = "490750560fa7851fbbf5d3ab1a8750f09350ed361ed8aed85b1e1e40587812ee"
|
||||
dependencies = [
|
||||
"sha2 0.11.1",
|
||||
]
|
||||
@@ -4212,10 +4249,10 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"png 0.18.1",
|
||||
"png",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4382,7 +4419,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4652,17 +4689,6 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-open-directory"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
@@ -4764,7 +4790,7 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
dependencies = [
|
||||
"parking_lot_core",
|
||||
"parking_lot_core 0.9.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4787,9 +4813,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.4.4"
|
||||
version = "5.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
|
||||
checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"is-wsl",
|
||||
@@ -4859,7 +4885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4923,8 +4949,18 @@ version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
"lock_api 0.4.14",
|
||||
"parking_lot_core 0.9.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9cf937ca4e1d0b7578784559063422fc39ae7c5a00f02471f2614d5486fd226"
|
||||
dependencies = [
|
||||
"lock_api 0.5.0",
|
||||
"parking_lot_core 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4935,7 +4971,19 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cfff060068cc18aa7815010fcaf80c68c7fb9e85fc7f5c0f9bf142c21d19826"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.9.4",
|
||||
"smallvec",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
@@ -5226,19 +5274,6 @@ dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
@@ -5524,7 +5559,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5668,6 +5703,15 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
@@ -5865,9 +5909,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
@@ -5875,16 +5919,18 @@ dependencies = [
|
||||
"gobject-sys 0.18.0",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6028,7 +6074,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6041,7 +6087,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6100,7 +6146,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6478,7 +6524,7 @@ version = "3.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.23.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
@@ -6720,7 +6766,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]]
|
||||
@@ -6738,7 +6784,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
@@ -6773,9 +6819,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "specta"
|
||||
version = "2.0.0-rc.25"
|
||||
version = "2.0.0-rc.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50"
|
||||
checksum = "ed155e448f42fc4be7a5cbc12bd43f6c549981f7c32fcba429ff6bc57cc4f30c"
|
||||
dependencies = [
|
||||
"paste",
|
||||
"rustc_version 0.4.1",
|
||||
@@ -6785,9 +6831,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "specta-macros"
|
||||
version = "2.0.0-rc.25"
|
||||
version = "2.0.0-rc.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb"
|
||||
checksum = "b34b2516e16a9ddd3dc4e73cd6454cfe21fe209f8cb5a6f37346fe3774d759b3"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
@@ -6797,9 +6843,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "specta-serde"
|
||||
version = "0.0.12"
|
||||
version = "0.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029"
|
||||
checksum = "4983177ed0eb28235080261953de5f9ebae8a2d107851b5852420bd2ab246245"
|
||||
dependencies = [
|
||||
"specta",
|
||||
"specta-macros",
|
||||
@@ -6807,18 +6853,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "specta-typescript"
|
||||
version = "0.0.12"
|
||||
version = "0.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0"
|
||||
checksum = "3ecbe75dc1d27b36141b50b02b7c73bed00945710cee6edffa86d296a159a6f9"
|
||||
dependencies = [
|
||||
"specta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta-util"
|
||||
version = "0.0.12"
|
||||
version = "0.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502"
|
||||
checksum = "d9bf531215ea70fba8264edecf98f12600462cf359f2038602a47aeda5ecd5b8"
|
||||
dependencies = [
|
||||
"specta",
|
||||
]
|
||||
@@ -6848,7 +6894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
|
||||
dependencies = [
|
||||
"new_debug_unreachable",
|
||||
"parking_lot",
|
||||
"parking_lot 0.12.5",
|
||||
"phf_shared 0.13.1",
|
||||
"precomputed-hash",
|
||||
]
|
||||
@@ -6984,16 +7030,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.39.6"
|
||||
version = "0.40.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
|
||||
checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"objc2-open-directory",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
@@ -7095,7 +7140,7 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
"parking_lot",
|
||||
"parking_lot 0.12.5",
|
||||
"percent-encoding",
|
||||
"raw-window-handle",
|
||||
"tao-macros",
|
||||
@@ -7149,9 +7194,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.12.4"
|
||||
version = "2.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
|
||||
checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -7204,9 +7249,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.7.3"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
|
||||
checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -7227,16 +7272,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.7.3"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
|
||||
checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"brotli",
|
||||
"ico",
|
||||
"json-patch",
|
||||
"plist",
|
||||
"png 0.18.1",
|
||||
"png",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver 1.0.28",
|
||||
@@ -7254,9 +7299,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.7.3"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
|
||||
checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -7302,7 +7347,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"deelevate",
|
||||
"libc",
|
||||
"parking_lot",
|
||||
"parking_lot 0.13.0",
|
||||
"serde",
|
||||
"specta",
|
||||
"sysinfo",
|
||||
@@ -7327,9 +7372,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.6.2"
|
||||
version = "2.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
|
||||
checksum = "ab551d9bec27bb79d1c7fe5ed38c5cfd1cebb86efa745acc65f5853f345f6e56"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"plist",
|
||||
@@ -7348,9 +7393,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-devtools"
|
||||
version = "2.2.2"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1acb4d1b8490ee79166b72c3f8c989e066650a11aa94dfd0d71fb05c61cdbed0"
|
||||
checksum = "299a62101a16c14b914ebfaf529b4b64ad9ca534a0ece1c21fc9308d1e3ba9db"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"bytes",
|
||||
@@ -7375,9 +7420,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.8.3"
|
||||
version = "2.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
|
||||
checksum = "ce0fb9c2b6d34a9720627ed930ebc8af26f61e648695aecaeb03d5c33c5896f5"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
@@ -7481,13 +7526,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.7.0"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
|
||||
checksum = "5bc6c194a95a535abdfbe3bc1af9a43935b8b9b29c216b8e71af8b59132703cf"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
"rand 0.9.5",
|
||||
"rand 0.10.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -7521,21 +7566,11 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74ae3d8549ae564c0397377373e262f340908eb23a5b9fbe03f2a00007dc0038"
|
||||
dependencies = [
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
|
||||
checksum = "7375b25f72f70f6168f27f1ba3a413e30eaa0a2098cff53a4046ebfe9e2f142d"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"libc",
|
||||
@@ -7590,9 +7625,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-wdio-webdriver"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
|
||||
checksum = "5a775b0b59f6f12b38d1f28667311a89cd07f532f540e27d65c6eadfba1156d8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -7663,9 +7698,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.12.2"
|
||||
version = "2.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
|
||||
checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -7673,7 +7708,6 @@ dependencies = [
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"raw-window-handle",
|
||||
"softbuffer",
|
||||
@@ -7690,11 +7724,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-specta"
|
||||
version = "2.0.0-rc.25"
|
||||
version = "2.0.0-rc.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2"
|
||||
checksum = "fe71fc209fa4c49f959644f1ae2d70f72d5675bcb3d3ad8518122fa7b330994e"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"paste",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
@@ -7708,15 +7743,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-specta-macros"
|
||||
version = "2.0.0-rc.25"
|
||||
version = "2.0.0-rc.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012"
|
||||
checksum = "dbf5689a1dbf2611e2351b79ba042ca089d0a3ce45274695e79084cd732a5aec"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.24.1",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7786,10 +7821,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]]
|
||||
@@ -8042,9 +8077,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.3"
|
||||
version = "1.54.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
|
||||
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -8400,7 +8435,7 @@ dependencies = [
|
||||
"flexi_logger",
|
||||
"log",
|
||||
"nu-ansi-term",
|
||||
"parking_lot",
|
||||
"parking_lot 0.12.5",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
@@ -8452,10 +8487,10 @@ dependencies = [
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"png 0.18.1",
|
||||
"png",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8564,7 +8599,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9010,9 +9045,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.9"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -9106,7 +9141,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9127,7 +9162,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
@@ -9702,7 +9737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10034,9 +10069,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.1.3"
|
||||
version = "9.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
|
||||
checksum = "eaeb7f16d2605b49aef586996ded29e65642fab7259a3077e537351df6d80426"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
@@ -10047,6 +10082,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"hmac",
|
||||
"indexmap 2.14.0",
|
||||
"libc",
|
||||
"lzma-rust2",
|
||||
"memchr",
|
||||
"nix",
|
||||
|
||||
+4
-6
@@ -61,21 +61,20 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
|
||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||
tracing = "0.1.44"
|
||||
|
||||
tauri = { version = "2.12.4" }
|
||||
tauri = { version = "2.12.5" }
|
||||
tauri-plugin-clipboard-manager = "2.5.0"
|
||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||
parking_lot = "0.13.0"
|
||||
anyhow = "1.0.104"
|
||||
chrono = "0.4.45"
|
||||
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
||||
tokio = { version = "1.53.3", features = [
|
||||
tokio = { version = "1.54.1", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
flexi_logger = "0.31.10"
|
||||
log = "0.4.34"
|
||||
regex = "1.13.1"
|
||||
log = "0.4.35"
|
||||
reqwest = { version = "0.13.5", features = [
|
||||
"json",
|
||||
"cookies",
|
||||
@@ -90,7 +89,6 @@ smartstring = { version = "1.0.1" }
|
||||
serde = { version = "1.0.229" }
|
||||
serde_json = { version = "1.0.154" }
|
||||
serde_yaml_ng = { version = "0.10.0" }
|
||||
bitflags = { version = "2.13.2" }
|
||||
socket2 = "0.6.5"
|
||||
|
||||
# *** For Windows platform only ***
|
||||
|
||||
+12
-7
@@ -19,19 +19,19 @@
|
||||
scripts-workflow/group_platforms.sh regroups, --check verifies
|
||||
HTML comments are stripped by the update dialog's sanitizer and hidden on
|
||||
GitHub, so they never reach users. -->
|
||||
## v2.5.10
|
||||
## v2.6.0
|
||||
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复全局模式缺少 `GLOBAL` 策略组,无法单独选择全局出口
|
||||
- 修复服务模式下,SSH 节点的 `private-key` 使用私钥文件时内核无法启动
|
||||
- 修复服务模式下,内核更新的 GEO 数据库不会同步回应用目录,导致使用新分类的规则无法通过校验
|
||||
- 修复订阅的 DNS 配置变化后,「DNS 覆写」实际已关闭但开关仍显示开启且没有提示
|
||||
- 修复检测更新后又发布新版时,应用内更新失败
|
||||
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
|
||||
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
|
||||
|
||||
**🖥️ Windows**
|
||||
**🍎 macOS**
|
||||
|
||||
- Windows:修复安装程序重新安装或覆盖时未提示退出运行中应用,导致卸载报错或覆盖失败
|
||||
- 修复 macOS TUN 模式下原有 DNS 被清除、关闭后 DNS 未恢复及联网后设置未生效
|
||||
- 修复 macOS 断网时误报代理已回退直连,恢复联网后自动重试系统代理
|
||||
|
||||
</details>
|
||||
|
||||
@@ -44,5 +44,10 @@
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
- 优化 macOS TUN 模式的系统 DNS,增加阿里和 Cloudflare 地址
|
||||
|
||||
</details>
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
|
||||
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
|
||||
clash-verge-logging = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
@@ -5,14 +5,14 @@ edition = "2024"
|
||||
rust-version = "1.91"
|
||||
|
||||
[dependencies]
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
|
||||
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
# sysinfo 0.38.2 conflicts with dark-light
|
||||
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623
|
||||
sysinfo = { version = "0.39.6", features = ["network", "system"] }
|
||||
sysinfo = { version = "0.40.1", default-features = false, features = ["network", "system"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc = "0.2.191"
|
||||
|
||||
@@ -27,9 +27,9 @@ pub struct SysInfo {
|
||||
impl Default for SysInfo {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let system_name = System::name().unwrap_or_else(|| "Null".into());
|
||||
let system_version = System::long_os_version().unwrap_or_else(|| "Null".into());
|
||||
let system_kernel_version = System::kernel_version().unwrap_or_else(|| "Null".into());
|
||||
let system_name = System::name().unwrap_or_else(|_| "Null".into());
|
||||
let system_version = System::long_os_version().unwrap_or_else(|_| "Null".into());
|
||||
let system_kernel_version = System::kernel_version().unwrap_or_else(|_| "Null".into());
|
||||
let system_arch = System::cpu_arch();
|
||||
Self {
|
||||
system_name,
|
||||
@@ -130,9 +130,14 @@ pub fn current_gid() -> u32 {
|
||||
|
||||
#[inline]
|
||||
pub fn list_network_interfaces() -> Vec<String> {
|
||||
let mut networks = Networks::new();
|
||||
let Ok(mut networks) = Networks::new() else {
|
||||
return Vec::new();
|
||||
};
|
||||
networks.refresh(false);
|
||||
networks.keys().map(|name| name.to_owned()).collect()
|
||||
networks
|
||||
.keys()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
## v2.5.10
|
||||
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复全局模式缺少 `GLOBAL` 策略组,无法单独选择全局出口
|
||||
- 修复服务模式下,SSH 节点的 `private-key` 使用私钥文件时内核无法启动
|
||||
- 修复服务模式下,内核更新的 GEO 数据库不会同步回应用目录,导致使用新分类的规则无法通过校验
|
||||
- 修复订阅的 DNS 配置变化后,「DNS 覆写」实际已关闭但开关仍显示开启且没有提示
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- Windows:修复安装程序重新安装或覆盖时未提示退出运行中应用,导致卸载报错或覆盖失败
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## v2.5.9
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.33**
|
||||
|
||||
+5
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.5.10",
|
||||
"version": "2.6.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"prepare": "husky || true",
|
||||
@@ -56,7 +56,7 @@
|
||||
"@tanstack/react-virtual": "^3.14.13",
|
||||
"@tauri-apps/api": "2.12.4",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "2.8.3",
|
||||
"@tauri-apps/plugin-dialog": "2.8.4",
|
||||
"@tauri-apps/plugin-fs": "2.7.1",
|
||||
"@tauri-apps/plugin-http": "2.8.3",
|
||||
"@tauri-apps/plugin-opener": "2.7.2",
|
||||
@@ -94,7 +94,7 @@
|
||||
"@biomejs/biome": "^2.5.14",
|
||||
"@eslint-react/eslint-plugin": "^5.20.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tauri-apps/cli": "2.12.3",
|
||||
"@tauri-apps/cli": "2.12.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/react": "19.3.0",
|
||||
@@ -102,7 +102,7 @@
|
||||
"@types/validator": "^13.15.10",
|
||||
"@vitejs/plugin-legacy": "^8.2.3",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"@wdio/tauri-service": "1.5.0",
|
||||
"@wdio/tauri-service": "1.5.1",
|
||||
"adm-zip": "^0.6.1",
|
||||
"axios": "^1.20.0",
|
||||
"cli-color": "^2.0.4",
|
||||
@@ -117,7 +117,6 @@
|
||||
"eslint-plugin-react-refresh": "^0.5.7",
|
||||
"eslint-plugin-sukka": "^10.2.0",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"glob": "^14.0.0",
|
||||
"globals": "^17.12.0",
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"husky": "^9.1.7",
|
||||
@@ -146,5 +145,5 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.16.1"
|
||||
"packageManager": "pnpm@12.18.0"
|
||||
}
|
||||
|
||||
Generated
+251
-287
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,27 @@
|
||||
"ghcr.io/devcontainers/features/rust"
|
||||
],
|
||||
"groupName": "Rust toolchain"
|
||||
},
|
||||
{
|
||||
"description": "Update the Specta contract toolchain together",
|
||||
"matchManagers": ["cargo"],
|
||||
"matchPackageNames": ["specta", "tauri-specta", "specta-typescript"],
|
||||
"groupName": "specta",
|
||||
"groupSlug": "specta",
|
||||
"prCreation": "immediate",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Keep Specta aligned with the supported tauri-specta release",
|
||||
"matchManagers": ["cargo"],
|
||||
"matchPackageNames": ["specta", "tauri-specta"],
|
||||
"allowedVersions": "/^2\\.0\\.0-rc\\.26$/"
|
||||
},
|
||||
{
|
||||
"description": "Keep the TypeScript exporter compatible with Specta rc.26",
|
||||
"matchManagers": ["cargo"],
|
||||
"matchPackageNames": ["specta-typescript"],
|
||||
"allowedVersions": "/^0\\.0\\.13$/"
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
|
||||
@@ -345,6 +345,8 @@ function releaseBody(tag, sha, tags, publishing) {
|
||||
.join('\n\n') || 'No new commits.'
|
||||
return `## ${titleFor(tag)}
|
||||
|
||||
@clash-verge-rev/release
|
||||
|
||||
Source: \`dev\` · [${sha}](${url}/commit/${sha}) \n${comparison}
|
||||
${
|
||||
publishing
|
||||
|
||||
@@ -6,7 +6,6 @@ import path from 'node:path'
|
||||
import zlib from 'node:zlib'
|
||||
|
||||
import AdmZip from 'adm-zip'
|
||||
import { glob } from 'glob'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import { extract } from 'tar'
|
||||
|
||||
@@ -266,13 +265,13 @@ async function getLatestReleaseVersion() {
|
||||
}
|
||||
|
||||
if (!META_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`)
|
||||
throw new Error(`mihomo unsupported platform "${platform}-${arch}"`)
|
||||
}
|
||||
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(`clash meta alpha unsupported platform "${platform}-${arch}"`)
|
||||
throw new Error(`mihomo alpha unsupported platform "${platform}-${arch}"`)
|
||||
}
|
||||
|
||||
function clashMetaAlpha() {
|
||||
function mihomoAlpha() {
|
||||
const name = META_ALPHA_MAP[`${platform}-${arch}`]
|
||||
const isWin = platform === 'win32'
|
||||
const urlExt = isWin ? 'zip' : 'gz'
|
||||
@@ -285,7 +284,7 @@ function clashMetaAlpha() {
|
||||
}
|
||||
}
|
||||
|
||||
function clashMeta() {
|
||||
function mihomo() {
|
||||
const name = META_MAP[`${platform}-${arch}`]
|
||||
const isWin = platform === 'win32'
|
||||
const urlExt = isWin ? 'zip' : 'gz'
|
||||
@@ -519,7 +518,7 @@ const resolveServicePermission = async () => {
|
||||
let hasChanges = false
|
||||
|
||||
for (const f of serviceExecutables) {
|
||||
const files = glob.sync(path.join(SERVICE_DIR, f))
|
||||
const files = fs.globSync(path.join(SERVICE_DIR, f))
|
||||
for (const filePath of files) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const currentHash = await calculateFileHash(filePath)
|
||||
@@ -733,13 +732,12 @@ const tasks = [
|
||||
{
|
||||
name: 'verge-mihomo-alpha',
|
||||
func: () =>
|
||||
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
||||
getLatestAlphaVersion().then(() => resolveSidecar(mihomoAlpha())),
|
||||
retry: 5,
|
||||
},
|
||||
{
|
||||
name: 'verge-mihomo',
|
||||
func: () =>
|
||||
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
|
||||
func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())),
|
||||
retry: 5,
|
||||
},
|
||||
// After both sidecar tasks: it hashes what they downloaded.
|
||||
|
||||
+50
-17
@@ -32,7 +32,9 @@ function is_valid_ip() {
|
||||
|
||||
# Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent.
|
||||
function sc_value() {
|
||||
printf 'show %s\nquit\n' "$1" | scutil | sed -n "s/^ *$2 : //p"
|
||||
local dictionary
|
||||
dictionary=$(printf 'show %s\nquit\n' "$1" | scutil) || return 1
|
||||
sed -n "s/^ *$2 : //p" <<<"$dictionary"
|
||||
}
|
||||
|
||||
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
|
||||
@@ -50,31 +52,62 @@ function dns_of() {
|
||||
echo "$dns"
|
||||
}
|
||||
|
||||
[ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1
|
||||
! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1
|
||||
[ $# -lt 1 ] && echo "Usage: $0 <newline-separated DNS servers> [legacy state directory]" >&2 && exit 1
|
||||
dns_servers=()
|
||||
while read -r server; do
|
||||
! is_valid_ip "$server" && echo "$server is not a valid IP address." >&2 && exit 1
|
||||
dns_servers+=("$server")
|
||||
done <<<"$1"
|
||||
|
||||
# Backups are keyed by service ID, which survives renaming the service and renumbering its interface.
|
||||
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
|
||||
hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName)
|
||||
[ -z "$hardware_port" ] && echo "No primary network service." && exit 1
|
||||
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService) || exit 1
|
||||
hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName) || exit 1
|
||||
[ -z "$hardware_port" ] && echo "No primary network service; waiting for the network." >&2 && exit 75
|
||||
|
||||
original_dns=$(dns_of "$hardware_port") || { echo "Cannot read DNS servers of $hardware_port." >&2; exit 1; }
|
||||
|
||||
backup=".original_dns/$service_id"
|
||||
mkdir -p .original_dns || exit 1
|
||||
# Older releases kept a single backup for whichever service was primary.
|
||||
if [ -f .original_dns.txt ]; then
|
||||
if [ -f "$backup" ]; then
|
||||
rm -f .original_dns.txt || exit 1
|
||||
else
|
||||
mv .original_dns.txt "$backup" || exit 1
|
||||
# Key old single-slot backups before import so a later network switch cannot reassign them.
|
||||
for state_dir in "$PWD" "${2:-$PWD}"; do
|
||||
if [ -f "$state_dir/.original_dns.txt" ]; then
|
||||
mkdir -p "$state_dir/.original_dns" || exit 1
|
||||
if [ -f "$state_dir/.original_dns/$service_id" ]; then
|
||||
rm -f "$state_dir/.original_dns.txt" || exit 1
|
||||
else
|
||||
mv "$state_dir/.original_dns.txt" "$state_dir/.original_dns/$service_id" || exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ ! -f "$backup" ] && [ -n "$2" ] && [ -f "$2/$backup" ]; then
|
||||
cp "$2/$backup" ".original_dns/.$service_id" || exit 1
|
||||
mv ".original_dns/.$service_id" "$backup" || exit 1
|
||||
fi
|
||||
backup_header='# clash-verge-dns-managed'
|
||||
function save_backup() {
|
||||
printf '%s\n%s\n' "$backup_header" "$1" >".original_dns/.$service_id" &&
|
||||
mv ".original_dns/.$service_id" "$backup"
|
||||
}
|
||||
|
||||
saved_dns=
|
||||
legacy_backup=false
|
||||
if [ -f "$backup" ]; then
|
||||
saved_dns=$(cat "$backup") || exit 1
|
||||
case "$saved_dns" in
|
||||
"$backup_header"$'\n'*) saved_dns=${saved_dns#*$'\n'} ;;
|
||||
*) legacy_backup=true ;;
|
||||
esac
|
||||
fi
|
||||
# The placeholder is ours, not the user's: keep the backup taken before it was applied.
|
||||
if [ "$original_dns" != "$1" ]; then
|
||||
echo "$original_dns" >"$backup" || exit 1
|
||||
networksetup -setdnsservers "$hardware_port" "$1"
|
||||
# Only an unmarked historical backup owns the old single-address placeholder.
|
||||
if [ "$original_dns" != "114.114.114.114" ] || [ "$legacy_backup" != true ]; then
|
||||
save_backup "$original_dns" || exit 1
|
||||
legacy_backup=false
|
||||
fi
|
||||
networksetup -setdnsservers "$hardware_port" "${dns_servers[@]}"
|
||||
[ "$(dns_of "$hardware_port")" = "$1" ] || { echo "Failed to set DNS for $hardware_port." >&2; exit 1; }
|
||||
elif [ ! -f "$backup" ]; then
|
||||
echo "empty" >"$backup" || exit 1
|
||||
fi
|
||||
# Keep legacy ownership until the new list is verified, including a retry after a failed marker write.
|
||||
if [ "$legacy_backup" = true ]; then
|
||||
save_backup "$saved_dns" || exit 1
|
||||
fi
|
||||
|
||||
+64
-35
@@ -1,9 +1,11 @@
|
||||
#!/bin/bash
|
||||
[ $# -lt 1 ] && echo "Usage: $0 <placeholder IP address>" && exit 1
|
||||
[ $# -lt 1 ] && echo "Usage: $0 <newline-separated DNS servers> [legacy state directory]" >&2 && exit 1
|
||||
|
||||
# Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent.
|
||||
function sc_value() {
|
||||
printf 'show %s\nquit\n' "$1" | scutil | sed -n "s/^ *$2 : //p"
|
||||
local dictionary
|
||||
dictionary=$(printf 'show %s\nquit\n' "$1" | scutil) || return 1
|
||||
sed -n "s/^ *$2 : //p" <<<"$dictionary"
|
||||
}
|
||||
|
||||
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
|
||||
@@ -21,42 +23,69 @@ function dns_of() {
|
||||
echo "$dns"
|
||||
}
|
||||
|
||||
result=0
|
||||
# Older releases kept a single backup for whichever service was primary.
|
||||
if [ -f .original_dns.txt ]; then
|
||||
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
|
||||
if [ -z "$service_id" ] || ! mkdir -p .original_dns; then
|
||||
result=1
|
||||
elif [ -f ".original_dns/$service_id" ]; then
|
||||
rm -f .original_dns.txt || result=1
|
||||
elif ! mv .original_dns.txt ".original_dns/$service_id"; then
|
||||
result=1
|
||||
fi
|
||||
fi
|
||||
backup_header='# clash-verge-dns-managed'
|
||||
completed_backups=()
|
||||
|
||||
for backup in .original_dns/*; do
|
||||
[ -f "$backup" ] || continue
|
||||
hardware_port=$(sc_value "Setup:/Network/Service/${backup##*/}" UserDefinedName)
|
||||
# Kept for a service that is gone: there is nothing to write it back to.
|
||||
[ -n "$hardware_port" ] || continue
|
||||
if ! current_dns=$(dns_of "$hardware_port"); then
|
||||
echo "Cannot read DNS servers of $hardware_port." >&2
|
||||
result=1
|
||||
continue
|
||||
function restore_dns() {
|
||||
local result=0
|
||||
# Older releases kept a single backup for whichever service was primary.
|
||||
if [ -f .original_dns.txt ]; then
|
||||
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
|
||||
if [ -z "$service_id" ] || ! mkdir -p .original_dns; then
|
||||
result=1
|
||||
elif [ -f ".original_dns/$service_id" ]; then
|
||||
rm -f .original_dns.txt || result=1
|
||||
elif ! mv .original_dns.txt ".original_dns/$service_id"; then
|
||||
result=1
|
||||
fi
|
||||
fi
|
||||
# A service no longer on the placeholder was changed by someone else, and that change stands.
|
||||
if [ "$current_dns" = "$1" ]; then
|
||||
original_dns=$(cat "$backup") || { result=1; continue; }
|
||||
# Older releases could back up the placeholder itself.
|
||||
[ "$original_dns" = "$1" ] && original_dns=empty
|
||||
networksetup -setdnsservers "$hardware_port" $original_dns
|
||||
if [ "$(dns_of "$hardware_port")" != "$original_dns" ]; then
|
||||
echo "Failed to restore DNS for $hardware_port." >&2
|
||||
|
||||
for backup in .original_dns/*; do
|
||||
[ -f "$backup" ] || continue
|
||||
# Current backups take precedence even after their DNS has already been restored.
|
||||
if [ "$PWD" != "$state_dir" ] && [ -f "$state_dir/$backup" ]; then
|
||||
completed_backups+=("$PWD/$backup")
|
||||
continue
|
||||
fi
|
||||
hardware_port=$(sc_value "Setup:/Network/Service/${backup##*/}" UserDefinedName) || { result=1; continue; }
|
||||
# Kept for a service that is gone: there is nothing to write it back to.
|
||||
[ -n "$hardware_port" ] || continue
|
||||
if ! current_dns=$(dns_of "$hardware_port"); then
|
||||
echo "Cannot read DNS servers of $hardware_port." >&2
|
||||
result=1
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
rm -f "$backup" || result=1
|
||||
done
|
||||
rmdir .original_dns 2>/dev/null
|
||||
original_dns=$(cat "$backup") || { result=1; continue; }
|
||||
legacy_backup=false
|
||||
case "$original_dns" in
|
||||
"$backup_header"$'\n'*) original_dns=${original_dns#*$'\n'} ;;
|
||||
*) legacy_backup=true ;;
|
||||
esac
|
||||
# A service no longer on its recorded placeholder was changed by someone else.
|
||||
if [ "$current_dns" = "$1" ] || { [ "$legacy_backup" = true ] && [ "$current_dns" = "114.114.114.114" ]; }; then
|
||||
networksetup -setdnsservers "$hardware_port" $original_dns
|
||||
if [ "$(dns_of "$hardware_port")" != "$original_dns" ]; then
|
||||
echo "Failed to restore DNS for $hardware_port." >&2
|
||||
result=1
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
completed_backups+=("$PWD/$backup")
|
||||
done
|
||||
return $result
|
||||
}
|
||||
|
||||
result=0
|
||||
state_dir=$PWD
|
||||
restore_dns "$1" || result=1
|
||||
if [ -n "$2" ] && [ "$2" != "$PWD" ]; then
|
||||
{ cd "$2" && restore_dns "$1"; } || result=1
|
||||
fi
|
||||
# Remove superseded legacy backups first; a cleanup failure must leave current authority intact.
|
||||
if [ "$result" -eq 0 ]; then
|
||||
for ((index=${#completed_backups[@]}-1; index>=0; index--)); do
|
||||
rm -f "${completed_backups[index]}" || { result=1; break; }
|
||||
done
|
||||
fi
|
||||
rmdir "$state_dir/.original_dns" "${2:-$state_dir}/.original_dns" 2>/dev/null
|
||||
exit $result
|
||||
|
||||
+16
-22
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.5.10"
|
||||
version = "2.6.0"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.7.3", features = [] }
|
||||
tauri-build = { version = "2.7.4", features = [] }
|
||||
|
||||
[dependencies]
|
||||
clash-verge-draft = { workspace = true }
|
||||
@@ -55,11 +55,10 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
smartstring = { workspace = true, features = ["serde"] }
|
||||
bitflags = { workspace = true }
|
||||
socket2 = { workspace = true }
|
||||
warp = { version = "0.4.3", features = ["server"] }
|
||||
open = "5.4.4"
|
||||
rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] }
|
||||
open = "5.4.5"
|
||||
rfd = { version = "0.17.2", default-features = false, features = ["common-controls-v6"] }
|
||||
dirs = "7.0.0"
|
||||
dunce = "1.0.5"
|
||||
nanoid = "0.5"
|
||||
@@ -67,50 +66,46 @@ chrono = { workspace = true }
|
||||
boa_engine = "0.22.0"
|
||||
percent-encoding = "2.3.2"
|
||||
reqwest = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch = "main", features = [
|
||||
"guard",
|
||||
] }
|
||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.5.0"
|
||||
tauri-plugin-shell = "2.6.0"
|
||||
tauri-plugin-opener = "2.7.2"
|
||||
tauri-plugin-dialog = "2.8.3"
|
||||
tauri-plugin-dialog = "2.8.4"
|
||||
tauri-plugin-fs = "2.7.1"
|
||||
tauri-plugin-process = "2.4.0"
|
||||
tauri-plugin-deep-link = "2.6.2"
|
||||
tauri-plugin-deep-link = "2.6.3"
|
||||
tauri-plugin-window-state = "2.5.0"
|
||||
zip = "9.1.3"
|
||||
zip = "9.3.0"
|
||||
flate2 = "1.1.10"
|
||||
reqwest_dav = "0.3.3"
|
||||
aes-gcm = { version = "0.11.1" }
|
||||
base64 = "0.23.1"
|
||||
minisign-verify = "0.3.0"
|
||||
sha2 = "0.11.1"
|
||||
getrandom = "0.4.3"
|
||||
futures = "0.3.34"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.7.0"
|
||||
tokio-stream = "0.1.19"
|
||||
tauri-plugin-notification = "2.8.0"
|
||||
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||
tauri-plugin-http = "2.8.3"
|
||||
tracing = { workspace = true }
|
||||
tauri-plugin-devtools = { version = "2.2.2", optional = true }
|
||||
tauri-plugin-devtools = { version = "2.2.3", optional = true }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
||||
async-trait = "0.1.92"
|
||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
|
||||
"client",
|
||||
] }
|
||||
arc-swap = "1.9.2"
|
||||
arc-swap = "1.9.3"
|
||||
tokio-rustls = "0.26.6"
|
||||
rustls = { version = "0.23.46", features = ["ring"] }
|
||||
webpki-roots = "1.0.9"
|
||||
webpki-roots = "1.0.11"
|
||||
dark-light = "3.0.0"
|
||||
bytes = "1.12.1"
|
||||
tokio-util = { version = "0.7.20", features = ["time"] }
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
|
||||
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
|
||||
specta-typescript = "=0.0.12"
|
||||
specta = { version = "=2.0.0-rc.26", features = ["derive", "function", "serde_json"] }
|
||||
tauri-specta = { version = "=2.0.0-rc.26", features = ["derive", "typescript"] }
|
||||
specta-typescript = "=0.0.13"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.191"
|
||||
@@ -120,7 +115,7 @@ libc = "0.2.191"
|
||||
clash-verge-signal = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true }
|
||||
tauri-plugin-wdio-webdriver = { version = "=1.5.1", optional = true }
|
||||
objc2 = "0.6.5"
|
||||
objc2-foundation = { version = "0.3.2", features = [
|
||||
"NSString",
|
||||
@@ -157,7 +152,6 @@ tauri-plugin-global-shortcut = "2.4.1"
|
||||
tauri-plugin-updater = "2.14.1"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
warp = { version = "0.4.3", features = ["test"] }
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
"global-shortcut:allow-register-all",
|
||||
"global-shortcut:allow-unregister",
|
||||
"global-shortcut:allow-unregister-all",
|
||||
"process:allow-exit",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"dialog:default"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
core::{SilentUpdater, updater::DownloadEvent},
|
||||
core::{
|
||||
SilentUpdater,
|
||||
updater::{DownloadEvent, InstallUpdateOutcome},
|
||||
},
|
||||
feat,
|
||||
utils::dirs,
|
||||
};
|
||||
@@ -61,8 +64,7 @@ pub async fn install_update(
|
||||
app_handle: AppHandle,
|
||||
version: std::string::String,
|
||||
on_event: Channel<DownloadEvent>,
|
||||
) -> CmdResult<bool> {
|
||||
let version: String = version.into();
|
||||
) -> CmdResult<InstallUpdateOutcome> {
|
||||
SilentUpdater::global()
|
||||
.install_update(&app_handle, &version, |event| {
|
||||
let _ = on_event.send(event);
|
||||
|
||||
@@ -184,84 +184,41 @@ impl IClashTemp {
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn guard_redir_port(config: &Mapping) -> u16 {
|
||||
fn guard_port_value(config: &Mapping, key: &str, default: u16) -> u16 {
|
||||
let mut port = config
|
||||
.get("redir-port")
|
||||
.get(key)
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7895);
|
||||
.unwrap_or(default);
|
||||
if port == 0 {
|
||||
port = 7895;
|
||||
port = default;
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn guard_redir_port(config: &Mapping) -> u16 {
|
||||
Self::guard_port_value(config, "redir-port", network::ports::DEFAULT_REDIR)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn guard_tproxy_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get("tproxy-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(network::ports::DEFAULT_TPROXY);
|
||||
if port == 0 {
|
||||
port = network::ports::DEFAULT_TPROXY;
|
||||
}
|
||||
port
|
||||
Self::guard_port_value(config, "tproxy-port", network::ports::DEFAULT_TPROXY)
|
||||
}
|
||||
|
||||
fn guard_mixed_port(config: &Mapping) -> u16 {
|
||||
let raw_value = config.get("mixed-port");
|
||||
|
||||
let mut port = raw_value
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(network::ports::DEFAULT_MIXED);
|
||||
|
||||
if port == 0 {
|
||||
port = network::ports::DEFAULT_MIXED;
|
||||
}
|
||||
|
||||
port
|
||||
Self::guard_port_value(config, "mixed-port", network::ports::DEFAULT_MIXED)
|
||||
}
|
||||
|
||||
fn guard_socks_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get("socks-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7898);
|
||||
if port == 0 {
|
||||
port = 7898;
|
||||
}
|
||||
port
|
||||
Self::guard_port_value(config, "socks-port", network::ports::DEFAULT_SOCKS)
|
||||
}
|
||||
|
||||
fn guard_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get("port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7899);
|
||||
if port == 0 {
|
||||
port = 7899;
|
||||
}
|
||||
port
|
||||
Self::guard_port_value(config, "port", network::ports::DEFAULT_HTTP)
|
||||
}
|
||||
|
||||
pub(super) fn guard_server_ctrl(config: &Mapping) -> String {
|
||||
|
||||
@@ -14,6 +14,15 @@ mod verge;
|
||||
pub(crate) use self::config::Config;
|
||||
pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*};
|
||||
|
||||
macro_rules! patch_opt_field {
|
||||
($lv:expr, $rv:expr, $key:tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key.clone();
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use patch_opt_field;
|
||||
|
||||
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
||||
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||
}
|
||||
|
||||
+71
-108
@@ -1,3 +1,5 @@
|
||||
use super::patch_opt_field;
|
||||
|
||||
use crate::{
|
||||
config::profiles,
|
||||
utils::{
|
||||
@@ -131,19 +133,18 @@ impl PrfOption {
|
||||
match (one, other) {
|
||||
(Some(a_ref), Some(b_ref)) => {
|
||||
let mut result = a_ref.clone();
|
||||
result.user_agent = b_ref.user_agent.clone().or(result.user_agent);
|
||||
result.with_proxy = b_ref.with_proxy.or(result.with_proxy);
|
||||
result.self_proxy = b_ref.self_proxy.or(result.self_proxy);
|
||||
result.danger_accept_invalid_certs =
|
||||
b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs);
|
||||
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update);
|
||||
result.update_interval = b_ref.update_interval.or(result.update_interval);
|
||||
result.merge = b_ref.merge.clone().or(result.merge);
|
||||
result.script = b_ref.script.clone().or(result.script);
|
||||
result.rules = b_ref.rules.clone().or(result.rules);
|
||||
result.proxies = b_ref.proxies.clone().or(result.proxies);
|
||||
result.groups = b_ref.groups.clone().or(result.groups);
|
||||
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
|
||||
patch_opt_field!(result, b_ref, user_agent);
|
||||
patch_opt_field!(result, b_ref, with_proxy);
|
||||
patch_opt_field!(result, b_ref, self_proxy);
|
||||
patch_opt_field!(result, b_ref, danger_accept_invalid_certs);
|
||||
patch_opt_field!(result, b_ref, allow_auto_update);
|
||||
patch_opt_field!(result, b_ref, update_interval);
|
||||
patch_opt_field!(result, b_ref, merge);
|
||||
patch_opt_field!(result, b_ref, script);
|
||||
patch_opt_field!(result, b_ref, rules);
|
||||
patch_opt_field!(result, b_ref, proxies);
|
||||
patch_opt_field!(result, b_ref, groups);
|
||||
patch_opt_field!(result, b_ref, timeout_seconds);
|
||||
Some(result)
|
||||
}
|
||||
(Some(a_ref), None) => Some(a_ref.clone()),
|
||||
@@ -181,6 +182,38 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
async fn append_companion_item(item: &mut Self) -> Result<String> {
|
||||
profiles::profiles_append_item_safe(item).await?;
|
||||
item.uid
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("appended companion item has no uid"))
|
||||
}
|
||||
|
||||
async fn ensure_companion_items(
|
||||
merge: &mut Option<String>,
|
||||
script: &mut Option<String>,
|
||||
rules: &mut Option<String>,
|
||||
proxies: &mut Option<String>,
|
||||
groups: &mut Option<String>,
|
||||
) -> Result<()> {
|
||||
if merge.is_none() {
|
||||
*merge = Some(Self::append_companion_item(&mut Self::from_merge(None)).await?);
|
||||
}
|
||||
if script.is_none() {
|
||||
*script = Some(Self::append_companion_item(&mut Self::from_script(None)).await?);
|
||||
}
|
||||
if rules.is_none() {
|
||||
*rules = Some(Self::append_companion_item(&mut Self::from_rules()).await?);
|
||||
}
|
||||
if proxies.is_none() {
|
||||
*proxies = Some(Self::append_companion_item(&mut Self::from_proxies()).await?);
|
||||
}
|
||||
if groups.is_none() {
|
||||
*groups = Some(Self::append_companion_item(&mut Self::from_groups()).await?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn from_local(
|
||||
name: String,
|
||||
desc: String,
|
||||
@@ -197,31 +230,7 @@ impl PrfItem {
|
||||
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut Self::from_merge(None);
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut Self::from_script(None);
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut Self::from_rules();
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut Self::from_proxies();
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut Self::from_groups();
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("local".into()),
|
||||
@@ -375,31 +384,7 @@ impl PrfItem {
|
||||
bail!("profile does not contain `proxies` or `proxy-providers`");
|
||||
}
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut Self::from_merge(None);
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut Self::from_script(None);
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut Self::from_rules();
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut Self::from_proxies();
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut Self::from_groups();
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
|
||||
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
@@ -461,46 +446,30 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn from_rules() -> Self {
|
||||
let uid = help::get_uid("r").into();
|
||||
fn new_seq_item(prefix: &str, itype: &str, template: &str) -> Self {
|
||||
let uid = help::get_uid(prefix).into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("rules".into()),
|
||||
itype: Some(itype.into()),
|
||||
file: Some(file),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_RULES.into()),
|
||||
file_data: Some(template.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn from_rules() -> Self {
|
||||
Self::new_seq_item("r", "rules", tmpl::ITEM_RULES)
|
||||
}
|
||||
|
||||
fn from_proxies() -> Self {
|
||||
let uid = help::get_uid("p").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("proxies".into()),
|
||||
file: Some(file),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_PROXIES.into()),
|
||||
..Default::default()
|
||||
}
|
||||
Self::new_seq_item("p", "proxies", tmpl::ITEM_PROXIES)
|
||||
}
|
||||
|
||||
fn from_groups() -> Self {
|
||||
let uid = help::get_uid("g").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("groups".into()),
|
||||
file: Some(file),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_GROUPS.into()),
|
||||
..Default::default()
|
||||
}
|
||||
Self::new_seq_item("g", "groups", tmpl::ITEM_GROUPS)
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file(&self) -> Result<String> {
|
||||
@@ -516,26 +485,20 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! current_field {
|
||||
($name:ident, $field:ident) => {
|
||||
pub(crate) fn $name(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.$field.as_ref())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl PrfItem {
|
||||
pub(crate) fn current_merge(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.merge.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_script(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.script.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_rules(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.rules.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_proxies(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.proxies.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_groups(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.groups.as_ref())
|
||||
}
|
||||
current_field!(current_merge, merge);
|
||||
current_field!(current_script, script);
|
||||
current_field!(current_rules, rules);
|
||||
current_field!(current_proxies, proxies);
|
||||
current_field!(current_groups, groups);
|
||||
}
|
||||
|
||||
fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{
|
||||
PrfOption,
|
||||
PrfOption, patch_opt_field,
|
||||
prfitem::{PrfItem, PrfSelected, normalize_profile_home_url},
|
||||
};
|
||||
use crate::core::notify::{Refresh, announce};
|
||||
@@ -86,14 +86,6 @@ impl ProfileDeletePlan {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! patch {
|
||||
($lv: expr, $rv: expr, $key: tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key.to_owned();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl IProfiles {
|
||||
fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<&str>) -> Option<String> {
|
||||
let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?;
|
||||
@@ -244,15 +236,15 @@ impl IProfiles {
|
||||
|
||||
for each in items.iter_mut() {
|
||||
if each.uid.as_deref() == Some(uid) {
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
patch!(each, item, desc);
|
||||
patch!(each, item, file);
|
||||
patch!(each, item, url);
|
||||
patch!(each, item, selected);
|
||||
patch!(each, item, extra);
|
||||
patch!(each, item, updated);
|
||||
patch!(each, item, option);
|
||||
patch_opt_field!(each, item, itype);
|
||||
patch_opt_field!(each, item, name);
|
||||
patch_opt_field!(each, item, desc);
|
||||
patch_opt_field!(each, item, file);
|
||||
patch_opt_field!(each, item, url);
|
||||
patch_opt_field!(each, item, selected);
|
||||
patch_opt_field!(each, item, extra);
|
||||
patch_opt_field!(each, item, updated);
|
||||
patch_opt_field!(each, item, option);
|
||||
|
||||
self.items = Some(items);
|
||||
return self.save_file().await;
|
||||
@@ -420,6 +412,21 @@ pub(crate) async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file
|
||||
profiles_append_item_safe(item).await
|
||||
}
|
||||
|
||||
/// Applies `edit` to the committed profiles; commits only when the edit succeeds.
|
||||
async fn with_profiles_edit<F, Fut>(edit: F) -> Result<()>
|
||||
where
|
||||
F: FnOnce(IProfiles) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<IProfiles>> + Send,
|
||||
{
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|profiles| async move {
|
||||
let profiles = edit(profiles).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
|
||||
let profiles = Config::profiles().await;
|
||||
profiles_append_item_to_safe(&profiles, item).await
|
||||
@@ -435,43 +442,19 @@ pub(super) async fn profiles_append_item_to_safe(profiles: &Draft<IProfiles>, it
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.patch_item(index, item).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
with_profiles_edit(|mut profiles| async move { profiles.patch_item(index, item).await.map(|_| profiles) }).await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.reorder(active_id, over_id).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
with_profiles_edit(|mut profiles| async move { profiles.reorder(active_id, over_id).await.map(|_| profiles) }).await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_save_file_safe() -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|profiles| async move {
|
||||
profiles.save_file().await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
with_profiles_edit(|profiles| async move { profiles.save_file().await.map(|_| profiles) }).await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.update_item(index, item).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
with_profiles_edit(|mut profiles| async move { profiles.update_item(index, item).await.map(|_| profiles) }).await
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -631,6 +614,53 @@ async fn select_node_with_timeout(group_name: &String, node: &String) -> Result<
|
||||
.with_context(|| format!("failed to select node [{node}] for group [{group_name}]"))
|
||||
}
|
||||
|
||||
// mihomo lists these first in GLOBAL, so an unpicked GLOBAL starts on DIRECT.
|
||||
const GLOBAL_BUILTINS: [&str; 2] = ["DIRECT", "REJECT"];
|
||||
|
||||
/// GLOBAL's first visible group, else its first node, while GLOBAL is still on a built-in.
|
||||
fn global_exit_target(proxies: &Proxies) -> Option<&str> {
|
||||
let global = proxies.proxies.get("GLOBAL")?;
|
||||
if !global.now.as_deref().is_some_and(|now| GLOBAL_BUILTINS.contains(&now)) {
|
||||
return None;
|
||||
}
|
||||
let members = global.all.as_deref()?;
|
||||
let is_visible_group = |name: &str| {
|
||||
proxies.proxies.get(name).is_some_and(|proxy| {
|
||||
proxy.hidden != Some(true)
|
||||
&& matches!(
|
||||
proxy.proxy_type,
|
||||
ProxyType::Selector | ProxyType::URLTest | ProxyType::Fallback | ProxyType::LoadBalance
|
||||
)
|
||||
})
|
||||
};
|
||||
members
|
||||
.iter()
|
||||
.find(|name| is_visible_group(name))
|
||||
.or_else(|| members.iter().find(|name| !GLOBAL_BUILTINS.contains(&name.as_str())))
|
||||
.map(|name| name.as_str())
|
||||
}
|
||||
|
||||
/// Moves GLOBAL off a built-in unless the profile records any GLOBAL choice.
|
||||
pub(crate) async fn leave_default_global_exit() -> Result<()> {
|
||||
let profiles = Config::profiles().await.data_arc();
|
||||
let has_record = profiles
|
||||
.current
|
||||
.as_ref()
|
||||
.and_then(|current| profiles.get_item(current).ok())
|
||||
.and_then(|item| item.selected.as_deref())
|
||||
.is_some_and(|selected| selected.iter().any(|item| item.name.as_deref() == Some("GLOBAL")));
|
||||
if has_record {
|
||||
return Ok(());
|
||||
}
|
||||
let snapshot = fetch_proxies_with_timeout().await?;
|
||||
let Some(target) = global_exit_target(&snapshot) else {
|
||||
return Ok(());
|
||||
};
|
||||
select_node_with_timeout(&String::from("GLOBAL"), &String::from(target)).await?;
|
||||
logging!(info, Type::Config, "moved GLOBAL off its built-in default to {target}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remaining_activations(
|
||||
activations: &[(String, String)],
|
||||
completed: &HashMap<String, String>,
|
||||
@@ -713,19 +743,21 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_item_mut(profiles: &mut IProfiles) -> Option<&mut PrfItem> {
|
||||
let current = profiles.current.clone()?;
|
||||
profiles
|
||||
.items
|
||||
.as_mut()?
|
||||
.iter_mut()
|
||||
.find(|item| item.uid.as_ref() == Some(¤t))
|
||||
}
|
||||
|
||||
/// Records a backend-made selection so the next core start restores it.
|
||||
pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> {
|
||||
let group_name = String::from(group_name);
|
||||
let node = String::from(node);
|
||||
persist_selected_update(true, move |mut profiles| async move {
|
||||
let Some(current) = profiles.current.clone() else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
let Some(item) = profiles
|
||||
.items
|
||||
.as_mut()
|
||||
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
||||
else {
|
||||
let Some(item) = current_item_mut(&mut profiles) else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
|
||||
@@ -760,14 +792,7 @@ fn remove_selected_node(selected: &mut Vec<PrfSelected>, group_name: &str) -> bo
|
||||
|
||||
pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> {
|
||||
persist_selected_update(true, move |mut profiles| async move {
|
||||
let Some(current) = profiles.current.clone() else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
let Some(item) = profiles
|
||||
.items
|
||||
.as_mut()
|
||||
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
||||
else {
|
||||
let Some(item) = current_item_mut(&mut profiles) else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
|
||||
@@ -1041,6 +1066,12 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
|
||||
if Config::clash().await.latest_arc().get_mode().as_deref() == Some("global")
|
||||
&& let Err(err) = leave_default_global_exit().await
|
||||
{
|
||||
logging!(warn, Type::Config, "failed to move GLOBAL off its default: {err:#}");
|
||||
}
|
||||
|
||||
if selected.is_empty() {
|
||||
if is_activation_current(generation) {
|
||||
announce(Refresh::Clash);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, patch_opt_field, serialize_encrypted},
|
||||
constants::network,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
@@ -39,7 +39,7 @@ pub struct IVerge {
|
||||
|
||||
pub traffic_graph: Option<bool>,
|
||||
|
||||
/// show memory info (only for Clash Meta)
|
||||
/// show memory info (only for Mihomo)
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -432,98 +432,90 @@ impl IVerge {
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn patch_config(&mut self, patch: &Self) {
|
||||
macro_rules! patch {
|
||||
($key: tt) => {
|
||||
if patch.$key.is_some() {
|
||||
self.$key = patch.$key.clone();
|
||||
}
|
||||
};
|
||||
}
|
||||
patch_opt_field!(self, patch, app_log_level);
|
||||
patch_opt_field!(self, patch, app_log_max_size);
|
||||
patch_opt_field!(self, patch, app_log_max_count);
|
||||
|
||||
patch!(app_log_level);
|
||||
patch!(app_log_max_size);
|
||||
patch!(app_log_max_count);
|
||||
|
||||
patch!(language);
|
||||
patch!(theme_mode);
|
||||
patch!(tray_event);
|
||||
patch!(env_type);
|
||||
patch!(start_page);
|
||||
patch!(startup_script);
|
||||
patch!(traffic_graph);
|
||||
patch!(enable_memory_usage);
|
||||
patch!(enable_group_icon);
|
||||
patch!(pause_render_traffic_stats_on_blur);
|
||||
patch_opt_field!(self, patch, language);
|
||||
patch_opt_field!(self, patch, theme_mode);
|
||||
patch_opt_field!(self, patch, tray_event);
|
||||
patch_opt_field!(self, patch, env_type);
|
||||
patch_opt_field!(self, patch, start_page);
|
||||
patch_opt_field!(self, patch, startup_script);
|
||||
patch_opt_field!(self, patch, traffic_graph);
|
||||
patch_opt_field!(self, patch, enable_memory_usage);
|
||||
patch_opt_field!(self, patch, enable_group_icon);
|
||||
patch_opt_field!(self, patch, pause_render_traffic_stats_on_blur);
|
||||
#[cfg(target_os = "macos")]
|
||||
patch!(tray_icon);
|
||||
patch!(menu_icon);
|
||||
patch!(menu_order);
|
||||
patch!(proxy_group_tools_position);
|
||||
patch!(notice_position);
|
||||
patch!(collapse_navbar);
|
||||
patch!(common_tray_icon);
|
||||
patch!(sysproxy_tray_icon);
|
||||
patch!(tun_tray_icon);
|
||||
patch_opt_field!(self, patch, tray_icon);
|
||||
patch_opt_field!(self, patch, menu_icon);
|
||||
patch_opt_field!(self, patch, menu_order);
|
||||
patch_opt_field!(self, patch, proxy_group_tools_position);
|
||||
patch_opt_field!(self, patch, notice_position);
|
||||
patch_opt_field!(self, patch, collapse_navbar);
|
||||
patch_opt_field!(self, patch, common_tray_icon);
|
||||
patch_opt_field!(self, patch, sysproxy_tray_icon);
|
||||
patch_opt_field!(self, patch, tun_tray_icon);
|
||||
|
||||
patch!(enable_tun_mode);
|
||||
patch!(enable_auto_launch);
|
||||
patch!(enable_silent_start);
|
||||
patch!(enable_hover_jump_navigator);
|
||||
patch!(hover_jump_navigator_delay);
|
||||
patch_opt_field!(self, patch, enable_tun_mode);
|
||||
patch_opt_field!(self, patch, enable_auto_launch);
|
||||
patch_opt_field!(self, patch, enable_silent_start);
|
||||
patch_opt_field!(self, patch, enable_hover_jump_navigator);
|
||||
patch_opt_field!(self, patch, hover_jump_navigator_delay);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
patch!(verge_redir_port);
|
||||
patch_opt_field!(self, patch, verge_redir_port);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
patch!(verge_redir_enabled);
|
||||
patch_opt_field!(self, patch, verge_redir_enabled);
|
||||
#[cfg(target_os = "linux")]
|
||||
patch!(verge_tproxy_port);
|
||||
patch_opt_field!(self, patch, verge_tproxy_port);
|
||||
#[cfg(target_os = "linux")]
|
||||
patch!(verge_tproxy_enabled);
|
||||
patch!(verge_mixed_port);
|
||||
patch!(verge_socks_port);
|
||||
patch!(verge_socks_enabled);
|
||||
patch!(verge_port);
|
||||
patch!(verge_http_enabled);
|
||||
patch!(enable_system_proxy);
|
||||
patch!(enable_proxy_guard);
|
||||
patch!(enable_bypass_check);
|
||||
patch!(use_default_bypass);
|
||||
patch!(system_proxy_bypass);
|
||||
patch!(proxy_guard_duration);
|
||||
patch!(proxy_auto_config);
|
||||
patch!(pac_file_content);
|
||||
patch!(proxy_host);
|
||||
patch!(theme_setting);
|
||||
patch!(web_ui_list);
|
||||
patch!(clash_core);
|
||||
patch!(hotkeys);
|
||||
patch!(enable_global_hotkey);
|
||||
patch_opt_field!(self, patch, verge_tproxy_enabled);
|
||||
patch_opt_field!(self, patch, verge_mixed_port);
|
||||
patch_opt_field!(self, patch, verge_socks_port);
|
||||
patch_opt_field!(self, patch, verge_socks_enabled);
|
||||
patch_opt_field!(self, patch, verge_port);
|
||||
patch_opt_field!(self, patch, verge_http_enabled);
|
||||
patch_opt_field!(self, patch, enable_system_proxy);
|
||||
patch_opt_field!(self, patch, enable_proxy_guard);
|
||||
patch_opt_field!(self, patch, enable_bypass_check);
|
||||
patch_opt_field!(self, patch, use_default_bypass);
|
||||
patch_opt_field!(self, patch, system_proxy_bypass);
|
||||
patch_opt_field!(self, patch, proxy_guard_duration);
|
||||
patch_opt_field!(self, patch, proxy_auto_config);
|
||||
patch_opt_field!(self, patch, pac_file_content);
|
||||
patch_opt_field!(self, patch, proxy_host);
|
||||
patch_opt_field!(self, patch, theme_setting);
|
||||
patch_opt_field!(self, patch, web_ui_list);
|
||||
patch_opt_field!(self, patch, clash_core);
|
||||
patch_opt_field!(self, patch, hotkeys);
|
||||
patch_opt_field!(self, patch, enable_global_hotkey);
|
||||
|
||||
patch!(auto_close_connection);
|
||||
patch!(auto_check_update);
|
||||
patch!(default_latency_test);
|
||||
patch!(default_latency_timeout);
|
||||
patch!(enable_auto_delay_detection);
|
||||
patch!(auto_delay_detection_interval_minutes);
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(test_list);
|
||||
patch!(auto_log_clean);
|
||||
patch!(enable_auto_backup_schedule);
|
||||
patch!(auto_backup_interval_hours);
|
||||
patch!(auto_backup_on_change);
|
||||
patch_opt_field!(self, patch, auto_close_connection);
|
||||
patch_opt_field!(self, patch, auto_check_update);
|
||||
patch_opt_field!(self, patch, default_latency_test);
|
||||
patch_opt_field!(self, patch, default_latency_timeout);
|
||||
patch_opt_field!(self, patch, enable_auto_delay_detection);
|
||||
patch_opt_field!(self, patch, auto_delay_detection_interval_minutes);
|
||||
patch_opt_field!(self, patch, enable_builtin_enhanced);
|
||||
patch_opt_field!(self, patch, proxy_layout_column);
|
||||
patch_opt_field!(self, patch, test_list);
|
||||
patch_opt_field!(self, patch, auto_log_clean);
|
||||
patch_opt_field!(self, patch, enable_auto_backup_schedule);
|
||||
patch_opt_field!(self, patch, auto_backup_interval_hours);
|
||||
patch_opt_field!(self, patch, auto_backup_on_change);
|
||||
|
||||
patch!(webdav_url);
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
patch_opt_field!(self, patch, webdav_url);
|
||||
patch_opt_field!(self, patch, webdav_username);
|
||||
patch_opt_field!(self, patch, webdav_password);
|
||||
#[cfg(target_os = "macos")]
|
||||
patch!(enable_tray_speed);
|
||||
patch!(tray_proxy_groups_display_mode);
|
||||
patch!(tray_inline_outbound_modes);
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
patch!(enable_dns_settings);
|
||||
patch!(home_cards);
|
||||
patch!(enable_external_controller);
|
||||
patch_opt_field!(self, patch, enable_tray_speed);
|
||||
patch_opt_field!(self, patch, tray_proxy_groups_display_mode);
|
||||
patch_opt_field!(self, patch, tray_inline_outbound_modes);
|
||||
patch_opt_field!(self, patch, enable_auto_light_weight_mode);
|
||||
patch_opt_field!(self, patch, auto_light_weight_minutes);
|
||||
patch_opt_field!(self, patch, enable_dns_settings);
|
||||
patch_opt_field!(self, patch, home_cards);
|
||||
patch_opt_field!(self, patch, enable_external_controller);
|
||||
}
|
||||
|
||||
pub fn get_log_level(&self) -> LevelFilter {
|
||||
|
||||
@@ -14,6 +14,13 @@ pub mod network {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod sysproxy {
|
||||
/// Single source of the macOS default bypass list; both proxy backends compose from it.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const MACOS_DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
}
|
||||
|
||||
pub mod timing {
|
||||
use super::Duration;
|
||||
|
||||
|
||||
@@ -7,11 +7,10 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
Arc, OnceLock,
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, AtomicI64, Ordering},
|
||||
},
|
||||
time::{Duration, UNIX_EPOCH},
|
||||
@@ -82,8 +81,9 @@ impl Default for AutoBackupSettings {
|
||||
}
|
||||
|
||||
pub struct AutoBackupManager {
|
||||
settings: Arc<RwLock<AutoBackupSettings>>,
|
||||
settings_tx: watch::Sender<AutoBackupSettings>,
|
||||
// The watch channel is the single settings store; no duplicate copies.
|
||||
settings_rx: watch::Receiver<AutoBackupSettings>,
|
||||
runner_started: AtomicBool,
|
||||
exec_lock: Mutex<()>,
|
||||
last_backup: AtomicI64,
|
||||
@@ -93,10 +93,10 @@ impl AutoBackupManager {
|
||||
pub fn global() -> &'static Self {
|
||||
static INSTANCE: OnceLock<AutoBackupManager> = OnceLock::new();
|
||||
INSTANCE.get_or_init(|| {
|
||||
let (tx, _rx) = watch::channel(AutoBackupSettings::default());
|
||||
let (settings_tx, settings_rx) = watch::channel(AutoBackupSettings::default());
|
||||
Self {
|
||||
settings: Arc::new(RwLock::new(AutoBackupSettings::default())),
|
||||
settings_tx: tx,
|
||||
settings_tx,
|
||||
settings_rx,
|
||||
runner_started: AtomicBool::new(false),
|
||||
exec_lock: Mutex::new(()),
|
||||
last_backup: AtomicI64::new(0),
|
||||
@@ -106,9 +106,6 @@ impl AutoBackupManager {
|
||||
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let settings = Self::load_settings().await;
|
||||
{
|
||||
*self.settings.write() = settings;
|
||||
}
|
||||
let _ = self.settings_tx.send_replace(settings);
|
||||
self.maybe_start_runner(settings);
|
||||
Ok(())
|
||||
@@ -116,9 +113,6 @@ impl AutoBackupManager {
|
||||
|
||||
pub async fn refresh_settings(&self) -> Result<()> {
|
||||
let settings = Self::load_settings().await;
|
||||
{
|
||||
*self.settings.write() = settings;
|
||||
}
|
||||
let _ = self.settings_tx.send_replace(settings);
|
||||
self.maybe_start_runner(settings);
|
||||
Ok(())
|
||||
@@ -193,7 +187,7 @@ impl AutoBackupManager {
|
||||
}
|
||||
|
||||
async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> {
|
||||
let snapshot = *self.settings.read();
|
||||
let snapshot = *self.settings_rx.borrow();
|
||||
|
||||
if trigger.is_schedule() && !snapshot.schedule_enabled {
|
||||
return Ok(());
|
||||
|
||||
@@ -466,12 +466,3 @@ impl Hotkey {
|
||||
(del_list, add_list)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Hotkey {
|
||||
fn drop(&mut self) {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||
logging!(error, Type::Hotkey, "Error unregistering all hotkeys: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,47 @@ impl LightweightState {
|
||||
|
||||
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
|
||||
|
||||
static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
struct WindowListener {
|
||||
handler_id: AtomicU32,
|
||||
event: &'static str,
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
impl WindowListener {
|
||||
fn setup(&self, on_event: impl Fn() + Send + 'static) {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
let previous_handler_id = self.handler_id.swap(0, Ordering::AcqRel);
|
||||
if previous_handler_id != 0 {
|
||||
window.unlisten(previous_handler_id);
|
||||
logging!(debug, Type::Lightweight, "覆盖旧的{}监听", self.label);
|
||||
}
|
||||
let handler_id = window.listen(self.event, move |_event| on_event());
|
||||
self.handler_id.store(handler_id, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self) {
|
||||
let id = self.handler_id.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
window.unlisten(id);
|
||||
}
|
||||
logging!(debug, Type::Lightweight, "取消了{}监听", self.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static WINDOW_CLOSE_LISTENER: WindowListener = WindowListener {
|
||||
handler_id: AtomicU32::new(0),
|
||||
event: "tauri://close-requested",
|
||||
label: "窗口关闭",
|
||||
};
|
||||
|
||||
static WEBVIEW_FOCUS_LISTENER: WindowListener = WindowListener {
|
||||
handler_id: AtomicU32::new(0),
|
||||
event: "tauri://focus",
|
||||
label: "窗口焦点",
|
||||
};
|
||||
|
||||
static CANCEL_TX: Mutex<Option<oneshot::Sender<()>>> = Mutex::new(None);
|
||||
|
||||
@@ -154,55 +193,27 @@ pub async fn add_light_weight_timer() {
|
||||
}
|
||||
|
||||
fn setup_window_close_listener() {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
let previous_handler_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if previous_handler_id != 0 {
|
||||
window.unlisten(previous_handler_id);
|
||||
logging!(debug, Type::Lightweight, "覆盖旧的窗口关闭监听");
|
||||
}
|
||||
let handler_id = window.listen("tauri://close-requested", move |_event| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
setup_light_weight_timer().await;
|
||||
}));
|
||||
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
|
||||
});
|
||||
WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release);
|
||||
}
|
||||
WINDOW_CLOSE_LISTENER.setup(|| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
setup_light_weight_timer().await;
|
||||
}));
|
||||
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
|
||||
});
|
||||
}
|
||||
|
||||
fn cancel_window_close_listener() {
|
||||
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
window.unlisten(id);
|
||||
}
|
||||
logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
|
||||
}
|
||||
WINDOW_CLOSE_LISTENER.cancel();
|
||||
}
|
||||
|
||||
fn setup_webview_focus_listener() {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
let previous_handler_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if previous_handler_id != 0 {
|
||||
window.unlisten(previous_handler_id);
|
||||
logging!(debug, Type::Lightweight, "覆盖旧的窗口焦点监听");
|
||||
}
|
||||
let handler_id = window.listen("tauri://focus", move |_event| {
|
||||
cancel_light_weight_timer();
|
||||
logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
|
||||
});
|
||||
WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release);
|
||||
}
|
||||
WEBVIEW_FOCUS_LISTENER.setup(|| {
|
||||
cancel_light_weight_timer();
|
||||
logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
|
||||
});
|
||||
}
|
||||
|
||||
fn cancel_webview_focus_listener() {
|
||||
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
window.unlisten(id);
|
||||
}
|
||||
logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
|
||||
}
|
||||
WEBVIEW_FOCUS_LISTENER.cancel();
|
||||
}
|
||||
|
||||
async fn setup_light_weight_timer() {
|
||||
|
||||
@@ -559,12 +559,13 @@ impl CoreManager {
|
||||
})?;
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::utils::resolve::dns::sync_public_dns().await;
|
||||
// At login the app usually beats the network. Leave the write to the network watcher
|
||||
// rather than fail — only if the watcher is actually live; a user toggling while
|
||||
// offline still fails fast.
|
||||
// Only automatic restores can wait for the watcher; explicit requests must report failure.
|
||||
#[cfg(target_os = "macos")]
|
||||
if crate::core::network_watch::is_armed()
|
||||
&& !crate::utils::resolve::is_resolve_done()
|
||||
let defer_until_network = crate::core::network_watch::is_armed()
|
||||
&& crate::core::notification::what_was_asked()
|
||||
== crate::core::notification::FailedOperation::SystemProxyRestore;
|
||||
#[cfg(target_os = "macos")]
|
||||
if defer_until_network
|
||||
&& Config::verge()
|
||||
.await
|
||||
.latest_arc()
|
||||
@@ -579,7 +580,20 @@ impl CoreManager {
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
proxy_control::apply().await?;
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Err(error) = proxy_control::apply().await {
|
||||
if defer_until_network && proxy_control::is_missing_network_service_failure(&error) {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
"network service disappeared; the system proxy waits for its return"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
if !expectation.is_valid(
|
||||
self.get_running_mode().as_ref(),
|
||||
self.current_core_readiness_generation(),
|
||||
|
||||
@@ -49,6 +49,7 @@ async fn reapply() {
|
||||
.enable_system_proxy
|
||||
.unwrap_or_default()
|
||||
{
|
||||
crate::utils::resolve::dns::sync_public_dns().await;
|
||||
return;
|
||||
}
|
||||
if !proxy_control::has_network_service().await {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
notification::{self, FailedOperation},
|
||||
runstate::RUN_STATE,
|
||||
service,
|
||||
sysopt::Sysopt,
|
||||
sysopt::{Sysopt, format_bypass},
|
||||
},
|
||||
process::AsyncHandler,
|
||||
utils::{
|
||||
@@ -180,6 +180,14 @@ pub const fn is_missing_network_service(_error: &sysproxy::Error) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_missing_network_service_failure(error: &anyhow::Error) -> bool {
|
||||
!SystemProxyStateUnknown::is_in(error)
|
||||
&& SysproxyFailure::from_chain(error).is_none()
|
||||
&& error
|
||||
.downcast_ref::<sysproxy::Error>()
|
||||
.is_some_and(is_missing_network_service)
|
||||
}
|
||||
|
||||
/// Distinguish a misplaced sidecar from a missing privileged service.
|
||||
async fn classify_local_apply_failure(error: anyhow::Error) -> anyhow::Error {
|
||||
if SysproxyFailure::from_chain(&error).is_some() || !was_refused_locally(&error) {
|
||||
@@ -204,8 +212,6 @@ const fn refusal_classification(service_usable: Option<bool>) -> SysproxyFailure
|
||||
}
|
||||
|
||||
const LOOPBACK_HOST: &str = "127.0.0.1";
|
||||
const MACOS_DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
const MAX_SERVICE_BYPASS_LEN: usize = 8192;
|
||||
|
||||
static SERVICE_PROXY_OPERATIONS: ServiceProxyOperations = ServiceProxyOperations::new();
|
||||
@@ -327,13 +333,7 @@ fn service_bypass(verge: &IVerge) -> Result<String> {
|
||||
let custom = verge.system_proxy_bypass.as_deref().unwrap_or("");
|
||||
ensure!(!custom.contains('\0'), "system proxy bypass contains NUL");
|
||||
|
||||
let mut bypass = if custom.is_empty() {
|
||||
MACOS_DEFAULT_BYPASS.to_owned()
|
||||
} else if verge.use_default_bypass.unwrap_or(true) {
|
||||
format!("{MACOS_DEFAULT_BYPASS},{custom}")
|
||||
} else {
|
||||
custom.to_owned()
|
||||
};
|
||||
let mut bypass: String = format_bypass(verge.use_default_bypass.unwrap_or(true), custom).into();
|
||||
truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN);
|
||||
Ok(bypass)
|
||||
}
|
||||
@@ -367,6 +367,13 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
|
||||
ProxyApplyOutcome::Applied | ProxyApplyOutcome::NotRequested => Ok(()),
|
||||
// Direct fallback satisfies a disable request.
|
||||
ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()),
|
||||
// Older helpers encode a failed service lookup as fallback even when no OS write occurred.
|
||||
#[cfg(target_os = "macos")]
|
||||
ProxyApplyOutcome::DirectFallback { message }
|
||||
if message == sysproxy::Error::NoActiveNetworkService.to_string() =>
|
||||
{
|
||||
Err(sysproxy::Error::NoActiveNetworkService.into())
|
||||
}
|
||||
ProxyApplyOutcome::DirectFallback { message } => {
|
||||
Err(SysproxyFailure::DirectFallback { detail: message }.into())
|
||||
}
|
||||
@@ -617,6 +624,10 @@ pub async fn refresh_guard() -> Result<()> {
|
||||
logging!(debug, Type::Core, "proxy guard superseded (generation {generation})");
|
||||
break;
|
||||
}
|
||||
Err(error) if is_missing_network_service_failure(&error) => {
|
||||
consecutive_failures = 0;
|
||||
logging!(debug, Type::Core, "system proxy guard waits for a network service");
|
||||
}
|
||||
Err(error) => {
|
||||
consecutive_failures += 1;
|
||||
logging!(
|
||||
@@ -1118,10 +1129,11 @@ mod tests {
|
||||
.err()
|
||||
.unwrap_or_else(|| anyhow::anyhow!("an enable that never reached the OS must fail"));
|
||||
|
||||
assert_eq!(
|
||||
SysproxyFailure::from_chain(&error).map(SysproxyFailure::code),
|
||||
Some("SYSPROXY_DIRECT_FALLBACK")
|
||||
);
|
||||
assert!(super::is_missing_network_service_failure(&error));
|
||||
assert!(SysproxyFailure::from_chain(&error).is_none());
|
||||
assert!(!super::is_missing_network_service_failure(
|
||||
&error.context(SystemProxyStateUnknown)
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -603,50 +603,71 @@ fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> R
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
pub(super) fn run_maybe_elevated(program: &Path, hide_window: bool) -> Result<std::process::ExitStatus> {
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::os::windows::process::CommandExt as _;
|
||||
|
||||
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall.exe", || {
|
||||
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe"))
|
||||
})?;
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
let status = match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
||||
_ => StdCommand::new(uninstall_path).creation_flags(0x08000000).status()?,
|
||||
PrivilegeLevel::NotPrivileged => {
|
||||
let mut command = RunasCommand::new(program);
|
||||
if hide_window {
|
||||
command.show(false);
|
||||
}
|
||||
command.status()?
|
||||
}
|
||||
_ => {
|
||||
let mut command = StdCommand::new(program);
|
||||
if hide_window {
|
||||
// 0x08000000 is CREATE_NO_WINDOW, the direct counterpart of runas's show(false).
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
command.status()?
|
||||
}
|
||||
};
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn uninstaller_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result<PathBuf>) -> Result<PathBuf> {
|
||||
let uninstall_path = packaged_service_tool_path(file_name, packaged_path)?;
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
Ok(uninstall_path)
|
||||
}
|
||||
|
||||
fn ensure_uninstall_success(status: &std::process::ExitStatus) -> Result<()> {
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall.exe", || {
|
||||
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe"))
|
||||
})?;
|
||||
|
||||
let status = run_maybe_elevated(&uninstall_path, true)?;
|
||||
ensure_uninstall_success(&status)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || {
|
||||
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || {
|
||||
Ok(tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall"))
|
||||
})?;
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let elevator = crate::utils::help::linux_elevator();
|
||||
let status = if linux_running_as_root() {
|
||||
StdCommand::new(&uninstall_path).status()?
|
||||
@@ -676,14 +697,7 @@ fn uninstall_service() -> Result<()> {
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
ensure_uninstall_success(&status)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -698,14 +712,10 @@ fn linux_running_as_root() -> bool {
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || {
|
||||
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || {
|
||||
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall"))
|
||||
})?;
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let uninstall_path = macos_service_tool_path(&uninstall_path)?;
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||
|
||||
@@ -721,14 +731,7 @@ fn uninstall_service() -> Result<()> {
|
||||
|
||||
let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?;
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
ensure_uninstall_success(&status)
|
||||
}
|
||||
|
||||
fn install_service() -> Result<()> {
|
||||
@@ -1967,7 +1970,7 @@ impl ServiceManager {
|
||||
|
||||
async fn run_operation(&self, operation: impl Future<Output = Result<()>>) -> Result<()> {
|
||||
run_operation_and_then(&RUN_STATE, operation, || async {
|
||||
if let Err(error) = Tray::global().update_menu().await {
|
||||
if let Err(error) = Tray::global().update_part().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
|
||||
@@ -241,20 +241,19 @@ async fn current_os_proxy_state(sys: Sysproxy, auto: Autoproxy) -> OsProxyState
|
||||
}
|
||||
|
||||
pub(crate) struct Sysopt {
|
||||
update_lock: TokioMutex<()>,
|
||||
// Owns the proxy target pair; every reader and writer goes through this lock.
|
||||
proxy_state_lock: TokioMutex<(Sysproxy, Autoproxy)>,
|
||||
guard_operation_lock: TokioMutex<()>,
|
||||
reset_sysproxy: AtomicBool,
|
||||
inner_proxy: Arc<RwLock<(Sysproxy, Autoproxy)>>,
|
||||
guard: Arc<RwLock<GuardMonitor>>,
|
||||
}
|
||||
|
||||
impl Default for Sysopt {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
update_lock: TokioMutex::new(()),
|
||||
proxy_state_lock: TokioMutex::new((Sysproxy::default(), Autoproxy::default())),
|
||||
guard_operation_lock: TokioMutex::new(()),
|
||||
reset_sysproxy: AtomicBool::new(false),
|
||||
inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))),
|
||||
guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))),
|
||||
}
|
||||
}
|
||||
@@ -269,10 +268,9 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
static BYPASS_SEPARATOR: &str = ",";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
static DEFAULT_BYPASS: &str = crate::constants::sysproxy::MACOS_DEFAULT_BYPASS;
|
||||
|
||||
fn format_bypass(use_default: bool, custom_bypass: &str) -> String {
|
||||
pub(super) fn format_bypass(use_default: bool, custom_bypass: &str) -> String {
|
||||
if custom_bypass.is_empty() {
|
||||
DEFAULT_BYPASS.into()
|
||||
} else if use_default {
|
||||
@@ -361,7 +359,7 @@ impl Sysopt {
|
||||
}
|
||||
}
|
||||
while self.access_guard().read().get_state().is_pendding() {
|
||||
tokio::task::yield_now().await;
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
// `start()` false may also mean a guard was already running.
|
||||
!self.access_guard().read().get_state().is_stopped()
|
||||
@@ -409,18 +407,21 @@ impl Sysopt {
|
||||
/// configuration instead of a partially-applied one (e.g. SOCKS already
|
||||
/// disabled but HTTP still enabled mid-transition).
|
||||
pub(crate) async fn wait_idle(&self) {
|
||||
let _ = self.update_lock.lock().await;
|
||||
let _ = self.proxy_state_lock.lock().await;
|
||||
}
|
||||
|
||||
/// init the sysproxy
|
||||
#[allow(
|
||||
clippy::significant_drop_tightening,
|
||||
reason = "the proxy state lock must stay held until the OS write finishes; wait_idle relies on it"
|
||||
)]
|
||||
pub(super) async fn update_sysproxy(&self) -> Result<()> {
|
||||
let _lock = self.update_lock.lock().await;
|
||||
let mut proxy_state = self.proxy_state_lock.lock().await;
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
// Configured, not live: this runs while the Core is being started or restarted, and
|
||||
// asking a Core that is not up yet would only fall back here anyway.
|
||||
let port = MixedPort::desired().await;
|
||||
let pac_port = server::embedded_server_port()?;
|
||||
// 先 await, 避免持有锁导致的 Send 问题
|
||||
let bypass = get_bypass().await;
|
||||
|
||||
let (sys_enable, pac_enable, proxy_host, proxy_guard) = (
|
||||
@@ -431,7 +432,7 @@ impl Sysopt {
|
||||
);
|
||||
|
||||
let (sys, auto, guard_type) = {
|
||||
let (sys, auto) = &mut *self.inner_proxy.write();
|
||||
let (sys, auto) = &mut *proxy_state;
|
||||
sys.host = proxy_host.into();
|
||||
sys.port = port;
|
||||
sys.bypass = bypass.into();
|
||||
@@ -528,6 +529,10 @@ impl Sysopt {
|
||||
}
|
||||
|
||||
/// reset the sysproxy
|
||||
#[allow(
|
||||
clippy::significant_drop_tightening,
|
||||
reason = "the proxy state lock must stay held until the disable finishes; wait_idle relies on it"
|
||||
)]
|
||||
pub(super) async fn reset_sysproxy(&self) -> Result<()> {
|
||||
if self
|
||||
.reset_sysproxy
|
||||
@@ -539,13 +544,13 @@ impl Sysopt {
|
||||
defer! {
|
||||
self.reset_sysproxy.store(false, Ordering::SeqCst);
|
||||
}
|
||||
let _lock = self.update_lock.lock().await;
|
||||
let mut proxy_state = self.proxy_state_lock.lock().await;
|
||||
let _guard_operation = self.guard_operation_lock.lock().await;
|
||||
let drained = self.stop_proxy_guard_locked().await;
|
||||
|
||||
// 直接关闭所有代理
|
||||
let (sys, auto) = {
|
||||
let (sys, auto) = &mut *self.inner_proxy.write();
|
||||
let (sys, auto) = &mut *proxy_state;
|
||||
sys.enable = false;
|
||||
auto.enable = false;
|
||||
(sys.clone(), auto.clone())
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use futures::StreamExt as _;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
@@ -21,7 +22,6 @@ use tokio::{
|
||||
sync::mpsc,
|
||||
time::{sleep, timeout},
|
||||
};
|
||||
use tokio_stream::StreamExt as _;
|
||||
use tokio_util::time::{DelayQueue, delay_queue::Key};
|
||||
|
||||
enum TimerCommand {
|
||||
@@ -501,6 +501,7 @@ impl Timer {
|
||||
mod tests {
|
||||
use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
|
||||
use crate::config::{PrfItem, PrfOption};
|
||||
use futures::StreamExt as _;
|
||||
use parking_lot::Mutex;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
@@ -513,7 +514,6 @@ mod tests {
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::Barrier;
|
||||
use tokio_stream::StreamExt as _;
|
||||
use tokio_util::time::DelayQueue;
|
||||
|
||||
struct CancellationProbe {
|
||||
|
||||
@@ -636,7 +636,8 @@ async fn create_tray_menu(
|
||||
.tray_proxy_groups_display_mode
|
||||
.as_deref()
|
||||
.unwrap_or("default");
|
||||
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable";
|
||||
let include_proxy_groups =
|
||||
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
||||
|
||||
let proxy_group_order_map = runtime_proxy_groups_order;
|
||||
|
||||
|
||||
@@ -18,6 +18,14 @@ pub struct SilentUpdater {
|
||||
background_cancel: Mutex<Option<CancellationToken>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, specta::Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InstallUpdateOutcome {
|
||||
Installed,
|
||||
Cancelled,
|
||||
UpdateChanged,
|
||||
}
|
||||
|
||||
singleton!(SilentUpdater, SILENT_UPDATER);
|
||||
|
||||
impl SilentUpdater {
|
||||
@@ -34,7 +42,7 @@ impl SilentUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, specta::Type)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct UpdateCacheMeta {
|
||||
version: String,
|
||||
downloaded_at: String,
|
||||
@@ -101,11 +109,11 @@ impl SilentUpdater {
|
||||
}
|
||||
|
||||
/// Returns the cached installer for `update`, deleting a cache that does not match it.
|
||||
fn verified_cache(app_handle: &tauri::AppHandle, update: &Update) -> Option<Vec<u8>> {
|
||||
fn verified_cache(update: &Update) -> Option<Vec<u8>> {
|
||||
let meta = Self::read_cache_meta().ok()?;
|
||||
let bytes = if meta.version == update.version {
|
||||
Self::read_cache_bytes().and_then(|bytes| {
|
||||
verify_signature(app_handle, &bytes, &update.signature, &update.version)?;
|
||||
update.verify(&bytes)?;
|
||||
Ok(bytes)
|
||||
})
|
||||
} else {
|
||||
@@ -127,46 +135,6 @@ impl SilentUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
/// The cache directory is user-writable and feeds an elevated installer, so it is re-verified
|
||||
/// with the same rules `Update::download` applies.
|
||||
fn verify_signature(app_handle: &tauri::AppHandle, bytes: &[u8], signature: &str, version: &str) -> Result<()> {
|
||||
use base64::Engine as _;
|
||||
|
||||
let decode = |value: &str| -> Result<String> {
|
||||
Ok(String::from_utf8(
|
||||
base64::engine::general_purpose::STANDARD.decode(value)?,
|
||||
)?)
|
||||
};
|
||||
let config: tauri_plugin_updater::Config = serde_json::from_value(
|
||||
app_handle
|
||||
.config()
|
||||
.plugins
|
||||
.0
|
||||
.get("updater")
|
||||
.cloned()
|
||||
.context("updater is not configured")?,
|
||||
)?;
|
||||
|
||||
let public_key = minisign_verify::PublicKey::decode(&decode(&config.pubkey)?)?;
|
||||
let signature = minisign_verify::Signature::decode(&decode(signature)?)?;
|
||||
public_key
|
||||
.verify(bytes, &signature, true)
|
||||
.context("signature mismatch")?;
|
||||
|
||||
match signature
|
||||
.trusted_comment()
|
||||
.split('\t')
|
||||
.find_map(|field| field.strip_prefix("version:"))
|
||||
{
|
||||
Some(signed) => anyhow::ensure!(
|
||||
signed.trim_start_matches('v') == version,
|
||||
"signed for v{signed}, not v{version}"
|
||||
),
|
||||
None => anyhow::ensure!(!config.require_signed_version, "signature carries no version"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_build_to_stable(current: &str, remote: &str) -> bool {
|
||||
let Some((base, build)) = current.trim_start_matches('v').split_once('+') else {
|
||||
return false;
|
||||
@@ -391,7 +359,7 @@ impl SilentUpdater {
|
||||
}
|
||||
};
|
||||
|
||||
let Some(bytes) = Self::verified_cache(app_handle, &update) else {
|
||||
let Some(bytes) = Self::verified_cache(&update) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -424,13 +392,13 @@ impl SilentUpdater {
|
||||
}
|
||||
|
||||
/// Installs `version` for the update dialog, reusing the cache when it holds that version;
|
||||
/// `false` when cancelled before the installer starts.
|
||||
/// A changed offer must be confirmed again before downloading or installing it.
|
||||
pub async fn install_update(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle,
|
||||
version: &str,
|
||||
on_event: impl Fn(DownloadEvent) + Sync,
|
||||
) -> Result<bool> {
|
||||
) -> Result<InstallUpdateOutcome> {
|
||||
let cancel = CancellationToken::new();
|
||||
*self.manual_cancel.lock() = Some(cancel.clone());
|
||||
let prepared = tokio::select! {
|
||||
@@ -440,31 +408,36 @@ impl SilentUpdater {
|
||||
// `cancel_download` cancels under the lock, so once the token is taken back its state is
|
||||
// final; a cancel during the last synchronous poll of `prepare_install` still wins.
|
||||
self.manual_cancel.lock().take();
|
||||
let Some((update, bytes)) = prepared.filter(|_| !cancel.is_cancelled()).transpose()? else {
|
||||
let Some(prepared) = prepared.filter(|_| !cancel.is_cancelled()).transpose()? else {
|
||||
logging!(info, Type::System, "Update v{version} cancelled");
|
||||
return Ok(false);
|
||||
return Ok(InstallUpdateOutcome::Cancelled);
|
||||
};
|
||||
let Some((update, bytes)) = prepared else {
|
||||
return Ok(InstallUpdateOutcome::UpdateChanged);
|
||||
};
|
||||
|
||||
logging!(info, Type::System, "Installing update v{version}...");
|
||||
Self::install(update, bytes).await?;
|
||||
Self::delete_cache();
|
||||
Ok(true)
|
||||
Ok(InstallUpdateOutcome::Installed)
|
||||
}
|
||||
|
||||
async fn prepare_install(
|
||||
app_handle: &tauri::AppHandle,
|
||||
version: &str,
|
||||
on_event: impl Fn(DownloadEvent) + Sync,
|
||||
) -> Result<(Update, Vec<u8>)> {
|
||||
let update = Self::check(app_handle)
|
||||
) -> Result<Option<(Update, Vec<u8>)>> {
|
||||
let Some(update) = Self::check(app_handle)
|
||||
.await?
|
||||
.filter(|update| update.version == version)
|
||||
.with_context(|| format!("v{version} is no longer offered"))?;
|
||||
let bytes = match Self::verified_cache(app_handle, &update) {
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bytes = match Self::verified_cache(&update) {
|
||||
Some(bytes) => bytes,
|
||||
None => download(&update, on_event).await?,
|
||||
};
|
||||
Ok((update, bytes))
|
||||
Ok(Some((update, bytes)))
|
||||
}
|
||||
|
||||
pub fn cancel_download(&self) {
|
||||
@@ -641,7 +614,7 @@ impl SilentUpdater {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if Self::verified_cache(app_handle, &update).is_some() {
|
||||
if Self::verified_cache(&update).is_some() {
|
||||
logging!(info, Type::System, "Silent updater: v{version} already cached");
|
||||
} else {
|
||||
logging!(info, Type::System, "Silent updater: downloading v{version}...");
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use nanoid::nanoid;
|
||||
use scopeguard::defer;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
hash::{DefaultHasher, Hash as _, Hasher as _},
|
||||
path::Path,
|
||||
sync::LazyLock,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
@@ -329,7 +332,7 @@ impl CoreConfigValidator {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Validate, "无法确定文件类型: {}, 错误: {}", config_path, err);
|
||||
return Self::validate_config_internal_outcome(config_path).await;
|
||||
return Self::validate_profile_yaml_outcome(config_path).await;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -344,7 +347,33 @@ impl CoreConfigValidator {
|
||||
}
|
||||
|
||||
logging!(info, Type::Validate, "使用Clash内核验证配置文件: {}", config_path);
|
||||
Self::validate_config_internal_outcome(config_path).await
|
||||
Self::validate_profile_yaml_outcome(config_path).await
|
||||
}
|
||||
|
||||
/// The user file is saved as written, so the core checks a copy carrying the tolerated short-ids.
|
||||
async fn validate_profile_yaml_outcome(config_path: &str) -> Result<ValidationOutcome> {
|
||||
let Some(tolerant) = Self::short_id_tolerant_text(config_path).await else {
|
||||
return Self::validate_config_internal_outcome(config_path).await;
|
||||
};
|
||||
let Ok(app_dir) = dirs::app_home_dir() else {
|
||||
return Self::validate_config_internal_outcome(config_path).await;
|
||||
};
|
||||
let check_path = app_dir.join(format!("short-id-check-{}.yaml", nanoid!()));
|
||||
defer! {
|
||||
let _ = std::fs::remove_file(&check_path);
|
||||
}
|
||||
if help::save_yaml_str(&check_path, &tolerant).await.is_err() {
|
||||
return Self::validate_config_internal_outcome(config_path).await;
|
||||
}
|
||||
Self::validate_config_internal_outcome(dirs::path_to_str(&check_path)?).await
|
||||
}
|
||||
|
||||
async fn short_id_tolerant_text(config_path: &str) -> Option<std::string::String> {
|
||||
let yaml = fs::read_to_string(config_path).await.ok()?;
|
||||
match help::short_id_tolerant(&yaml, Path::new(config_path)) {
|
||||
Ok((Cow::Owned(tolerant), _)) => Some(tolerant),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn validate_config_internal_outcome(config_path: &str) -> Result<ValidationOutcome> {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use crate::utils::dirs;
|
||||
use crate::{core::service, utils::dirs};
|
||||
use anyhow::{Result, bail};
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
pub fn invoke_uwptools() -> Result<()> {
|
||||
let resource_dir = dirs::app_resources_dir()?;
|
||||
@@ -14,13 +11,7 @@ pub fn invoke_uwptools() -> Result<()> {
|
||||
bail!("enableLoopback exe not found");
|
||||
}
|
||||
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
|
||||
match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
|
||||
_ => StdCommand::new(tool_path).status()?,
|
||||
};
|
||||
service::run_maybe_elevated(&tool_path, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// This function is exported for use by the Clash core
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
function main(config, _name) {
|
||||
if (config.mode === 'script') {
|
||||
config.mode = 'rule'
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// This function is exported for use by the Clash core
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
function main(config, _name) {
|
||||
if (Array.isArray(config.proxies)) {
|
||||
config.proxies.forEach((p, i) => {
|
||||
if (p.type === 'hysteria' && typeof p.alpn === 'string') {
|
||||
config.proxies[i].alpn = [p.alpn]
|
||||
}
|
||||
})
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -3,8 +3,10 @@ use crate::{
|
||||
config::PrfItem,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -22,12 +24,6 @@ pub enum ChainType {
|
||||
Groups(SeqMap),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChainSupport {
|
||||
ClashMeta,
|
||||
ClashMetaAlpha,
|
||||
}
|
||||
|
||||
// Helper trait to allow async conversion
|
||||
pub trait AsyncChainItemFrom {
|
||||
async fn from_async(item: &PrfItem) -> Option<ChainItem>;
|
||||
@@ -53,70 +49,65 @@ impl AsyncChainItemFrom for Option<ChainItem> {
|
||||
uid,
|
||||
data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
|
||||
}),
|
||||
"rules" => {
|
||||
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
|
||||
Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Rules(seq_map),
|
||||
})
|
||||
}
|
||||
"proxies" => {
|
||||
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
|
||||
Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Proxies(seq_map),
|
||||
})
|
||||
}
|
||||
"groups" => {
|
||||
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
|
||||
Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Groups(seq_map),
|
||||
})
|
||||
"rules" | "proxies" | "groups" => {
|
||||
let yaml = fs::read_to_string(&path).await.ok()?;
|
||||
let seq_map = parse_seq_map(&yaml, &path)?;
|
||||
let data = match itype {
|
||||
"rules" => ChainType::Rules(seq_map),
|
||||
"proxies" => ChainType::Proxies(seq_map),
|
||||
_ => ChainType::Groups(seq_map),
|
||||
};
|
||||
Some(ChainItem { uid, data })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ChainItem {
|
||||
/// 内建支持一些脚本
|
||||
pub fn builtin() -> Vec<(ChainSupport, Self)> {
|
||||
// meta 的一些处理
|
||||
let meta_guard = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
// meta 的一些处理
|
||||
let meta_guard_alpha = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn_alpha = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
vec![
|
||||
(ChainSupport::ClashMeta, hy_alpn),
|
||||
(ChainSupport::ClashMeta, meta_guard),
|
||||
(ChainSupport::ClashMetaAlpha, hy_alpn_alpha),
|
||||
(ChainSupport::ClashMetaAlpha, meta_guard_alpha),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn to_script<U: Into<String>, D: Into<String>>(uid: U, data: D) -> Self {
|
||||
Self {
|
||||
uid: uid.into(),
|
||||
data: ChainType::Script(data.into()),
|
||||
/// Typed parsing reads `delete: [1234]` as strings where a `Value` round trip would reject it.
|
||||
fn parse_seq_map(yaml: &str, path: &Path) -> Option<SeqMap> {
|
||||
let original = serde_yaml_ng::from_str::<SeqMap>(yaml).ok()?;
|
||||
let Ok((Cow::Owned(tolerant), _)) = help::short_id_tolerant(yaml, path) else {
|
||||
return Some(original);
|
||||
};
|
||||
match serde_yaml_ng::from_str::<SeqMap>(&tolerant) {
|
||||
Ok(seq_map) => Some(seq_map),
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"kept numeric short-id in {}: quoted text does not load",
|
||||
path.display()
|
||||
);
|
||||
Some(original)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainSupport {
|
||||
pub fn is_support(&self, core: Option<&String>) -> bool {
|
||||
match core {
|
||||
Some(core) => matches!(
|
||||
(self, core.as_str()),
|
||||
(Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha")
|
||||
),
|
||||
None => true,
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
|
||||
mod tests {
|
||||
use super::parse_seq_map;
|
||||
use serde_yaml_ng::Value;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn seq_short_id_keeps_typed_parse() {
|
||||
for (input, delete) in [
|
||||
("prepend: []\nappend: []\ndelete: [1234]\n", "1234"),
|
||||
("prepend: []\nappend: []\ndelete: [true]\n", "true"),
|
||||
] {
|
||||
let seq_map = parse_seq_map(input, Path::new("b8.yaml")).expect("seq file should load");
|
||||
assert_eq!(seq_map.delete, [delete]);
|
||||
}
|
||||
|
||||
let input = "prepend:\n - name: r\n reality-opts:\n short-id: 1234e123\nappend: []\ndelete: []\n";
|
||||
let seq_map = parse_seq_map(input, Path::new("seq.yaml")).expect("seq file should load");
|
||||
let short_id = seq_map
|
||||
.prepend
|
||||
.first()
|
||||
.and_then(|proxy| proxy.get("reality-opts"))
|
||||
.and_then(|opts| opts.get("short-id"));
|
||||
assert_eq!(short_id, Some(&Value::String("1234e123".into())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ fn lowercase_key(key: &str) -> Value {
|
||||
pub fn use_lowercase(config: &Mapping) -> Mapping {
|
||||
let mut lowercased = Mapping::new();
|
||||
|
||||
for (key, value) in config.into_iter() {
|
||||
for (key, value) in config.iter() {
|
||||
if let Some(key_str) = key.as_str() {
|
||||
lowercased.insert(lowercase_key(key_str), value.clone());
|
||||
}
|
||||
@@ -35,17 +35,6 @@ pub fn use_lowercase(config: &Mapping) -> Mapping {
|
||||
lowercased
|
||||
}
|
||||
|
||||
pub fn use_lowercase_owned(config: Mapping) -> Mapping {
|
||||
let mut lowercased = Mapping::new();
|
||||
|
||||
for (key, value) in config {
|
||||
if let Some(key_str) = key.as_str() {
|
||||
lowercased.insert(lowercase_key(key_str), value);
|
||||
}
|
||||
}
|
||||
lowercased
|
||||
}
|
||||
|
||||
pub fn use_sort(mut config: Mapping) -> Mapping {
|
||||
let mut sorted = Mapping::new();
|
||||
HANDLE_FIELDS.into_iter().for_each(|key| {
|
||||
|
||||
+64
-127
@@ -8,7 +8,7 @@ mod tun;
|
||||
|
||||
use self::{
|
||||
chain::{AsyncChainItemFrom as _, ChainItem, ChainType},
|
||||
field::{use_keys, use_lowercase_owned, use_sort},
|
||||
field::{use_keys, use_lowercase, use_sort},
|
||||
merge::use_merge,
|
||||
script::use_script,
|
||||
seq::{SeqMap, use_seq},
|
||||
@@ -34,7 +34,6 @@ type ResultLog = Vec<(String, String)>;
|
||||
#[derive(Debug)]
|
||||
struct ConfigValues {
|
||||
clash_config: Mapping,
|
||||
clash_core: Option<String>,
|
||||
enable_tun: bool,
|
||||
enable_builtin: bool,
|
||||
socks_enabled: bool,
|
||||
@@ -128,8 +127,7 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues {
|
||||
let dns_settings = verge_arc.dns_settings_for(profile_uid);
|
||||
let dns_override_confirmation = dns_settings.confirmation;
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = (
|
||||
Some(verge_arc.get_valid_clash_core()),
|
||||
let (enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = (
|
||||
enable_tun_mode.unwrap_or(false),
|
||||
enable_builtin_enhanced.unwrap_or(true),
|
||||
verge_socks_enabled.unwrap_or(false),
|
||||
@@ -148,7 +146,6 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues {
|
||||
|
||||
ConfigValues {
|
||||
clash_config,
|
||||
clash_core,
|
||||
enable_tun,
|
||||
enable_builtin,
|
||||
socks_enabled,
|
||||
@@ -199,35 +196,25 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
|
||||
|
||||
let name = current_item.name.clone().unwrap_or_default();
|
||||
|
||||
let ProfileItems {
|
||||
merge_item: default_merge,
|
||||
script_item: default_script,
|
||||
rules_item: default_rules,
|
||||
proxies_item: default_proxies,
|
||||
groups_item: default_groups,
|
||||
global_merge: default_global_merge,
|
||||
global_script: default_global_script,
|
||||
..
|
||||
} = ProfileItems::default();
|
||||
|
||||
let (merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script) = tokio::join!(
|
||||
chain_item_or_default(profiles.get_item(&merge_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&script_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&rules_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Rules(SeqMap::default()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Proxies(SeqMap::default()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&groups_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Groups(SeqMap::default()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item("Merge").ok(), || ChainItem {
|
||||
uid: "Merge".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item("Script").ok(), || ChainItem {
|
||||
uid: "Script".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&merge_uid).ok(), || default_merge,),
|
||||
chain_item_or_default(profiles.get_item(&script_uid).ok(), || default_script,),
|
||||
chain_item_or_default(profiles.get_item(&rules_uid).ok(), || default_rules,),
|
||||
chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || default_proxies,),
|
||||
chain_item_or_default(profiles.get_item(&groups_uid).ok(), || default_groups,),
|
||||
chain_item_or_default(profiles.get_item("Merge").ok(), || default_global_merge,),
|
||||
chain_item_or_default(profiles.get_item("Script").ok(), || default_global_script,),
|
||||
);
|
||||
|
||||
Ok(ProfileItems {
|
||||
@@ -243,16 +230,16 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_global_items(
|
||||
async fn process_override_items(
|
||||
mut config: Mapping,
|
||||
mut exists_keys: Vec<String>,
|
||||
mut result_map: HashMap<String, ResultLog>,
|
||||
global_merge: ChainItem,
|
||||
global_script: ChainItem,
|
||||
merge_item: ChainItem,
|
||||
script_item: ChainItem,
|
||||
profile_name: &String,
|
||||
authoritative: &AuthoritativeFields,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Merge(merge) = global_merge.data {
|
||||
if let ChainType::Merge(merge) = merge_item.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
let before = authoritative.current(&config);
|
||||
config = use_merge(&merge, config);
|
||||
@@ -262,11 +249,11 @@ async fn process_global_items(
|
||||
.map(discarded_note)
|
||||
.collect();
|
||||
if !notes.is_empty() {
|
||||
result_map.entry(global_merge.uid).or_default().extend(notes);
|
||||
result_map.entry(merge_item.uid).or_default().extend(notes);
|
||||
}
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = global_script.data {
|
||||
if let ChainType::Script(script) = script_item.data {
|
||||
let before = authoritative.current(&config);
|
||||
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
|
||||
exists_keys.extend(changed_keys);
|
||||
@@ -277,7 +264,7 @@ async fn process_global_items(
|
||||
.into_iter()
|
||||
.map(discarded_note),
|
||||
);
|
||||
result_map.insert(global_script.uid, logs);
|
||||
result_map.insert(script_item.uid, logs);
|
||||
}
|
||||
|
||||
(config, exists_keys, result_map)
|
||||
@@ -544,46 +531,6 @@ fn ensure_lan_bind_address(mut config: Mapping) -> Mapping {
|
||||
config
|
||||
}
|
||||
|
||||
async fn process_profile_items(
|
||||
mut config: Mapping,
|
||||
mut exists_keys: Vec<String>,
|
||||
mut result_map: HashMap<String, ResultLog>,
|
||||
merge_item: ChainItem,
|
||||
script_item: ChainItem,
|
||||
profile_name: &String,
|
||||
authoritative: &AuthoritativeFields,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Merge(merge) = merge_item.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
let before = authoritative.current(&config);
|
||||
config = use_merge(&merge, config);
|
||||
let notes: ResultLog = authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note)
|
||||
.collect();
|
||||
if !notes.is_empty() {
|
||||
result_map.entry(merge_item.uid).or_default().extend(notes);
|
||||
}
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = script_item.data {
|
||||
let before = authoritative.current(&config);
|
||||
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
|
||||
exists_keys.extend(changed_keys);
|
||||
config = res_config;
|
||||
logs.extend(
|
||||
authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note),
|
||||
);
|
||||
result_map.insert(script_item.uid, logs);
|
||||
}
|
||||
|
||||
(config, exists_keys, result_map)
|
||||
}
|
||||
|
||||
/// Merges the app's clash config over the profile; switches are parameters to keep it pure.
|
||||
fn merge_default_config(
|
||||
mut config: Mapping,
|
||||
@@ -655,25 +602,12 @@ fn merge_default_config(
|
||||
config
|
||||
}
|
||||
|
||||
fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable_builtin: bool) -> Mapping {
|
||||
fn apply_builtin_scripts(mut config: Mapping, enable_builtin: bool) -> Mapping {
|
||||
if enable_builtin {
|
||||
let items: Vec<_> = ChainItem::builtin()
|
||||
.into_iter()
|
||||
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||
.map(|(_, c)| c)
|
||||
.collect();
|
||||
if !items.is_empty() {
|
||||
// The JS path saw a lowercased view; lowercase once, only when a builtin runs.
|
||||
config = use_lowercase_owned(config);
|
||||
for item in items {
|
||||
logging!(debug, Type::Core, "run builtin script {}", item.uid);
|
||||
config = match item.uid.as_str() {
|
||||
"verge_hy_alpn" => builtin_hy_alpn(config),
|
||||
"verge_meta_guard" => builtin_meta_guard(config),
|
||||
_ => config,
|
||||
};
|
||||
}
|
||||
}
|
||||
// The JS path saw a lowercased view; lowercase once, only when the builtin runs.
|
||||
config = use_lowercase(&config);
|
||||
logging!(debug, Type::Core, "run builtin script verge_hy_alpn");
|
||||
config = builtin_hy_alpn(config);
|
||||
}
|
||||
|
||||
config
|
||||
@@ -693,14 +627,6 @@ fn builtin_hy_alpn(mut config: Mapping) -> Mapping {
|
||||
config
|
||||
}
|
||||
|
||||
fn builtin_meta_guard(mut config: Mapping) -> Mapping {
|
||||
if config.get("mode").and_then(Value::as_str) == Some("script") {
|
||||
config.insert(Value::from("mode"), Value::from("rule"));
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
|
||||
// built-in proxies docs: https://wiki.metacubex.one/config/proxies/built-in
|
||||
const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS", "PASS-RULE"];
|
||||
@@ -879,7 +805,6 @@ pub async fn enhance(
|
||||
let cfg_vals = get_config_values(profile_uid).await;
|
||||
let ConfigValues {
|
||||
clash_config,
|
||||
clash_core,
|
||||
enable_tun,
|
||||
enable_builtin,
|
||||
socks_enabled,
|
||||
@@ -929,13 +854,13 @@ pub async fn enhance(
|
||||
tproxy_enabled,
|
||||
);
|
||||
|
||||
let config = apply_builtin_scripts(config, clash_core, enable_builtin);
|
||||
let config = apply_builtin_scripts(config, enable_builtin);
|
||||
let config = use_tun(config, enable_tun);
|
||||
let (config, dns_settings) = apply_dns_settings(config, enable_dns_settings).await;
|
||||
|
||||
let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, dns_settings);
|
||||
|
||||
let (config, exists_keys, result_map) = process_global_items(
|
||||
let (config, exists_keys, result_map) = process_override_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
@@ -946,7 +871,7 @@ pub async fn enhance(
|
||||
)
|
||||
.await;
|
||||
|
||||
let (config, exists_keys, result_map) = process_profile_items(
|
||||
let (config, exists_keys, result_map) = process_override_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
@@ -1428,7 +1353,7 @@ mod authoritative_field_tests {
|
||||
mod tests {
|
||||
use super::{
|
||||
AuthoritativeFields, ChainItem, ChainType, Mapping, cleanup_proxy_groups, ensure_lan_bind_address,
|
||||
process_global_items, process_profile_items, use_keys,
|
||||
process_override_items, use_keys,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -1476,22 +1401,28 @@ mod tests {
|
||||
data: ChainType::Merge(merge.clone()),
|
||||
};
|
||||
let profile_name = "test-profile".into();
|
||||
let (config, keys, logs) = process_global_items(
|
||||
let (config, keys, logs) = process_override_items(
|
||||
config,
|
||||
vec![],
|
||||
HashMap::new(),
|
||||
merge_item("global-merge"),
|
||||
ChainItem::to_script("global-script", script),
|
||||
ChainItem {
|
||||
uid: "global-script".into(),
|
||||
data: ChainType::Script(script.into()),
|
||||
},
|
||||
&profile_name,
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
let (config, _, logs) = process_profile_items(
|
||||
let (config, _, logs) = process_override_items(
|
||||
config,
|
||||
keys,
|
||||
logs,
|
||||
merge_item("profile-merge"),
|
||||
ChainItem::to_script("profile-script", script),
|
||||
ChainItem {
|
||||
uid: "profile-script".into(),
|
||||
data: ChainType::Script(script.into()),
|
||||
},
|
||||
&profile_name,
|
||||
&authoritative,
|
||||
)
|
||||
@@ -1536,16 +1467,19 @@ mod tests {
|
||||
nested: {winner: global-merge}, dns: {enable: false}, tun: {enable: false}}",
|
||||
)),
|
||||
};
|
||||
let global_script = ChainItem::to_script(
|
||||
"Script",
|
||||
r#"function main(config) {
|
||||
let global_script = ChainItem {
|
||||
uid: "Script".into(),
|
||||
data: ChainType::Script(
|
||||
r#"function main(config) {
|
||||
config["global-script-wins"] = "global-script";
|
||||
config["profile-merge-wins"] = "global-script";
|
||||
config["profile-script-wins"] = "global-script";
|
||||
config.nested.winner = "global-script";
|
||||
return config;
|
||||
}"#,
|
||||
);
|
||||
}"#
|
||||
.into(),
|
||||
),
|
||||
};
|
||||
let profile_merge = ChainItem {
|
||||
uid: "profile-merge".into(),
|
||||
data: ChainType::Merge(mapping(
|
||||
@@ -1553,18 +1487,21 @@ mod tests {
|
||||
nested: {winner: profile-merge}}",
|
||||
)),
|
||||
};
|
||||
let profile_script = ChainItem::to_script(
|
||||
"profile-script",
|
||||
r#"function main(config) {
|
||||
let profile_script = ChainItem {
|
||||
uid: "profile-script".into(),
|
||||
data: ChainType::Script(
|
||||
r#"function main(config) {
|
||||
config["profile-script-wins"] = "profile-script";
|
||||
config.nested.winner = "profile-script";
|
||||
return config;
|
||||
}"#,
|
||||
);
|
||||
}"#
|
||||
.into(),
|
||||
),
|
||||
};
|
||||
|
||||
let profile_name = "test-profile".into();
|
||||
let authoritative = AuthoritativeFields::capture(&config, &[], Mapping::new());
|
||||
let (config, exists_keys, result_map) = process_global_items(
|
||||
let (config, exists_keys, result_map) = process_override_items(
|
||||
config,
|
||||
exists_keys,
|
||||
HashMap::new(),
|
||||
@@ -1574,7 +1511,7 @@ mod tests {
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
let (config, exists_keys, _) = process_profile_items(
|
||||
let (config, exists_keys, _) = process_override_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{process::AsyncHandler, utils::tmpl};
|
||||
|
||||
use super::field::{use_lowercase, use_lowercase_owned};
|
||||
use super::field::use_lowercase;
|
||||
use anyhow::{Error, Result};
|
||||
use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
@@ -22,7 +22,7 @@ type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>)
|
||||
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
|
||||
// The template is identity-plus-lowercasing; skip the Boa round-trip.
|
||||
if script == tmpl::ITEM_SCRIPT {
|
||||
return (use_lowercase_owned(config), vec![], vec![]);
|
||||
return (use_lowercase(&config), vec![], vec![]);
|
||||
}
|
||||
|
||||
let json = match serde_json::to_string(&use_lowercase(&config)) {
|
||||
@@ -173,7 +173,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
||||
}
|
||||
|
||||
match parse_json_safely(&result) {
|
||||
Ok(config) => (Ok(use_lowercase_owned(config)), outputs.lock().to_vec()),
|
||||
Ok(config) => (Ok(use_lowercase(&config)), outputs.lock().to_vec()),
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
|
||||
@@ -28,7 +28,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("fake-ip");
|
||||
|
||||
if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) {
|
||||
if current_mode == "fake-ip" {
|
||||
revise!(dns_val, "enable", true);
|
||||
revise!(dns_val, "ipv6", ipv6_val);
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ async fn finalize_restored_verge_config(
|
||||
webdav_username: Option<String>,
|
||||
webdav_password: Option<String>,
|
||||
) -> Result<()> {
|
||||
let config_write = Config::lock_config_write().await;
|
||||
|
||||
// A broken restored `verge.yaml` is a restore failure, not a reason to load defaults.
|
||||
let mut restored = help::read_yaml::<IVerge>(&verge_path()?).await?;
|
||||
restored.webdav_url = webdav_url;
|
||||
@@ -43,43 +45,42 @@ async fn finalize_restored_verge_config(
|
||||
let restored_clash = IClashTemp::new().await;
|
||||
let clash_draft = Config::clash().await;
|
||||
clash_draft.edit_draft(|d| {
|
||||
*d = restored_clash.clone();
|
||||
*d = restored_clash;
|
||||
});
|
||||
clash_draft.apply();
|
||||
|
||||
let restored_profiles = IProfiles::new().await;
|
||||
let profiles_draft = Config::profiles().await;
|
||||
profiles_draft.edit_draft(|d| {
|
||||
*d = restored_profiles.clone();
|
||||
*d = restored_profiles;
|
||||
});
|
||||
profiles_draft.apply();
|
||||
|
||||
let verge_draft = Config::verge().await;
|
||||
{
|
||||
// Hold the write lock so a concurrent patch cannot stage a draft the proxy write reads.
|
||||
let _config_write = Config::lock_config_write().await;
|
||||
verge_draft.edit_draft(|d| {
|
||||
*d = restored.clone();
|
||||
});
|
||||
verge_draft.apply();
|
||||
verge_draft.edit_draft(|d| {
|
||||
*d = restored.clone();
|
||||
});
|
||||
verge_draft.apply();
|
||||
|
||||
// Turn it off here; a failing core side effect below would otherwise skip this step.
|
||||
if !restored.enable_system_proxy.unwrap_or_default() {
|
||||
let result = async {
|
||||
let _lifecycle = CoreManager::global().lifecycle_lock.lock().await;
|
||||
proxy_control::apply().await?;
|
||||
proxy_control::refresh_guard().await
|
||||
}
|
||||
.await;
|
||||
if let Err(err) = result {
|
||||
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
|
||||
if SystemProxyStateUnknown::is_in(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
// Turn it off here; a failing core side effect below would otherwise skip this step.
|
||||
if !restored.enable_system_proxy.unwrap_or_default() {
|
||||
let result = async {
|
||||
let _lifecycle = CoreManager::global().lifecycle_lock.lock().await;
|
||||
proxy_control::apply().await?;
|
||||
proxy_control::refresh_guard().await
|
||||
}
|
||||
.await;
|
||||
if let Err(err) = result {
|
||||
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
|
||||
if SystemProxyStateUnknown::is_in(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `patch_verge` re-acquires the configuration write lock.
|
||||
drop(config_write);
|
||||
|
||||
// Run configuration side effects without rewriting the already-restored file.
|
||||
if let Err(err) = super::patch_verge(&restored, true).await {
|
||||
logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#}");
|
||||
@@ -133,14 +134,34 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Backup creation strips the WebDAV credentials, so the live ones are kept.
|
||||
async fn restore_backup_archive(path: PathBuf) -> Result<()> {
|
||||
let (webdav_url, webdav_username, webdav_password) = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_arc();
|
||||
(
|
||||
verge.webdav_url.clone(),
|
||||
verge.webdav_username.clone(),
|
||||
verge.webdav_password.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let home_dir = app_home_dir()?;
|
||||
AsyncHandler::spawn_blocking(move || -> Result<()> {
|
||||
let file = std::fs::File::open(&path)?;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
zip.extract(&home_dir)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|join| anyhow!("backup extraction task failed: {join}"))??;
|
||||
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename))]
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let verge = Config::verge().await;
|
||||
let verge_data = verge.latest_arc();
|
||||
let webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
|
||||
let backup_storage_path = app_home_dir()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
|
||||
.join(filename.as_str());
|
||||
@@ -157,12 +178,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
err
|
||||
})?;
|
||||
|
||||
let value = backup_storage_path.clone();
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await;
|
||||
let res = restore_backup_archive(backup_storage_path.clone()).await;
|
||||
let _ = backup_storage_path.remove_if_exists().await;
|
||||
res
|
||||
}
|
||||
@@ -331,22 +347,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
|
||||
return Err(anyhow!("Backup file not found: {}", filename));
|
||||
}
|
||||
|
||||
let (webdav_url, webdav_username, webdav_password) = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_arc();
|
||||
(
|
||||
verge.webdav_url.clone(),
|
||||
verge.webdav_username.clone(),
|
||||
verge.webdav_password.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?;
|
||||
Ok(())
|
||||
restore_backup_archive(target_path).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))]
|
||||
|
||||
+17
-45
@@ -3,7 +3,7 @@ use crate::core::notify::{Refresh, announce};
|
||||
use crate::{
|
||||
config::{Config, MixedPort},
|
||||
core::{CoreManager, handle, tray},
|
||||
feat::clean_async,
|
||||
feat::run_exit_cleanup_flow,
|
||||
process::AsyncHandler,
|
||||
utils,
|
||||
};
|
||||
@@ -14,15 +14,8 @@ use smartstring::alias::String;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| {
|
||||
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("Failed to set TLS versions")
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
Arc::new(config)
|
||||
});
|
||||
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> =
|
||||
LazyLock::new(|| Arc::new(utils::network::build_webpki_rooted_tls_config().expect("Failed to set TLS versions")));
|
||||
|
||||
pub async fn restart_clash_core() {
|
||||
match CoreManager::global().restart_core().await {
|
||||
@@ -38,38 +31,9 @@ pub async fn restart_clash_core() {
|
||||
|
||||
pub async fn restart_app() {
|
||||
logging!(debug, Type::System, "启动重启应用流程");
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
Config::apply_all_and_save_file().await;
|
||||
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
"资源清理完成,退出代码: {}",
|
||||
if cleanup_result.all_success { 0 } else { 1 }
|
||||
);
|
||||
let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppRestartCoreStopFailed).await;
|
||||
|
||||
if !cleanup_result.core_stopped {
|
||||
handle::Handle::global().clear_is_exiting();
|
||||
// A failed stop may still have marked the core stopped, and exit cleanup skipped the DNS restore.
|
||||
// A core that kept running keeps its DNS, whatever an unapplied config draft says.
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let manager = CoreManager::global();
|
||||
let _lifecycle = manager.lifecycle_lock.lock().await;
|
||||
if matches!(
|
||||
*manager.get_running_mode(),
|
||||
crate::core::manager::RunningMode::NotRunning
|
||||
) {
|
||||
crate::utils::resolve::dns::sync_public_dns().await;
|
||||
}
|
||||
}
|
||||
handle::Handle::notice(
|
||||
NoticeStatus::AppRestartCoreStopFailed,
|
||||
cleanup_result.stop_error.unwrap_or_default(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,12 +66,20 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
|
||||
return Err(err.to_string().into());
|
||||
}
|
||||
|
||||
let clash = Config::clash().await;
|
||||
clash.edit_draft(|d| d.patch_config(&mapping));
|
||||
clash.apply();
|
||||
if mode.as_str() == "global"
|
||||
&& let Err(err) = crate::config::profiles::leave_default_global_exit().await
|
||||
{
|
||||
logging!(warn, Type::Core, "failed to move GLOBAL off its default: {err:#}");
|
||||
}
|
||||
|
||||
let clash_data = clash.data_arc();
|
||||
if clash_data.save_config().await.is_ok() {
|
||||
let clash = Config::clash().await;
|
||||
let save_result = {
|
||||
let _config_write = Config::lock_config_write().await;
|
||||
clash.edit_draft(|d| d.patch_config(&mapping));
|
||||
clash.apply();
|
||||
clash.data_arc().save_config().await
|
||||
};
|
||||
if save_result.is_ok() {
|
||||
announce(Refresh::Clash);
|
||||
tray::Tray::global().update_menu_and_icon().await;
|
||||
}
|
||||
|
||||
@@ -47,26 +47,11 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
|
||||
record_switched_node(group_name, proxy_name).await;
|
||||
announce(Refresh::Proxies);
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Tray, "切换代理失败: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
match handle::Handle::mihomo()
|
||||
.select_node_for_group(group_name, proxy_name)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Tray, "代理切换回退成功: {} -> {}", group_name, proxy_name);
|
||||
record_switched_node(group_name, proxy_name).await;
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Tray, "代理切换最终失败: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> {
|
||||
|
||||
@@ -130,9 +130,7 @@ pub async fn open_or_close_dashboard() {
|
||||
logging!(info, Type::Window, "Window toggle result: {result:?}");
|
||||
}
|
||||
|
||||
pub async fn quit() {
|
||||
logging!(debug, Type::System, "启动退出流程");
|
||||
// 设置退出标志
|
||||
pub(crate) async fn run_exit_cleanup_flow(stop_failed_notice: NoticeStatus) -> CleanupResult {
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
Config::apply_all_and_save_file().await;
|
||||
@@ -162,11 +160,18 @@ pub async fn quit() {
|
||||
}
|
||||
}
|
||||
handle::Handle::notice(
|
||||
NoticeStatus::AppQuitCoreStopFailed,
|
||||
cleanup_result.stop_error.unwrap_or_default(),
|
||||
stop_failed_notice,
|
||||
cleanup_result.stop_error.clone().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
cleanup_result
|
||||
}
|
||||
|
||||
pub async fn quit() {
|
||||
logging!(debug, Type::System, "启动退出流程");
|
||||
let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppQuitCoreStopFailed).await;
|
||||
|
||||
utils::server::shutdown_embedded_server();
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
|
||||
|
||||
@@ -54,7 +54,6 @@ mod app_init {
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
|
||||
+285
-2
@@ -7,6 +7,8 @@ use scopeguard::{ScopeGuard, guard};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
@@ -45,8 +47,8 @@ pub async fn read_mapping(path: &Path) -> Result<Mapping> {
|
||||
}
|
||||
|
||||
fn parse_mapping(yaml_str: String, path: PathBuf) -> Result<Mapping> {
|
||||
match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&yaml_str) {
|
||||
Ok(mut val) => {
|
||||
match short_id_tolerant(&yaml_str, &path) {
|
||||
Ok((_, mut val)) => {
|
||||
val.apply_merge()
|
||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
|
||||
@@ -66,6 +68,150 @@ fn parse_mapping(yaml_str: String, path: PathBuf) -> Result<Mapping> {
|
||||
}
|
||||
}
|
||||
|
||||
const SHORT_ID_KEY: &str = "short-id:";
|
||||
|
||||
/// A float like `1234e123` loses its hex text, so quote short-ids before parsing and keep that only if nothing else changed.
|
||||
pub(crate) fn short_id_tolerant<'a>(yaml: &'a str, path: &Path) -> serde_yaml_ng::Result<(Cow<'a, str>, Value)> {
|
||||
let original = serde_yaml_ng::from_str::<Value>(yaml)?;
|
||||
if !has_numeric_short_id(&original) {
|
||||
return Ok((Cow::Borrowed(yaml), original));
|
||||
}
|
||||
let quoted = quote_plain_short_ids(yaml);
|
||||
if matches!(quoted, Cow::Borrowed(_)) {
|
||||
return Ok((quoted, original));
|
||||
}
|
||||
|
||||
match serde_yaml_ng::from_str::<Value>("ed) {
|
||||
Ok(tolerant) if same_except_short_id_text(&original, &tolerant) => Ok((quoted, tolerant)),
|
||||
_ => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"kept numeric short-id in {}: quoting it would change other values",
|
||||
path.display()
|
||||
);
|
||||
Ok((Cow::Borrowed(yaml), original))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_numeric_short_id(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::Mapping(map) => map
|
||||
.iter()
|
||||
.any(|(key, value)| (is_short_id_key(key) && value.is_number()) || has_numeric_short_id(value)),
|
||||
Value::Sequence(seq) => seq.iter().any(has_numeric_short_id),
|
||||
Value::Tagged(tagged) => has_numeric_short_id(&tagged.value),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn same_except_short_id_text(original: &Value, tolerant: &Value) -> bool {
|
||||
match (original, tolerant) {
|
||||
(Value::Mapping(original), Value::Mapping(tolerant)) => {
|
||||
original.len() == tolerant.len()
|
||||
&& original
|
||||
.iter()
|
||||
.zip(tolerant.iter())
|
||||
.all(|((key, old), (new_key, new))| {
|
||||
key == new_key && (same_except_short_id_text(old, new) || is_short_id_text(key, old, new))
|
||||
})
|
||||
}
|
||||
(Value::Sequence(original), Value::Sequence(tolerant)) => {
|
||||
original.len() == tolerant.len()
|
||||
&& original
|
||||
.iter()
|
||||
.zip(tolerant.iter())
|
||||
.all(|(old, new)| same_except_short_id_text(old, new))
|
||||
}
|
||||
(Value::Tagged(original), Value::Tagged(tolerant)) => {
|
||||
original.tag == tolerant.tag && same_except_short_id_text(&original.value, &tolerant.value)
|
||||
}
|
||||
_ => original == tolerant,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_short_id_text(key: &Value, old: &Value, new: &Value) -> bool {
|
||||
is_short_id_key(key)
|
||||
&& old.is_number()
|
||||
&& matches!(new, Value::String(text) if serde_yaml_ng::from_str::<Value>(text).ok().as_ref() == Some(old))
|
||||
}
|
||||
|
||||
fn is_short_id_key(key: &Value) -> bool {
|
||||
key.as_str() == Some("short-id")
|
||||
}
|
||||
|
||||
fn quote_plain_short_ids(yaml: &str) -> Cow<'_, str> {
|
||||
let mut quoted = String::new();
|
||||
let mut copied = 0;
|
||||
for (key, _) in yaml.match_indices(SHORT_ID_KEY) {
|
||||
let Some(value) = plain_short_id_at(yaml, key) else {
|
||||
continue;
|
||||
};
|
||||
let (Some(head), Some(id)) = (yaml.get(copied..value.start), yaml.get(value.clone())) else {
|
||||
continue;
|
||||
};
|
||||
quoted.push_str(head);
|
||||
quoted.push('"');
|
||||
quoted.push_str(id);
|
||||
quoted.push('"');
|
||||
copied = value.end;
|
||||
}
|
||||
|
||||
if copied == 0 {
|
||||
return Cow::Borrowed(yaml);
|
||||
}
|
||||
quoted.push_str(yaml.get(copied..).unwrap_or_default());
|
||||
Cow::Owned(quoted)
|
||||
}
|
||||
|
||||
fn plain_short_id_at(yaml: &str, key: usize) -> Option<Range<usize>> {
|
||||
let bytes = yaml.as_bytes();
|
||||
let before = bytes.get(..key)?;
|
||||
let flow = matches!(
|
||||
before.iter().rev().find(|byte| !byte.is_ascii_whitespace()),
|
||||
Some(b'{' | b',')
|
||||
);
|
||||
let line_start = before
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|&&byte| byte != b'\n')
|
||||
.all(|&byte| is_blank(byte));
|
||||
if !flow && !line_start {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start = key + SHORT_ID_KEY.len();
|
||||
let gap = bytes.get(start..)?.iter().take_while(|&&byte| is_blank(byte)).count();
|
||||
if gap == 0 {
|
||||
return None;
|
||||
}
|
||||
let start = start + gap;
|
||||
let len = bytes
|
||||
.get(start..)?
|
||||
.iter()
|
||||
.take_while(|byte| byte.is_ascii_hexdigit())
|
||||
.count();
|
||||
if len == 0 || !ends_plain_short_id(bytes.get(start + len..)?, flow) {
|
||||
return None;
|
||||
}
|
||||
Some(start..start + len)
|
||||
}
|
||||
|
||||
fn ends_plain_short_id(rest: &[u8], flow: bool) -> bool {
|
||||
let blanks = rest.iter().take_while(|&&byte| is_blank(byte)).count();
|
||||
match rest.get(blanks..) {
|
||||
Some([] | [b'\n', ..] | [b'\r', b'\n', ..]) => true,
|
||||
Some([b'#', ..]) => blanks > 0,
|
||||
Some([b',' | b'}', ..]) => flow,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_blank(byte: u8) -> bool {
|
||||
matches!(byte, b' ' | b'\t')
|
||||
}
|
||||
|
||||
pub async fn save_yaml<T: Serialize + Sync>(path: &Path, data: &T, prefix: Option<&str>) -> Result<()> {
|
||||
let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?;
|
||||
|
||||
@@ -319,3 +465,140 @@ pub fn snapshot_path(original_path: &Path) -> Result<PathBuf> {
|
||||
|
||||
Ok(temp_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
|
||||
mod tests {
|
||||
use super::{has_numeric_short_id, parse_mapping, quote_plain_short_ids, short_id_tolerant};
|
||||
use serde_yaml_ng::Value;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn quotes_plain_short_ids() {
|
||||
let cases = [
|
||||
("short-id: 1234e123", "short-id: \"1234e123\""),
|
||||
("short-id: 0e5", "short-id: \"0e5\""),
|
||||
("short-id: 12345678", "short-id: \"12345678\""),
|
||||
("short-id: 6ba85179", "short-id: \"6ba85179\""),
|
||||
("short-id: \"1234e123\"", "short-id: \"1234e123\""),
|
||||
("short-id: '1234e123'", "short-id: '1234e123'"),
|
||||
("short-id: \"\"", "short-id: \"\""),
|
||||
("short-id:\npublic-key: k", "short-id:\npublic-key: k"),
|
||||
("short-id: null", "short-id: null"),
|
||||
("short-id: ~", "short-id: ~"),
|
||||
("short-id: 1234e123 # note", "short-id: \"1234e123\" # note"),
|
||||
("# short-id: 1234e123", "# short-id: 1234e123"),
|
||||
(
|
||||
"reality-opts: {public-key: k, short-id: 1234e123}",
|
||||
"reality-opts: {public-key: k, short-id: \"1234e123\"}",
|
||||
),
|
||||
(
|
||||
"reality-opts: {short-id: 1234e123, public-key: k}",
|
||||
"reality-opts: {short-id: \"1234e123\", public-key: k}",
|
||||
),
|
||||
(
|
||||
"{\n public-key: k,\n short-id: 1234e123\n}",
|
||||
"{\n public-key: k,\n short-id: \"1234e123\"\n}",
|
||||
),
|
||||
("short-id: &sid 1234e123", "short-id: &sid 1234e123"),
|
||||
("short-id: !!str 1234e123", "short-id: !!str 1234e123"),
|
||||
("short-id: abc,def", "short-id: abc,def"),
|
||||
("short-id: 1234e123\r\n", "short-id: \"1234e123\"\r\n"),
|
||||
("my-short-id: 1234e123", "my-short-id: 1234e123"),
|
||||
("xshort-id: 1", "xshort-id: 1"),
|
||||
("short-id:1234e123", "short-id:1234e123"),
|
||||
("short-id: 1.5e3", "short-id: 1.5e3"),
|
||||
];
|
||||
|
||||
for (input, expected) in cases {
|
||||
let output = quote_plain_short_ids(input);
|
||||
assert_eq!(output, expected, "input: {input:?}");
|
||||
assert_eq!(
|
||||
matches!(output, Cow::Borrowed(_)),
|
||||
input == expected,
|
||||
"input: {input:?}"
|
||||
);
|
||||
assert_eq!(quote_plain_short_ids(&output), expected, "rerun: {input:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_short_id_survives_profile_parse() {
|
||||
let profile = "\
|
||||
proxies:
|
||||
- name: r
|
||||
type: vless
|
||||
reality-opts:
|
||||
public-key: SFv3pZQdZ1fpnNg9yI2RwXKqAOz4mZHRbT2Wq4kz8Ws
|
||||
short-id: 1234e123
|
||||
";
|
||||
let mapping = parse_mapping(profile.into(), PathBuf::from("profile.yaml")).expect("profile should parse");
|
||||
let short_id = mapping
|
||||
.get("proxies")
|
||||
.and_then(|proxies| proxies.get(0))
|
||||
.and_then(|proxy| proxy.get("reality-opts"))
|
||||
.and_then(|opts| opts.get("short-id"));
|
||||
assert_eq!(short_id, Some(&Value::String("1234e123".into())));
|
||||
|
||||
let output = serde_yaml_ng::to_string(&mapping).expect("mapping should serialize");
|
||||
assert!(output.contains("short-id: '1234e123'"), "{output}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_id_rewrite_falls_back_to_original_parse() {
|
||||
// (case, input, has a numeric short-id today)
|
||||
let cases = [
|
||||
("B2", "proxies:\n - name: \"node, short-id: 1234e123}\"\n", false),
|
||||
(
|
||||
"B3",
|
||||
"proxies:\n - name: \"node, short-id: 1234e123}\"\n - name: r\n reality-opts:\n short-id: 1234e123\n",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"B4",
|
||||
"payload: |\n short-id: 1234e123\nreality-opts:\n short-id: 1234e123\n",
|
||||
true,
|
||||
),
|
||||
("B5", "reality-opts:\n short-id: 1234e123\n more\n", false),
|
||||
("B6", "reality-opts: {public-key: k, # c\n short-id: 1234e123}\n", true),
|
||||
];
|
||||
|
||||
for (case, input, numeric) in cases {
|
||||
let today = serde_yaml_ng::from_str::<Value>(input).expect("input should parse");
|
||||
assert_eq!(has_numeric_short_id(&today), numeric, "{case}");
|
||||
let (text, value) = short_id_tolerant(input, Path::new("b.yaml")).expect("input should parse");
|
||||
assert!(matches!(text, Cow::Borrowed(_)), "{case}");
|
||||
assert_eq!(value, today, "{case}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_id_rewrite_keeps_every_numeric_short_id() {
|
||||
let input = "\
|
||||
proxies:
|
||||
- name: a
|
||||
reality-opts:
|
||||
short-id: 1234e123
|
||||
- {name: b, reality-opts: {short-id: 0e5}}
|
||||
";
|
||||
let (text, value) = short_id_tolerant(input, Path::new("b7.yaml")).expect("input should parse");
|
||||
assert!(matches!(text, Cow::Owned(_)));
|
||||
let short_ids: Vec<_> = value
|
||||
.get("proxies")
|
||||
.and_then(Value::as_sequence)
|
||||
.expect("proxies should be a sequence")
|
||||
.iter()
|
||||
.map(|proxy| proxy.get("reality-opts").and_then(|opts| opts.get("short-id")).cloned())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
short_ids,
|
||||
[
|
||||
Some(Value::String("1234e123".into())),
|
||||
Some(Value::String("0e5".into()))
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +354,6 @@ pub(super) async fn init_dns_config() -> Result<()> {
|
||||
Value::String("*.arpa".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("ntp.*.com".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("+.market.xiaomi.com".into()),
|
||||
Value::String("localhost.ptlogin2.qq.com".into()),
|
||||
Value::String("*.msftncsi.com".into()),
|
||||
@@ -509,11 +508,9 @@ pub async fn init_resources() -> Result<()> {
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let res_dir = dirs::app_resources_dir()?;
|
||||
|
||||
if !app_dir.exists() {
|
||||
std::mem::drop(fs::create_dir_all(&app_dir).await);
|
||||
}
|
||||
// app_home is created by `ensure_directories` in `init_config`, which always runs first.
|
||||
if !res_dir.exists() {
|
||||
std::mem::drop(fs::create_dir_all(&res_dir).await);
|
||||
fs::create_dir_all(&res_dir).await?;
|
||||
}
|
||||
|
||||
let file_list = ["Country.mmdb", "ASN.mmdb", "geoip.dat", "geosite.dat"];
|
||||
|
||||
@@ -48,6 +48,17 @@ enum TlsRootMode {
|
||||
StaticWebpkiRoots,
|
||||
}
|
||||
|
||||
/// ALPN stays unset: raw TLS connectors offer none, while reqwest needs the h2 offer.
|
||||
pub(crate) fn build_webpki_rooted_tls_config() -> Result<rustls::ClientConfig> {
|
||||
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub struct NetworkManager;
|
||||
|
||||
impl NetworkManager {
|
||||
@@ -102,13 +113,7 @@ impl NetworkManager {
|
||||
}
|
||||
|
||||
fn build_static_webpki_tls_config() -> Result<rustls::ClientConfig> {
|
||||
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let mut config =
|
||||
rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
let mut config = build_webpki_rooted_tls_config()?;
|
||||
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||
|
||||
Ok(config)
|
||||
@@ -155,6 +160,23 @@ impl NetworkManager {
|
||||
detail.contains("protocolversion") || detail.contains("protocol version")
|
||||
}
|
||||
|
||||
/// `allow_fallback` stays false when the caller already disabled certificate verification.
|
||||
async fn request_with_static_webpki_fallback<T, F, Fut>(allow_fallback: bool, mut request: F) -> Result<T>
|
||||
where
|
||||
F: FnMut(TlsRootMode) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<T>>,
|
||||
{
|
||||
match request(TlsRootMode::PlatformVerifier).await {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) if allow_fallback && Self::should_retry_with_static_webpki_roots(&err) => {
|
||||
request(TlsRootMode::StaticWebpkiRoots).await.map_err(|fallback_err| {
|
||||
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
|
||||
})
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_with_tls_mode(
|
||||
&self,
|
||||
url: &str,
|
||||
@@ -264,34 +286,21 @@ impl NetworkManager {
|
||||
user_agent: Option<String>,
|
||||
accept_invalid_certs: bool,
|
||||
) -> Result<HttpResponse> {
|
||||
let platform_result = self
|
||||
.get_with_tls_mode(
|
||||
url,
|
||||
proxy_type,
|
||||
timeout_secs,
|
||||
user_agent.clone(),
|
||||
accept_invalid_certs,
|
||||
TlsRootMode::PlatformVerifier,
|
||||
)
|
||||
.await;
|
||||
|
||||
match platform_result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) if !accept_invalid_certs && Self::should_retry_with_static_webpki_roots(&err) => self
|
||||
.get_with_tls_mode(
|
||||
Self::request_with_static_webpki_fallback(!accept_invalid_certs, |tls_root_mode| {
|
||||
let user_agent = user_agent.clone();
|
||||
async move {
|
||||
self.get_with_tls_mode(
|
||||
url,
|
||||
proxy_type,
|
||||
timeout_secs,
|
||||
user_agent,
|
||||
accept_invalid_certs,
|
||||
TlsRootMode::StaticWebpkiRoots,
|
||||
tls_root_mode,
|
||||
)
|
||||
.await
|
||||
.map_err(|fallback_err| {
|
||||
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_bytes_with_tls_mode(
|
||||
@@ -340,19 +349,10 @@ impl NetworkManager {
|
||||
timeout_secs: Option<u64>,
|
||||
max_bytes: usize,
|
||||
) -> Result<Vec<u8>> {
|
||||
let platform_result = self
|
||||
.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::PlatformVerifier)
|
||||
.await;
|
||||
|
||||
match platform_result {
|
||||
Ok(body) => Ok(body),
|
||||
Err(err) if Self::should_retry_with_static_webpki_roots(&err) => self
|
||||
.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::StaticWebpkiRoots)
|
||||
Self::request_with_static_webpki_fallback(true, |tls_root_mode| async move {
|
||||
self.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, tls_root_mode)
|
||||
.await
|
||||
.map_err(|fallback_err| {
|
||||
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,14 @@ use serde_yaml_ng::{Mapping, Value};
|
||||
use std::{
|
||||
io::Read as _,
|
||||
os::unix::process::CommandExt as _,
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const DNS_STATE_FILE: &str = ".original_dns.txt";
|
||||
const DNS_BACKUP_DIR: &str = ".original_dns";
|
||||
// Any address routed into TUN works, since dns-hijack answers it; the value only matters if left behind.
|
||||
const PLACEHOLDER_DNS: &str = "114.114.114.114";
|
||||
const PLACEHOLDER_DNS: &str = "114.114.114.114\n223.5.5.5\n1.1.1.1";
|
||||
// Scripts run under lifecycle_lock, and networksetup waits for other preference writers without a limit.
|
||||
pub(crate) const DNS_SCRIPT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
@@ -26,14 +24,6 @@ fn dns_state_dir() -> anyhow::Result<PathBuf> {
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
fn restore_dns_state_dir(resource_dir: &Path, state_dir: PathBuf) -> PathBuf {
|
||||
if resource_dir.join(DNS_STATE_FILE).exists() || resource_dir.join(DNS_BACKUP_DIR).exists() {
|
||||
resource_dir.to_path_buf()
|
||||
} else {
|
||||
state_dir
|
||||
}
|
||||
}
|
||||
|
||||
// Without the placeholder, a LAN resolver is reached outside TUN and fake-ip is bypassed.
|
||||
fn needs_public_dns(config: &Mapping) -> bool {
|
||||
let section = |name: &str| config.get(name).and_then(Value::as_mapping);
|
||||
@@ -79,25 +69,19 @@ pub async fn restore_public_dns() -> anyhow::Result<()> {
|
||||
|
||||
async fn set_public_dns() {
|
||||
logging!(debug, Type::Config, "try to set system dns");
|
||||
let result = async {
|
||||
let script = crate::utils::dirs::app_resources_dir()?.join("set_dns.sh");
|
||||
run_dns_script(&script, dns_state_dir()?).await
|
||||
}
|
||||
.await;
|
||||
let result = run_dns_script("set_dns.sh", true).await;
|
||||
match result {
|
||||
Ok(()) => logging!(info, Type::Config, "set system dns successfully"),
|
||||
Ok(DnsScriptOutcome::Complete) => logging!(info, Type::Config, "set system dns successfully"),
|
||||
Ok(DnsScriptOutcome::Deferred) => {
|
||||
logging!(debug, Type::Config, "system DNS waits for a primary network service")
|
||||
}
|
||||
Err(err) => logging!(error, Type::Config, "set system dns failed: {err:#}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn unset_public_dns() -> anyhow::Result<()> {
|
||||
logging!(debug, Type::Config, "try to unset system dns");
|
||||
let result = async {
|
||||
let resource_dir = crate::utils::dirs::app_resources_dir()?;
|
||||
let state_dir = restore_dns_state_dir(&resource_dir, dns_state_dir()?);
|
||||
run_dns_script(&resource_dir.join("unset_dns.sh"), state_dir).await
|
||||
}
|
||||
.await;
|
||||
let result = run_dns_script("unset_dns.sh", false).await.map(|_| ());
|
||||
match &result {
|
||||
Ok(()) => logging!(info, Type::Config, "unset system dns successfully"),
|
||||
Err(err) => logging!(error, Type::Config, "unset system dns failed: {err:#}"),
|
||||
@@ -105,12 +89,21 @@ async fn unset_public_dns() -> anyhow::Result<()> {
|
||||
result
|
||||
}
|
||||
|
||||
enum DnsScriptOutcome {
|
||||
Complete,
|
||||
Deferred,
|
||||
}
|
||||
|
||||
/// On timeout the script's whole process group is killed, so no networksetup it started writes late.
|
||||
async fn run_dns_script(script: &Path, dir: PathBuf) -> anyhow::Result<()> {
|
||||
async fn run_dns_script(script_name: &str, allow_deferred: bool) -> anyhow::Result<DnsScriptOutcome> {
|
||||
let resource_dir = crate::utils::dirs::app_resources_dir()?;
|
||||
let script = resource_dir.join(script_name);
|
||||
let dir = dns_state_dir()?;
|
||||
ensure!(script.exists(), "DNS script not found: {}", script.display());
|
||||
let mut child = Command::new("bash")
|
||||
.arg(script)
|
||||
.arg(&script)
|
||||
.arg(PLACEHOLDER_DNS)
|
||||
.arg(resource_dir)
|
||||
.current_dir(dir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
@@ -134,41 +127,18 @@ async fn run_dns_script(script: &Path, dir: PathBuf) -> anyhow::Result<()> {
|
||||
if let Some(mut pipe) = child.stderr.take() {
|
||||
pipe.read_to_string(&mut stderr)?;
|
||||
}
|
||||
// EX_TEMPFAIL is reserved by set_dns.sh for a network service that is not ready.
|
||||
if allow_deferred && status.code() == Some(75) {
|
||||
return Ok(DnsScriptOutcome::Deferred);
|
||||
}
|
||||
ensure!(
|
||||
status.success(),
|
||||
"exit code {}: {}",
|
||||
status.code().unwrap_or(-1),
|
||||
stderr.trim()
|
||||
);
|
||||
Ok(())
|
||||
Ok(DnsScriptOutcome::Complete)
|
||||
})
|
||||
.await
|
||||
.context("DNS script task failed")?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn restore_dns_state_dir_defaults_to_app_data_but_honors_legacy_file() {
|
||||
use super::restore_dns_state_dir;
|
||||
|
||||
let root = std::env::temp_dir().join(format!("clash-verge-dns-{}", nanoid::nanoid!()));
|
||||
let resource_dir = root.join("resources");
|
||||
let state_dir = root.join("app-data");
|
||||
std::fs::create_dir_all(&resource_dir).expect("create test resource directory");
|
||||
|
||||
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir.clone()), state_dir);
|
||||
|
||||
std::fs::write(resource_dir.join(".original_dns.txt"), "empty").expect("create legacy DNS state");
|
||||
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir.clone()), resource_dir);
|
||||
|
||||
// A migrated legacy backup whose restore failed must still be retried there.
|
||||
std::fs::remove_file(resource_dir.join(".original_dns.txt")).expect("remove legacy DNS state");
|
||||
std::fs::create_dir(resource_dir.join(".original_dns")).expect("create migrated DNS state");
|
||||
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir), resource_dir);
|
||||
|
||||
std::fs::remove_dir_all(root).expect("remove test directory");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::core::notify::NoticeStatus;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{
|
||||
@@ -97,18 +95,6 @@ async fn resolve_setup() {
|
||||
resolve_done();
|
||||
}
|
||||
|
||||
pub async fn resolve_reset_async() -> Result<(), anyhow::Error> {
|
||||
CoreManager::global().stop_core().await?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use dns::restore_public_dns;
|
||||
restore_public_dns().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_scheme() {
|
||||
logging_error!(Type::Setup, init::init_scheme());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.5.10",
|
||||
"version": "2.6.0",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
@@ -33,9 +33,9 @@
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
||||
"endpoints": [
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
|
||||
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
|
||||
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json"
|
||||
],
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
|
||||
@@ -26,8 +26,9 @@
|
||||
"active": true,
|
||||
"dialog": false,
|
||||
"endpoints": [
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
|
||||
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
|
||||
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@
|
||||
"active": true,
|
||||
"dialog": false,
|
||||
"endpoints": [
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
|
||||
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
|
||||
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@
|
||||
"active": true,
|
||||
"dialog": false,
|
||||
"endpoints": [
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
|
||||
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
|
||||
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -464,21 +464,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
]
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
setColumnOrder((prevValue) => {
|
||||
const baseFields = baseColumns.map((col) => col.field)
|
||||
const prev = Array.isArray(prevValue) ? prevValue : []
|
||||
const reconciled = reconcileColumnOrder(prev, baseFields)
|
||||
if (
|
||||
reconciled.length === prev.length &&
|
||||
reconciled.every((field, i) => field === prev[i])
|
||||
) {
|
||||
return prevValue
|
||||
}
|
||||
return reconciled
|
||||
})
|
||||
}, [baseColumns, setColumnOrder])
|
||||
|
||||
const orderedColumns = useMemo(() => {
|
||||
const baseFields = baseColumns.map((column) => column.field)
|
||||
const reconciledOrder = reconcileColumnOrder(columnOrder, baseFields)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DeveloperBoardOutlined } from '@mui/icons-material'
|
||||
import { Divider, Stack, Typography } from '@mui/material'
|
||||
import { useMemo } from 'react'
|
||||
import { DeveloperBoardOutlined, RestartAltRounded } from '@mui/icons-material'
|
||||
import { Divider, IconButton, Stack, Tooltip, Typography } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useClash } from '@/hooks/use-clash'
|
||||
@@ -11,6 +12,8 @@ import {
|
||||
useRulesData,
|
||||
useSystemData,
|
||||
} from '@/providers/app-data-context'
|
||||
import { restartCore } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
|
||||
import { EnhancedCard } from './enhanced-card'
|
||||
|
||||
@@ -30,6 +33,23 @@ export const ClashInfoCard = () => {
|
||||
const { rules } = useRulesData()
|
||||
const uptime = useAppUptime()
|
||||
const { systemProxyAddress } = useSystemData()
|
||||
const [restarting, setRestarting] = useState(false)
|
||||
|
||||
const onRestart = useLockFn(async () => {
|
||||
setRestarting(true)
|
||||
try {
|
||||
await mutate(() => restartCore(), {
|
||||
id: 'restart-core',
|
||||
successNotice: t(
|
||||
'settings.feedback.notifications.clash.restartSuccess',
|
||||
),
|
||||
})
|
||||
} catch {
|
||||
// the funnel already notified
|
||||
} finally {
|
||||
setRestarting(false)
|
||||
}
|
||||
})
|
||||
|
||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime])
|
||||
@@ -101,7 +121,20 @@ export const ClashInfoCard = () => {
|
||||
title={t('home.components.clashInfo.title')}
|
||||
icon={<DeveloperBoardOutlined />}
|
||||
iconColor="warning"
|
||||
action={null}
|
||||
action={
|
||||
<Tooltip title={t('shared.actions.restart')}>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={t('shared.actions.restart')}
|
||||
loading={restarting}
|
||||
onClick={onRestart}
|
||||
>
|
||||
<RestartAltRounded />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{cardContent}
|
||||
</EnhancedCard>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @eslint-react/set-state-in-effect */
|
||||
import {
|
||||
AccessTimeRounded,
|
||||
ArrowDropDown,
|
||||
@@ -31,7 +30,15 @@ import {
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
@@ -570,20 +577,22 @@ export const CurrentProxyCard = () => {
|
||||
return profileValue
|
||||
}
|
||||
|
||||
if (profileKey !== baseKey) {
|
||||
const legacyValue = localStorage.getItem(baseKey)
|
||||
if (legacyValue != null) {
|
||||
localStorage.removeItem(baseKey)
|
||||
localStorage.setItem(profileKey, legacyValue)
|
||||
return legacyValue
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return profileKey !== baseKey ? localStorage.getItem(baseKey) : null
|
||||
},
|
||||
[getProfileStorageKey],
|
||||
)
|
||||
|
||||
// Hand the pre-profile-scoping group key to the first profile that shows the card.
|
||||
useEffect(() => {
|
||||
const profileKey = getProfileStorageKey(STORAGE_KEY_GROUP)
|
||||
if (profileKey === STORAGE_KEY_GROUP) return
|
||||
if (localStorage.getItem(profileKey) != null) return
|
||||
const legacyValue = localStorage.getItem(STORAGE_KEY_GROUP)
|
||||
if (legacyValue == null) return
|
||||
localStorage.removeItem(STORAGE_KEY_GROUP)
|
||||
localStorage.setItem(profileKey, legacyValue)
|
||||
}, [getProfileStorageKey])
|
||||
|
||||
const writeProfileScopedItem = useCallback(
|
||||
(baseKey: string, value: string) => {
|
||||
if (typeof window === 'undefined') return
|
||||
@@ -596,7 +605,7 @@ export const CurrentProxyCard = () => {
|
||||
[getProfileStorageKey],
|
||||
)
|
||||
|
||||
const { handleSelectChange } = useProxySelection({
|
||||
const { select } = useProxySelection({
|
||||
onSuccess: () => {
|
||||
refreshProxy()
|
||||
},
|
||||
@@ -615,25 +624,17 @@ export const CurrentProxyCard = () => {
|
||||
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0
|
||||
})
|
||||
|
||||
const [selectedGroupName, setSelectedGroupName] = useState('')
|
||||
const [lastGroupName, setLastGroupName] = useState('')
|
||||
const [openSelect, setOpenSelect] = useState<OpenSelect>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const [openSelectMode, setOpenSelectMode] = useState(mode)
|
||||
if (openSelectMode !== mode) {
|
||||
setOpenSelectMode(mode)
|
||||
setOpenSelect(null)
|
||||
}, [mode])
|
||||
}
|
||||
|
||||
const delayButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const delays = useGroupDelays(selectedGroupName || null)
|
||||
|
||||
const autoCheckInProgressRef = useRef(false)
|
||||
const latestTimeoutRef = useRef<number>(
|
||||
verge?.default_latency_timeout || 10000,
|
||||
)
|
||||
const latestProxyMemberRef = useRef<ResolvedProxyMember | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
latestTimeoutRef.current = verge?.default_latency_timeout || 10000
|
||||
}, [verge?.default_latency_timeout])
|
||||
|
||||
const selectableGroups = useMemo(() => {
|
||||
if (!proxyView) return []
|
||||
@@ -644,6 +645,39 @@ export const CurrentProxyCard = () => {
|
||||
)
|
||||
}, [proxyView])
|
||||
|
||||
const selectedGroupName = useMemo(() => {
|
||||
if (isDirectMode) return 'DIRECT'
|
||||
if (!proxyView) return lastGroupName
|
||||
if (isGlobalMode) return proxyView.global?.name ?? 'GLOBAL'
|
||||
|
||||
const hasGroup = (name: string | null) =>
|
||||
!!name && selectableGroups.some((group) => group.name === name)
|
||||
if (hasGroup(lastGroupName)) return lastGroupName
|
||||
const savedGroup = readProfileScopedItem(STORAGE_KEY_GROUP)
|
||||
if (hasGroup(savedGroup)) return savedGroup!
|
||||
|
||||
const primaryKeywords = ['auto', 'select', 'proxy', '节点选择', '自动选择']
|
||||
const primaryGroup =
|
||||
selectableGroups.find((group) =>
|
||||
primaryKeywords.some((keyword) =>
|
||||
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||
),
|
||||
) ?? selectableGroups[0]
|
||||
return primaryGroup?.name ?? ''
|
||||
}, [
|
||||
isDirectMode,
|
||||
isGlobalMode,
|
||||
lastGroupName,
|
||||
proxyView,
|
||||
readProfileScopedItem,
|
||||
selectableGroups,
|
||||
])
|
||||
// Keep the shown group, auto picks included, until it leaves the profile.
|
||||
if (proxyView && lastGroupName !== selectedGroupName) {
|
||||
setLastGroupName(selectedGroupName)
|
||||
}
|
||||
const delays = useGroupDelays(selectedGroupName || null)
|
||||
|
||||
const selectedGroup = useMemo<ProxyGroupView | null>(() => {
|
||||
if (!proxyView || isDirectMode) return null
|
||||
if (isGlobalMode) return proxyView.global
|
||||
@@ -680,46 +714,6 @@ export const CurrentProxyCard = () => {
|
||||
[optionsForGroup, selectedGroup],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!proxyView) return
|
||||
if (isDirectMode) {
|
||||
setSelectedGroupName('DIRECT')
|
||||
return
|
||||
}
|
||||
if (isGlobalMode) {
|
||||
setSelectedGroupName(proxyView.global?.name ?? 'GLOBAL')
|
||||
return
|
||||
}
|
||||
|
||||
const savedGroup = readProfileScopedItem(STORAGE_KEY_GROUP)
|
||||
const primaryKeywords = ['auto', 'select', 'proxy', '节点选择', '自动选择']
|
||||
const primaryGroup =
|
||||
selectableGroups.find((group) =>
|
||||
primaryKeywords.some((keyword) =>
|
||||
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||
),
|
||||
) ?? selectableGroups[0]
|
||||
const nextGroup = selectableGroups.some(
|
||||
({ name }) => name === selectedGroupName,
|
||||
)
|
||||
? selectedGroupName
|
||||
: selectableGroups.some(({ name }) => name === savedGroup)
|
||||
? savedGroup!
|
||||
: (primaryGroup?.name ?? '')
|
||||
if (nextGroup !== selectedGroupName) {
|
||||
setSelectedGroupName(nextGroup)
|
||||
if (nextGroup) writeProfileScopedItem(STORAGE_KEY_GROUP, nextGroup)
|
||||
}
|
||||
}, [
|
||||
isDirectMode,
|
||||
isGlobalMode,
|
||||
proxyView,
|
||||
readProfileScopedItem,
|
||||
selectableGroups,
|
||||
selectedGroupName,
|
||||
writeProfileScopedItem,
|
||||
])
|
||||
|
||||
const currentOption = useMemo(() => {
|
||||
if (!proxyView) return undefined
|
||||
if (isDirectMode) {
|
||||
@@ -743,12 +737,10 @@ export const CurrentProxyCard = () => {
|
||||
: undefined
|
||||
}, [isDirectMode, proxyView, selectedGroup])
|
||||
|
||||
latestProxyMemberRef.current = currentOption?.member ?? null
|
||||
|
||||
const handleGroupChange = useCallback(
|
||||
(newGroupName: string) => {
|
||||
if (isGlobalMode || isDirectMode) return
|
||||
setSelectedGroupName(newGroupName)
|
||||
setLastGroupName(newGroupName)
|
||||
writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName)
|
||||
},
|
||||
[isDirectMode, isGlobalMode, writeProfileScopedItem],
|
||||
@@ -766,15 +758,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(() => {
|
||||
@@ -799,7 +785,7 @@ export const CurrentProxyCard = () => {
|
||||
color: 'text.secondary',
|
||||
}
|
||||
|
||||
const checkCurrentProxyDelay = useCallback(async () => {
|
||||
const checkCurrentProxyDelay = useEffectEvent(async () => {
|
||||
if (autoCheckInProgressRef.current) return
|
||||
if (isDirectMode) return
|
||||
|
||||
@@ -808,7 +794,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
if (!groupName || !proxyName) return
|
||||
|
||||
const proxyMember = latestProxyMemberRef.current
|
||||
const proxyMember = currentOption?.member
|
||||
if (!proxyMember || !isInteractableMember(proxyMember)) {
|
||||
debugLog(
|
||||
`[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`,
|
||||
@@ -818,7 +804,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
autoCheckInProgressRef.current = true
|
||||
|
||||
const timeout = latestTimeoutRef.current || 10000
|
||||
const timeout = verge?.default_latency_timeout || 10000
|
||||
|
||||
try {
|
||||
debugLog(
|
||||
@@ -834,7 +820,7 @@ export const CurrentProxyCard = () => {
|
||||
autoCheckInProgressRef.current = false
|
||||
refreshProxy()
|
||||
}
|
||||
}, [isDirectMode, refreshProxy, selectedGroupName, selectedProxyName])
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isDirectMode) return
|
||||
@@ -864,7 +850,6 @@ export const CurrentProxyCard = () => {
|
||||
if (intervalTimer) clearTimeout(intervalTimer)
|
||||
}
|
||||
}, [
|
||||
checkCurrentProxyDelay,
|
||||
autoDelayIntervalMs,
|
||||
isDirectMode,
|
||||
selectedGroupName,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useReducer,
|
||||
@@ -14,7 +15,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 +23,7 @@ import {
|
||||
isSameTrafficData,
|
||||
} from '@/utils/traffic-sampler'
|
||||
|
||||
interface ITrafficItem {
|
||||
up: number
|
||||
down: number
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
export interface EnhancedCanvasTrafficGraphRef {
|
||||
appendData: (data: ITrafficItem) => void
|
||||
toggleStyle: () => void
|
||||
}
|
||||
|
||||
@@ -128,7 +121,6 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
const drawFrameRef = useRef<number | undefined>(undefined)
|
||||
const hoverFrameRef = useRef<number | undefined>(undefined)
|
||||
const mouseMoveFrameRef = useRef<number | undefined>(undefined)
|
||||
const scheduleDrawGraphRef = useRef<() => void>(() => {})
|
||||
const pendingMousePositionRef = useRef<{
|
||||
clientX: number
|
||||
clientY: number
|
||||
@@ -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 = useEffectEvent((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],
|
||||
)
|
||||
scheduleDrawGraph()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
@@ -235,7 +217,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
window.removeEventListener('blur', handleBlur)
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
}, [handleFocusStateChange])
|
||||
}, [])
|
||||
|
||||
const calculateY = useCallback(
|
||||
(
|
||||
@@ -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)
|
||||
@@ -930,25 +906,21 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
scheduleDrawGraph()
|
||||
}, [scheduleDrawGraph])
|
||||
|
||||
useEffect(() => {
|
||||
scheduleDrawGraphRef.current = scheduleDrawGraph
|
||||
}, [scheduleDrawGraph])
|
||||
const redrawGraph = useEffectEvent(() => scheduleDrawGraph())
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas || typeof window === 'undefined') return
|
||||
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
const handleResize = () => scheduleDrawGraphRef.current()
|
||||
const handleResize = () => redrawGraph()
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() =>
|
||||
scheduleDrawGraphRef.current(),
|
||||
)
|
||||
const resizeObserver = new ResizeObserver(() => redrawGraph())
|
||||
resizeObserver.observe(canvas)
|
||||
|
||||
return () => {
|
||||
@@ -984,21 +956,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 +1100,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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
useTheme,
|
||||
Fade,
|
||||
} from '@mui/material'
|
||||
import { useState, useMemo, memo, FC } from 'react'
|
||||
import { useState, useMemo, useCallback, memo, FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
|
||||
@@ -152,10 +152,15 @@ export const ProxyTunCard: FC = () => {
|
||||
showNotice.error(err)
|
||||
}
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab)
|
||||
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab)
|
||||
}
|
||||
const handleSystemTabClick = useCallback(() => {
|
||||
setActiveTab('system')
|
||||
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, 'system')
|
||||
}, [])
|
||||
|
||||
const handleTunTabClick = useCallback(() => {
|
||||
setActiveTab('tun')
|
||||
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, 'tun')
|
||||
}, [])
|
||||
|
||||
const tabDescription = useMemo(() => {
|
||||
if (activeTab === 'system') {
|
||||
@@ -191,14 +196,14 @@ export const ProxyTunCard: FC = () => {
|
||||
>
|
||||
<TabButton
|
||||
isActive={activeTab === 'system'}
|
||||
onClick={() => handleTabChange('system')}
|
||||
onClick={handleSystemTabClick}
|
||||
icon={ComputerRounded}
|
||||
label={t('settings.sections.system.toggles.systemProxy')}
|
||||
hasIndicator={systemProxyIndicator}
|
||||
/>
|
||||
<TabButton
|
||||
isActive={activeTab === 'tun'}
|
||||
onClick={() => handleTabChange('tun')}
|
||||
onClick={handleTunTabClick}
|
||||
icon={TroubleshootRounded}
|
||||
label={t('settings.sections.system.toggles.tunMode')}
|
||||
hasIndicator={enable_tun_mode! && isTunModeAvailable}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@mui/icons-material'
|
||||
import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
@@ -22,10 +22,23 @@ import {
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { getSystemInfo } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useQuery } from '@/services/query-client'
|
||||
import { version as appVersion } from '@root/package.json'
|
||||
|
||||
import { EnhancedCard } from './enhanced-card'
|
||||
|
||||
const getOsInfo = async () => {
|
||||
const info = await getSystemInfo()
|
||||
const sysName = info.system_name
|
||||
let sysVersion = info.system_version
|
||||
|
||||
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) {
|
||||
sysVersion = sysVersion.substring(sysName.length).trim()
|
||||
}
|
||||
|
||||
return `${sysName} ${sysVersion}`
|
||||
}
|
||||
|
||||
export const SystemInfoCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const { verge, patchVerge } = useVerge()
|
||||
@@ -36,31 +49,16 @@ export const SystemInfoCard = () => {
|
||||
|
||||
const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true)
|
||||
|
||||
const [osInfo, setOsInfo] = useState('')
|
||||
const { data: osInfo = '' } = useQuery({
|
||||
queryKey: ['getSystemInfo'],
|
||||
queryFn: getOsInfo,
|
||||
})
|
||||
|
||||
const lastCheckUpdateText = useMemo(
|
||||
() => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'),
|
||||
[lastCheckUpdate],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
getSystemInfo()
|
||||
.then((info) => {
|
||||
const sysName = info.system_name
|
||||
let sysVersion = info.system_version
|
||||
|
||||
if (
|
||||
sysName &&
|
||||
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
|
||||
) {
|
||||
sysVersion = sysVersion.substring(sysName.length).trim()
|
||||
}
|
||||
|
||||
setOsInfo(`${sysName} ${sysVersion}`)
|
||||
})
|
||||
.catch(console.error)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!verge?.auto_check_update) return
|
||||
if (readLastCheckTime() !== null) return
|
||||
@@ -86,16 +84,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 +115,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>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -113,7 +114,6 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const setProfileLoading = useSetProfileLoading()
|
||||
|
||||
const [showNextUpdate, setShowNextUpdate] = useState(false)
|
||||
const showNextUpdateRef = useRef(false)
|
||||
const [nextUpdateTime, setNextUpdateTime] = useState('')
|
||||
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||
undefined,
|
||||
@@ -203,10 +203,6 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
setShowNextUpdate(!showNextUpdate)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
showNextUpdateRef.current = showNextUpdate
|
||||
}, [showNextUpdate])
|
||||
|
||||
useEffect(() => {
|
||||
if (showNextUpdate) {
|
||||
fetchNextUpdateTime()
|
||||
@@ -218,8 +214,10 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
updated,
|
||||
])
|
||||
|
||||
const isNextUpdateShown = useEffectEvent(() => showNextUpdate)
|
||||
|
||||
useEffect(() => {
|
||||
if (timerUpdateRevision === 0 || !showNextUpdateRef.current) return
|
||||
if (timerUpdateRevision === 0 || !isNextUpdateShown()) return
|
||||
|
||||
if (refreshTimeoutRef.current !== undefined) {
|
||||
clearTimeout(refreshTimeoutRef.current)
|
||||
@@ -236,7 +234,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
}, [fetchNextUpdateTime, timerUpdateRevision])
|
||||
|
||||
useEffect(() => {
|
||||
if (completedUpdateRevision === 0 || !showNextUpdateRef.current) return
|
||||
if (completedUpdateRevision === 0 || !isNextUpdateShown()) return
|
||||
fetchNextUpdateTime()
|
||||
}, [completedUpdateRevision, fetchNextUpdateTime])
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import type { Ref } from 'react'
|
||||
import { useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -77,17 +77,6 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
},
|
||||
}))
|
||||
|
||||
const selfProxy = watch('option.self_proxy')
|
||||
const withProxy = watch('option.with_proxy')
|
||||
|
||||
useEffect(() => {
|
||||
if (selfProxy) setValue('option.with_proxy', false)
|
||||
}, [selfProxy, setValue])
|
||||
|
||||
useEffect(() => {
|
||||
if (withProxy) setValue('option.self_proxy', false)
|
||||
}, [setValue, withProxy])
|
||||
|
||||
const handleOk = useLockFn(
|
||||
handleSubmit(async (form) => {
|
||||
setLoading(true)
|
||||
@@ -419,7 +408,15 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
<InputLabel>
|
||||
{t('profiles.modals.profileForm.fields.useSystemProxy')}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value!} {...field} color="primary" />
|
||||
<Switch
|
||||
checked={field.value!}
|
||||
{...field}
|
||||
onChange={(_, checked) => {
|
||||
field.onChange(checked)
|
||||
if (checked) setValue('option.self_proxy', false)
|
||||
}}
|
||||
color="primary"
|
||||
/>
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
@@ -432,7 +429,15 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
<InputLabel>
|
||||
{t('profiles.modals.profileForm.fields.useClashProxy')}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value!} {...field} color="primary" />
|
||||
<Switch
|
||||
checked={field.value!}
|
||||
{...field}
|
||||
onChange={(_, checked) => {
|
||||
field.onChange(checked)
|
||||
if (checked) setValue('option.with_proxy', false)
|
||||
}}
|
||||
color="primary"
|
||||
/>
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import * as yaml from 'js-yaml'
|
||||
|
||||
import {
|
||||
getRecord,
|
||||
rebindNode,
|
||||
@@ -34,3 +36,31 @@ export const rebindProxyChainItems = (
|
||||
delay: record?.history.at(-1)?.delay,
|
||||
}
|
||||
})
|
||||
|
||||
interface ParsedChainConfig {
|
||||
proxies?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
[key: string]: any
|
||||
}>
|
||||
}
|
||||
|
||||
export const parseChainConfigItems = (configData: string): ProxyChainItem[] => {
|
||||
try {
|
||||
// JSON is valid YAML, so one parser covers both persisted formats.
|
||||
const parsedConfig = yaml.load(configData) as ParsedChainConfig | null
|
||||
const timestamp = Date.now()
|
||||
|
||||
return (
|
||||
parsedConfig?.proxies?.map((proxy, index) => ({
|
||||
id: `${proxy.name}_${timestamp}_${index}`,
|
||||
name: proxy.name,
|
||||
type: proxy.type,
|
||||
delay: undefined,
|
||||
})) || []
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to process chain config data:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
keyframes,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import * as yaml from 'js-yaml'
|
||||
import {
|
||||
type Ref,
|
||||
useCallback,
|
||||
@@ -60,18 +59,9 @@ const chainPointerSensor = PointerSensor.configure({
|
||||
|
||||
type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown }
|
||||
|
||||
interface ParsedChainConfig {
|
||||
proxies?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
[key: string]: any
|
||||
}>
|
||||
}
|
||||
|
||||
interface ProxyChainProps {
|
||||
proxyChain: ProxyChainItem[]
|
||||
onUpdateChain: (chain: ProxyChainItem[]) => void
|
||||
chainConfigData?: string | null
|
||||
onMarkUnsavedChanges?: () => void
|
||||
mode?: string
|
||||
selectedGroup?: string | null
|
||||
@@ -98,21 +88,6 @@ const arrowFadeIn = keyframes`
|
||||
}
|
||||
`
|
||||
|
||||
const toChainItems = (
|
||||
parsedConfig: ParsedChainConfig | null | undefined,
|
||||
): ProxyChainItem[] => {
|
||||
const timestamp = Date.now()
|
||||
|
||||
return (
|
||||
parsedConfig?.proxies?.map((proxy, index) => ({
|
||||
id: `${proxy.name}_${timestamp}_${index}`,
|
||||
name: proxy.name,
|
||||
type: proxy.type,
|
||||
delay: undefined,
|
||||
})) || []
|
||||
)
|
||||
}
|
||||
|
||||
interface ChainCardProps {
|
||||
proxy: ProxyChainItem
|
||||
index: number
|
||||
@@ -328,7 +303,6 @@ const SortableProxyChainItem = ({
|
||||
export const ProxyChain = ({
|
||||
proxyChain,
|
||||
onUpdateChain,
|
||||
chainConfigData,
|
||||
onMarkUnsavedChanges,
|
||||
mode,
|
||||
selectedGroup,
|
||||
@@ -580,23 +554,6 @@ export const ProxyChain = ({
|
||||
recordSelection,
|
||||
])
|
||||
|
||||
// 处理链式代理配置数据
|
||||
useEffect(() => {
|
||||
if (chainConfigData) {
|
||||
try {
|
||||
// JSON is valid YAML, so one parser covers both persisted formats.
|
||||
const parsedConfig = yaml.load(chainConfigData) as ParsedChainConfig
|
||||
const chainItems = toChainItems(parsedConfig)
|
||||
|
||||
if (chainItems.length > 0) {
|
||||
onUpdateChain(chainItems)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to process chain config data:', error)
|
||||
}
|
||||
}
|
||||
}, [chainConfigData, onUpdateChain])
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={1}
|
||||
|
||||
@@ -76,7 +76,7 @@ interface GroupSelectMenuProps {
|
||||
|
||||
interface ProxyGroupsChainProps {
|
||||
mode: string
|
||||
chainConfigData?: string | null
|
||||
chainConfigItems?: ProxyChainItem[] | null
|
||||
onExitChainMode: () => void
|
||||
availableGroups: any[]
|
||||
activeSelectedGroup: string | null
|
||||
@@ -305,7 +305,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
mode,
|
||||
chainConfigData,
|
||||
chainConfigItems,
|
||||
onExitChainMode,
|
||||
availableGroups,
|
||||
activeSelectedGroup,
|
||||
@@ -338,6 +338,16 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
return []
|
||||
})
|
||||
|
||||
// Each fetched runtime chain replaces the stored one once.
|
||||
const configItems = chainConfigItems ?? null
|
||||
const [appliedConfigItems, setAppliedConfigItems] = useState<
|
||||
ProxyChainItem[] | null
|
||||
>(null)
|
||||
if (configItems !== appliedConfigItems) {
|
||||
setAppliedConfigItems(configItems)
|
||||
if (configItems?.length) setProxyChain(configItems)
|
||||
}
|
||||
|
||||
const candidateNodes = useMemo(
|
||||
() =>
|
||||
renderList.flatMap((item) => {
|
||||
@@ -520,7 +530,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
<ProxyChain
|
||||
proxyChain={currentProxyChain}
|
||||
onUpdateChain={setProxyChain}
|
||||
chainConfigData={chainConfigData}
|
||||
mode={mode}
|
||||
selectedGroup={activeSelectedGroup}
|
||||
onExitChainMode={onExitChainMode}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from '@/types/proxy-view'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
|
||||
import type { ProxyChainItem } from './proxy-chain-model'
|
||||
import { ProxyEmptyState } from './proxy-empty-state'
|
||||
import {
|
||||
resolveEmptyListReason,
|
||||
@@ -68,7 +69,7 @@ function useStableCallback<T extends (...args: any[]) => any>(fn: T): T {
|
||||
interface Props {
|
||||
mode: string
|
||||
isChainMode?: boolean
|
||||
chainConfigData?: string | null
|
||||
chainConfigItems?: ProxyChainItem[] | null
|
||||
onExitChainMode: () => void
|
||||
}
|
||||
|
||||
@@ -183,6 +184,7 @@ function useProxyRenderState(
|
||||
renderList,
|
||||
onProxies,
|
||||
onHeadState,
|
||||
setHeadState: setListHeadState,
|
||||
handleCheckAll,
|
||||
saveScrollPosition,
|
||||
getScrollPosition,
|
||||
@@ -191,10 +193,10 @@ function useProxyRenderState(
|
||||
|
||||
function ChainProxyGroups(props: {
|
||||
mode: string
|
||||
chainConfigData?: string | null
|
||||
chainConfigItems?: ProxyChainItem[] | null
|
||||
onExitChainMode: () => void
|
||||
}) {
|
||||
const { mode, chainConfigData, onExitChainMode } = props
|
||||
const { mode, chainConfigItems, onExitChainMode } = props
|
||||
const { proxyView } = useProxiesData()
|
||||
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
|
||||
|
||||
@@ -348,7 +350,7 @@ function ChainProxyGroups(props: {
|
||||
<Suspense fallback={<BaseLoading />}>
|
||||
<ProxyGroupsChain
|
||||
mode={mode}
|
||||
chainConfigData={chainConfigData}
|
||||
chainConfigItems={chainConfigItems}
|
||||
onExitChainMode={onExitChainMode}
|
||||
availableGroups={availableGroups}
|
||||
activeSelectedGroup={activeSelectedGroup}
|
||||
@@ -377,12 +379,17 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
renderList,
|
||||
onProxies,
|
||||
onHeadState,
|
||||
setHeadState,
|
||||
handleCheckAll,
|
||||
getScrollPosition,
|
||||
saveScrollPosition,
|
||||
} = useProxyRenderState(mode, false, null)
|
||||
const emptyList = useEmptyRenderList()
|
||||
const { onDragEnd: onHeaderDragEnd } = useProxyGroupHeaderLayout()
|
||||
const { proxyView } = useProxiesData()
|
||||
const globalGroupName = proxyView?.global?.name
|
||||
// The revealed group sits below GLOBAL's rows, so a click must bring it into view.
|
||||
const pendingRevealRef = useRef<string | null>(null)
|
||||
const renderFirstRef = useRef(true)
|
||||
// Do not persist intermediate positions produced while restoring virtual scroll.
|
||||
const isRestoringRef = useRef(false)
|
||||
@@ -462,7 +469,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
}
|
||||
}, [handleScroll])
|
||||
|
||||
const { handleProxyGroupChange } = useProxySelection({
|
||||
const { select } = useProxySelection({
|
||||
onSuccess: () => {
|
||||
onProxies()
|
||||
},
|
||||
@@ -477,9 +484,17 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
|
||||
if (!isInteractableMember(member)) return
|
||||
|
||||
handleProxyGroupChange(group, { name: member.ref.name })
|
||||
if (group.name === globalGroupName) {
|
||||
pendingRevealRef.current =
|
||||
member.kind === 'group' ? member.ref.name : null
|
||||
// The selection refetches the view, so skip onHeadState's refetch.
|
||||
if (member.kind === 'group') {
|
||||
setHeadState(member.ref.name, { open: true })
|
||||
}
|
||||
}
|
||||
select(group.name, member.ref.name, group.now!, group.fixed!)
|
||||
},
|
||||
[handleProxyGroupChange],
|
||||
[globalGroupName, select, setHeadState],
|
||||
)
|
||||
|
||||
const handleLocation = useStableCallback((group: ProxyGroupView) => {
|
||||
@@ -525,6 +540,17 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
return Array.from(new Set(names))
|
||||
}, [renderList])
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'global') {
|
||||
pendingRevealRef.current = null
|
||||
return
|
||||
}
|
||||
const groupName = pendingRevealRef.current
|
||||
if (!groupName || !proxyGroupNames.includes(groupName)) return
|
||||
pendingRevealRef.current = null
|
||||
handleGroupLocationByName(groupName)
|
||||
}, [handleGroupLocationByName, mode, proxyGroupNames])
|
||||
|
||||
const handleGroupToggle = useCallback(
|
||||
async (group: ProxyGroupView) => {
|
||||
const index = renderList.findIndex(
|
||||
@@ -608,7 +634,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
/>
|
||||
</DragDropProvider>
|
||||
|
||||
{(mode === 'rule' || mode === 'global') && (
|
||||
{mode === 'rule' && (
|
||||
<ProxyGroupNavigator
|
||||
proxyGroupNames={proxyGroupNames}
|
||||
onGroupLocation={handleGroupLocationByName}
|
||||
@@ -621,7 +647,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
}
|
||||
|
||||
export const ProxyGroups = (props: Props) => {
|
||||
const { mode, isChainMode = false, chainConfigData, onExitChainMode } = props
|
||||
const { mode, isChainMode = false, chainConfigItems, onExitChainMode } = props
|
||||
const { profiles, isLoading: isProfilesLoading } = useProfiles()
|
||||
const { isProxyViewPending } = useProxiesData()
|
||||
const { isRunningModePending } = useSystemData()
|
||||
@@ -645,7 +671,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
return isChainMode ? (
|
||||
<ChainProxyGroups
|
||||
mode={mode}
|
||||
chainConfigData={chainConfigData}
|
||||
chainConfigItems={chainConfigItems}
|
||||
onExitChainMode={onExitChainMode}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useVerge } from '@/hooks/use-verge'
|
||||
import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context'
|
||||
import delayManager, { type DelaySnapshot } from '@/services/delay'
|
||||
import {
|
||||
findCurrentGroupMember,
|
||||
isInteractableMember,
|
||||
resolveMember,
|
||||
selectGlobalChainNodes,
|
||||
@@ -209,8 +210,10 @@ export const useRenderList = (
|
||||
const renderGroups = useMemo(() => {
|
||||
if (!proxyView) return []
|
||||
if (mode === 'rule') return proxyView.groups
|
||||
const global = proxyView.global ? [proxyView.global] : []
|
||||
return mode === 'global' ? [...global, ...proxyView.groups] : global
|
||||
const { global } = proxyView
|
||||
if (!global) return []
|
||||
const current = findCurrentGroupMember(proxyView, global)?.member
|
||||
return current?.kind === 'group' ? [global, current.group] : [global]
|
||||
}, [mode, proxyView])
|
||||
|
||||
// Every group this list draws, so a test settling in any of them re-sorts that group.
|
||||
@@ -260,10 +263,10 @@ export const useRenderList = (
|
||||
}))
|
||||
}
|
||||
|
||||
const useRule = mode === 'rule' || mode === 'global'
|
||||
const cache = groupCacheRef.current
|
||||
|
||||
const retList = renderGroups.flatMap((group) => {
|
||||
const isFlat = group === proxyView.global
|
||||
const headState = headStates[group.name] || DEFAULT_STATE
|
||||
const cached = cache.get(group.name)
|
||||
if (
|
||||
@@ -279,17 +282,19 @@ export const useRenderList = (
|
||||
}
|
||||
|
||||
const ret: IRenderItem[] = [
|
||||
{
|
||||
type: 0,
|
||||
key: group.name,
|
||||
group,
|
||||
headState,
|
||||
icon: group.icon!,
|
||||
testUrl: group.testUrl!,
|
||||
},
|
||||
isFlat
|
||||
? { type: 1, key: `head-${group.name}`, group, headState }
|
||||
: {
|
||||
type: 0,
|
||||
key: group.name,
|
||||
group,
|
||||
headState,
|
||||
icon: group.icon!,
|
||||
testUrl: group.testUrl!,
|
||||
},
|
||||
]
|
||||
|
||||
if (headState.open || !useRule) {
|
||||
if (headState.open || isFlat) {
|
||||
const occurrences = filterSort(
|
||||
resolveOccurrences(proxyView, group),
|
||||
group.name,
|
||||
@@ -302,9 +307,6 @@ export const useRenderList = (
|
||||
useRegularExpression: headState.filterUseRegularExpression,
|
||||
},
|
||||
)
|
||||
if (!useRule) {
|
||||
ret.push({ type: 1, key: `head-${group.name}`, group, headState })
|
||||
}
|
||||
if (occurrences.length === 0) {
|
||||
ret.push({ type: 3, key: `empty-${group.name}`, group, headState })
|
||||
} else if (col > 1) {
|
||||
@@ -343,9 +345,8 @@ export const useRenderList = (
|
||||
return ret
|
||||
})
|
||||
|
||||
const filtered = !useRule
|
||||
? retList.slice(1)
|
||||
: retList.filter((item) => !item.group.hidden)
|
||||
const filtered =
|
||||
mode === 'rule' ? retList.filter((item) => !item.group.hidden) : retList
|
||||
if (
|
||||
prevListRef.current.length === filtered.length &&
|
||||
filtered.every((item, index) => item === prevListRef.current[index])
|
||||
|
||||
@@ -133,7 +133,11 @@ export function AutoBackupSettings() {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem divider disableGutters>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.backup.auto.scheduleLabel"
|
||||
divider
|
||||
disableGutters
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
@@ -152,7 +156,11 @@ export function AutoBackupSettings() {
|
||||
</Stack>
|
||||
</ListItem>
|
||||
|
||||
<ListItem divider disableGutters>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.backup.auto.intervalLabel"
|
||||
divider
|
||||
disableGutters
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
@@ -194,7 +202,11 @@ export function AutoBackupSettings() {
|
||||
</Stack>
|
||||
</ListItem>
|
||||
|
||||
<ListItem divider disableGutters>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.backup.auto.changeLabel"
|
||||
divider
|
||||
disableGutters
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
|
||||
@@ -114,17 +114,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')
|
||||
@@ -175,6 +173,7 @@ export const BackupConfigViewer = memo(
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
data-setting-target="settings.modals.backup.fields.webdavUrl"
|
||||
fullWidth
|
||||
label={t('settings.modals.backup.fields.webdavUrl')}
|
||||
variant="outlined"
|
||||
@@ -189,6 +188,7 @@ export const BackupConfigViewer = memo(
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TextField
|
||||
data-setting-target="settings.modals.backup.fields.username"
|
||||
label={t('settings.modals.backup.fields.username')}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@@ -201,6 +201,7 @@ export const BackupConfigViewer = memo(
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TextField
|
||||
data-setting-target="webdav-password"
|
||||
label={t('shared.labels.password')}
|
||||
type="password"
|
||||
variant="outlined"
|
||||
@@ -232,6 +233,7 @@ export const BackupConfigViewer = memo(
|
||||
color={'primary'}
|
||||
sx={{ height: '100%' }}
|
||||
type="button"
|
||||
data-setting-target="webdav-save"
|
||||
onClick={handleSubmit(save)}
|
||||
>
|
||||
{t('shared.actions.save')}
|
||||
@@ -241,6 +243,7 @@ export const BackupConfigViewer = memo(
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
data-setting-target="webdav-backup"
|
||||
onClick={handleBackup}
|
||||
type="button"
|
||||
size="large"
|
||||
@@ -249,6 +252,7 @@ export const BackupConfigViewer = memo(
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
data-setting-target="webdav-refresh"
|
||||
onClick={onRefresh}
|
||||
type="button"
|
||||
size="large"
|
||||
|
||||
@@ -146,7 +146,12 @@ export const BackupHistoryViewer = ({
|
||||
setRows([])
|
||||
return
|
||||
}
|
||||
if (!isLocal && webdavStatus === 'failed' && !options?.force) {
|
||||
// Read the status here: as a dependency, the write below re-runs the fetch.
|
||||
if (
|
||||
!isLocal &&
|
||||
getWebdavStatus(webdavSignature) === 'failed' &&
|
||||
!options?.force
|
||||
) {
|
||||
setRows([])
|
||||
return
|
||||
}
|
||||
@@ -180,7 +185,7 @@ export const BackupHistoryViewer = ({
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature, webdavStatus],
|
||||
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -322,7 +327,13 @@ export const BackupHistoryViewer = ({
|
||||
onCancel={onClose}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Box sx={{ position: 'relative', minHeight: 320 }}>
|
||||
<Box
|
||||
data-settings-dialog="backup-history"
|
||||
data-search-ready={!loading}
|
||||
data-setting-target="backup-history"
|
||||
tabIndex={-1}
|
||||
sx={{ position: 'relative', minHeight: 320 }}
|
||||
>
|
||||
<BaseLoadingOverlay isLoading={isBusy} />
|
||||
<Stack spacing={2}>
|
||||
<Stack
|
||||
@@ -407,6 +418,7 @@ export const BackupHistoryViewer = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={isBusy}
|
||||
data-setting-target="backup-history-export"
|
||||
onClick={() => handleExport(row.filename)}
|
||||
>
|
||||
<DownloadRounded fontSize="small" />
|
||||
@@ -415,6 +427,7 @@ export const BackupHistoryViewer = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={isBusy}
|
||||
data-setting-target="backup-history-delete"
|
||||
onClick={() => handleDelete(row.filename)}
|
||||
>
|
||||
<DeleteOutlined fontSize="small" />
|
||||
@@ -422,6 +435,7 @@ export const BackupHistoryViewer = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={isBusy}
|
||||
data-setting-target="backup-history-restore"
|
||||
onClick={() => handleRestore(row.filename)}
|
||||
>
|
||||
<RestoreRounded fontSize="small" />
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import type { ReactNode, Ref } from 'react'
|
||||
import { useCallback, useImperativeHandle, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, DialogRef } from '@/components/base'
|
||||
@@ -23,6 +23,8 @@ import { mutate } from '@/services/mutate'
|
||||
import { errorDetail, showNotice } from '@/services/notice-service'
|
||||
import { buildWebdavSignature, setWebdavStatus } from '@/services/webdav-status'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
import { AutoBackupSettings } from './auto-backup-settings'
|
||||
import { BackupHistoryViewer } from './backup-history-viewer'
|
||||
import { BackupWebdavDialog } from './backup-webdav-dialog'
|
||||
@@ -39,9 +41,18 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const [historySource, setHistorySource] = useState<BackupSource>('local')
|
||||
const [historyPage, setHistoryPage] = useState(0)
|
||||
const [webdavDialogOpen, setWebdavDialogOpen] = useState(false)
|
||||
const searchParentRef = useRef<'webdav' | 'history' | null>(null)
|
||||
const closeSearchDetail = (detail: 'webdav' | 'history') => {
|
||||
if (detail === 'webdav') setWebdavDialogOpen(false)
|
||||
else setHistoryOpen(false)
|
||||
if (searchParentRef.current === detail) {
|
||||
searchParentRef.current = null
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
const webdavSignature = buildWebdavSignature(verge)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
useSettingDialog(ref, 'backup-viewer', open, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}))
|
||||
@@ -52,6 +63,23 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
setHistoryOpen(true)
|
||||
}
|
||||
|
||||
useSettingDialog(undefined, 'backup-webdav', webdavDialogOpen, () => ({
|
||||
open: () => {
|
||||
if (!open) searchParentRef.current = 'webdav'
|
||||
setOpen(true)
|
||||
setWebdavDialogOpen(true)
|
||||
},
|
||||
close: () => setWebdavDialogOpen(false),
|
||||
}))
|
||||
useSettingDialog(undefined, 'backup-history', historyOpen, () => ({
|
||||
open: () => {
|
||||
if (!open) searchParentRef.current = 'history'
|
||||
setOpen(true)
|
||||
openHistory('local')
|
||||
},
|
||||
close: () => setHistoryOpen(false),
|
||||
}))
|
||||
|
||||
const handleBackup = useLockFn(async (target: BackupSource) => {
|
||||
try {
|
||||
setBusyAction(target)
|
||||
@@ -129,6 +157,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onCancel={() => setOpen(false)}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<span data-settings-dialog="backup-viewer" />
|
||||
<Stack spacing={2}>
|
||||
<Stack
|
||||
spacing={1}
|
||||
@@ -166,6 +195,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
description: t('settings.modals.backup.manual.local'),
|
||||
actions: [
|
||||
<Button
|
||||
data-setting-target="settings.modals.backup.actions.backup"
|
||||
key="backup"
|
||||
variant="contained"
|
||||
size="small"
|
||||
@@ -176,6 +206,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
{t('settings.modals.backup.actions.backup')}
|
||||
</Button>,
|
||||
<Button
|
||||
data-setting-target="settings.modals.backup.actions.viewHistory"
|
||||
key="history"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@@ -185,6 +216,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
{t('settings.modals.backup.actions.viewHistory')}
|
||||
</Button>,
|
||||
<Button
|
||||
data-setting-target="settings.modals.backup.actions.importBackup"
|
||||
key="import"
|
||||
variant="text"
|
||||
size="small"
|
||||
@@ -263,11 +295,11 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
page={historyPage}
|
||||
onSourceChange={setHistorySource}
|
||||
onPageChange={setHistoryPage}
|
||||
onClose={() => setHistoryOpen(false)}
|
||||
onClose={() => closeSearchDetail('history')}
|
||||
/>
|
||||
<BackupWebdavDialog
|
||||
open={webdavDialogOpen}
|
||||
onClose={() => setWebdavDialogOpen(false)}
|
||||
onClose={() => closeSearchDetail('webdav')}
|
||||
onBackupSuccess={() => openHistory('webdav')}
|
||||
setBusy={setWebdavBusy}
|
||||
/>
|
||||
|
||||
@@ -76,7 +76,13 @@ export const BackupWebdavDialog = ({
|
||||
onCancel={onClose}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Box
|
||||
data-settings-dialog="backup-webdav"
|
||||
data-search-ready={!loading}
|
||||
data-setting-target="webdav-state"
|
||||
tabIndex={-1}
|
||||
sx={{ position: 'relative' }}
|
||||
>
|
||||
<BaseLoadingOverlay isLoading={loading} />
|
||||
<BackupConfigViewer
|
||||
setLoading={handleLoading}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import type { Ref } from 'react'
|
||||
import { useImperativeHandle, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
@@ -24,6 +24,8 @@ import { changeClashCore, restartCore, upgradeClashCore } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
const VALID_CORE = [
|
||||
{
|
||||
name: 'Mihomo',
|
||||
@@ -49,7 +51,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const [restarting, setRestarting] = useState(false)
|
||||
const [changingCore, setChangingCore] = useState<string | null>(null)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
useSettingDialog(ref, 'clash-core-viewer', open, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}))
|
||||
@@ -139,6 +141,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
loading={upgrading}
|
||||
disabled={restarting || changingCore !== null}
|
||||
sx={{ marginRight: '8px' }}
|
||||
data-setting-target="core-upgrade"
|
||||
onClick={onUpgrade}
|
||||
>
|
||||
{t('shared.actions.upgrade')}
|
||||
@@ -150,6 +153,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
loadingPosition="start"
|
||||
loading={restarting}
|
||||
disabled={upgrading}
|
||||
data-setting-target="core-restart"
|
||||
onClick={onRestart}
|
||||
>
|
||||
{t('shared.actions.restart')}
|
||||
@@ -170,7 +174,12 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List component="nav">
|
||||
<List
|
||||
component="nav"
|
||||
data-settings-dialog="clash-core-viewer"
|
||||
data-setting-target="core-variants"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{VALID_CORE.map((each) => (
|
||||
<ListItemButton
|
||||
key={each.core}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TextField,
|
||||
} from '@mui/material'
|
||||
import { useLockFn, useRequest } from 'ahooks'
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react'
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, Switch } from '@/components/base'
|
||||
@@ -22,6 +22,8 @@ import { showNotice } from '@/services/notice-service'
|
||||
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
const OS = getSystem()
|
||||
|
||||
interface ClashPortViewerRef {
|
||||
@@ -87,6 +89,7 @@ const readPortSettings = (
|
||||
})
|
||||
|
||||
interface PortRowProps {
|
||||
searchTarget: string
|
||||
label: string
|
||||
secondary?: string
|
||||
port: number
|
||||
@@ -97,6 +100,7 @@ interface PortRowProps {
|
||||
}
|
||||
|
||||
const PortRow = ({
|
||||
searchTarget,
|
||||
label,
|
||||
secondary,
|
||||
port,
|
||||
@@ -107,7 +111,10 @@ const PortRow = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<ListItem sx={{ padding: '4px 0', minHeight: 36 }}>
|
||||
<ListItem
|
||||
data-setting-target={searchTarget}
|
||||
sx={{ padding: '4px 0', minHeight: 36 }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={label}
|
||||
secondary={secondary}
|
||||
@@ -129,6 +136,7 @@ const PortRow = ({
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
data-setting-target="port-random"
|
||||
onClick={() => onPortChange(generateRandomPort())}
|
||||
title={t('settings.modals.clashPort.actions.random')}
|
||||
disabled={!enabled}
|
||||
@@ -192,7 +200,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
},
|
||||
)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
useSettingDialog(ref, 'clash-port-viewer', open, () => ({
|
||||
open: () => {
|
||||
setPorts(readPortSettings(verge, configuredMixedPort))
|
||||
setOpen(true)
|
||||
@@ -245,9 +253,11 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<span data-settings-dialog="clash-port-viewer" />
|
||||
<List sx={{ width: '100%' }}>
|
||||
<PortRow
|
||||
label={t('settings.modals.clashPort.fields.mixed')}
|
||||
searchTarget="settings.modals.clashPort.fields.mixed"
|
||||
secondary={
|
||||
displayedMixedPort !== configuredMixedPort
|
||||
? t('settings.modals.clashPort.messages.runningPort', {
|
||||
@@ -265,6 +275,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
<PortRow
|
||||
key={key}
|
||||
label={t(label)}
|
||||
searchTarget={label}
|
||||
port={ports[key].port}
|
||||
enabled={ports[key].enabled}
|
||||
onPortChange={(port) => updateListener(key, { port })}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Tooltip,
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useImperativeHandle, useState, type Ref } from 'react'
|
||||
import { useState, type Ref } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, DialogRef, Switch } from '@/components/base'
|
||||
@@ -20,6 +20,8 @@ import { useClashInfo } from '@/hooks/use-clash'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -35,7 +37,7 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
)
|
||||
|
||||
// 对话框打开时初始化配置
|
||||
useImperativeHandle(ref, () => ({
|
||||
useSettingDialog(ref, 'controller-viewer', open, () => ({
|
||||
open: async () => {
|
||||
setOpen(true)
|
||||
setController(clashInfo?.server || '')
|
||||
@@ -124,8 +126,10 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<span data-settings-dialog="controller-viewer" />
|
||||
<List>
|
||||
<ListItem
|
||||
data-setting-target="settings.sections.externalController.fields.enable"
|
||||
sx={{
|
||||
padding: '5px 2px',
|
||||
display: 'flex',
|
||||
@@ -144,6 +148,7 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
data-setting-target="settings.sections.externalController.fields.address"
|
||||
sx={{
|
||||
padding: '5px 2px',
|
||||
display: 'flex',
|
||||
@@ -184,6 +189,7 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
data-setting-target="settings.sections.externalController.fields.secret"
|
||||
sx={{
|
||||
padding: '5px 2px',
|
||||
display: 'flex',
|
||||
|
||||
@@ -16,14 +16,7 @@ import {
|
||||
import { useLockFn } from 'ahooks'
|
||||
import * as yaml from 'js-yaml'
|
||||
import type { Ref } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
@@ -47,6 +40,8 @@ import { useThemeMode } from '@/services/states'
|
||||
import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import { MONACO_FONT_FAMILY } from '@/utils/font-family'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
const Item = styled(ListItem)(() => ({
|
||||
padding: '5px 2px',
|
||||
'& textarea': {
|
||||
@@ -190,6 +185,110 @@ const DEFAULT_DNS_CONFIG = {
|
||||
},
|
||||
}
|
||||
|
||||
type DnsFormValues = {
|
||||
enable: boolean
|
||||
listen: string
|
||||
enhancedMode: 'fake-ip' | 'redir-host'
|
||||
fakeIpRange: string
|
||||
fakeIpRange6: string
|
||||
fakeIpFilterMode: 'blacklist' | 'whitelist'
|
||||
preferH3: boolean
|
||||
respectRules: boolean
|
||||
useHosts: boolean
|
||||
useSystemHosts: boolean
|
||||
ipv6: boolean
|
||||
fakeIpFilter: string
|
||||
nameserver: string
|
||||
fallback: string
|
||||
defaultNameserver: string
|
||||
proxyServerNameserver: string
|
||||
directNameserver: string
|
||||
directNameserverFollowPolicy: boolean
|
||||
fallbackGeoip: boolean
|
||||
fallbackGeoipCode: string
|
||||
fallbackIpcidr: string
|
||||
fallbackDomain: string
|
||||
nameserverPolicy: string
|
||||
hosts: string // hosts设置,独立于dns
|
||||
}
|
||||
|
||||
const DEFAULT_FORM_VALUES: DnsFormValues = {
|
||||
enable: DEFAULT_DNS_CONFIG.enable,
|
||||
listen: DEFAULT_DNS_CONFIG.listen,
|
||||
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
|
||||
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
|
||||
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
|
||||
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
|
||||
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
|
||||
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
|
||||
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
|
||||
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
|
||||
ipv6: DEFAULT_DNS_CONFIG.ipv6,
|
||||
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
|
||||
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
|
||||
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
|
||||
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
|
||||
proxyServerNameserver:
|
||||
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
|
||||
directNameserver: DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
|
||||
directNameserverFollowPolicy:
|
||||
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
|
||||
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
|
||||
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
|
||||
fallbackIpcidr:
|
||||
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
|
||||
fallbackDomain:
|
||||
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
|
||||
nameserverPolicy: '',
|
||||
hosts: '',
|
||||
}
|
||||
|
||||
function buildDnsConfig(values: DnsFormValues) {
|
||||
const dnsConfig: any = {
|
||||
enable: values.enable,
|
||||
listen: values.listen,
|
||||
'enhanced-mode': values.enhancedMode,
|
||||
'fake-ip-range': values.fakeIpRange,
|
||||
'fake-ip-range6': values.fakeIpRange6,
|
||||
'fake-ip-filter-mode': values.fakeIpFilterMode,
|
||||
'prefer-h3': values.preferH3,
|
||||
'respect-rules': values.respectRules,
|
||||
'use-hosts': values.useHosts,
|
||||
'use-system-hosts': values.useSystemHosts,
|
||||
ipv6: values.ipv6,
|
||||
'fake-ip-filter': parseList(values.fakeIpFilter),
|
||||
'default-nameserver': parseList(values.defaultNameserver),
|
||||
nameserver: parseList(values.nameserver),
|
||||
'nameserver-policy': parseNameserverPolicy(values.nameserverPolicy),
|
||||
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
|
||||
'fallback-filter': {
|
||||
geoip: values.fallbackGeoip,
|
||||
'geoip-code': values.fallbackGeoipCode,
|
||||
ipcidr: parseList(values.fallbackIpcidr),
|
||||
domain: parseList(values.fallbackDomain),
|
||||
},
|
||||
|
||||
fallback: parseList(values.fallback),
|
||||
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
|
||||
'direct-nameserver': parseList(values.directNameserver),
|
||||
}
|
||||
|
||||
return dnsConfig
|
||||
}
|
||||
|
||||
function buildDnsYaml(values: DnsFormValues) {
|
||||
const config: Record<string, any> = {}
|
||||
|
||||
const dnsConfig = buildDnsConfig(values)
|
||||
if (Object.keys(dnsConfig).length > 0) {
|
||||
config.dns = dnsConfig
|
||||
}
|
||||
|
||||
config.hosts = parseHosts(values.hosts)
|
||||
|
||||
return yaml.dump(config, { forceQuotes: true })
|
||||
}
|
||||
|
||||
export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { t } = useTranslation()
|
||||
const { mutateClash } = useClash()
|
||||
@@ -203,64 +302,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const themeMode = useThemeMode()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [initializing, setInitializing] = useState(false)
|
||||
const [visualization, setVisualization] = useState(true)
|
||||
const skipYamlSyncRef = useRef(false)
|
||||
const editorRef = useRef<MonacoEditorInstance | null>(null)
|
||||
const [values, setValues] = useState<{
|
||||
enable: boolean
|
||||
listen: string
|
||||
enhancedMode: 'fake-ip' | 'redir-host'
|
||||
fakeIpRange: string
|
||||
fakeIpRange6: string
|
||||
fakeIpFilterMode: 'blacklist' | 'whitelist'
|
||||
preferH3: boolean
|
||||
respectRules: boolean
|
||||
useHosts: boolean
|
||||
useSystemHosts: boolean
|
||||
ipv6: boolean
|
||||
fakeIpFilter: string
|
||||
nameserver: string
|
||||
fallback: string
|
||||
defaultNameserver: string
|
||||
proxyServerNameserver: string
|
||||
directNameserver: string
|
||||
directNameserverFollowPolicy: boolean
|
||||
fallbackGeoip: boolean
|
||||
fallbackGeoipCode: string
|
||||
fallbackIpcidr: string
|
||||
fallbackDomain: string
|
||||
nameserverPolicy: string
|
||||
hosts: string // hosts设置,独立于dns
|
||||
}>({
|
||||
enable: DEFAULT_DNS_CONFIG.enable,
|
||||
listen: DEFAULT_DNS_CONFIG.listen,
|
||||
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
|
||||
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
|
||||
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
|
||||
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
|
||||
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
|
||||
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
|
||||
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
|
||||
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
|
||||
ipv6: DEFAULT_DNS_CONFIG.ipv6,
|
||||
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
|
||||
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
|
||||
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
|
||||
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
|
||||
proxyServerNameserver:
|
||||
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
|
||||
directNameserver: DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
|
||||
directNameserverFollowPolicy:
|
||||
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
|
||||
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
|
||||
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
|
||||
fallbackIpcidr:
|
||||
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
|
||||
fallbackDomain:
|
||||
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
|
||||
nameserverPolicy: '',
|
||||
hosts: '',
|
||||
})
|
||||
const [values, setValues] = useState<DnsFormValues>(DEFAULT_FORM_VALUES)
|
||||
|
||||
// Prevent YAML-to-form sync before configuration loads.
|
||||
const [yamlContent, setYamlContent] = useReducer(
|
||||
@@ -269,7 +314,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
)
|
||||
|
||||
const updateValuesFromConfig = useCallback((config: any) => {
|
||||
if (!config) return
|
||||
if (!config) return undefined
|
||||
|
||||
const dnsConfig = config.dns || {}
|
||||
const hostsConfig = config.hosts || {}
|
||||
@@ -289,7 +334,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
? fakeIpFilterMode
|
||||
: DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
|
||||
|
||||
setValues({
|
||||
const next: DnsFormValues = {
|
||||
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
|
||||
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
|
||||
enhancedMode: validEnhancedMode,
|
||||
@@ -341,127 +386,27 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
nameserverPolicy:
|
||||
formatNameserverPolicy(dnsConfig['nameserver-policy']) || '',
|
||||
hosts: formatHosts(hostsConfig) || '',
|
||||
})
|
||||
}
|
||||
setValues(next)
|
||||
return next
|
||||
}, [])
|
||||
|
||||
const generateDnsConfig = useCallback(() => {
|
||||
const dnsConfig: any = {
|
||||
enable: values.enable,
|
||||
listen: values.listen,
|
||||
'enhanced-mode': values.enhancedMode,
|
||||
'fake-ip-range': values.fakeIpRange,
|
||||
'fake-ip-range6': values.fakeIpRange6,
|
||||
'fake-ip-filter-mode': values.fakeIpFilterMode,
|
||||
'prefer-h3': values.preferH3,
|
||||
'respect-rules': values.respectRules,
|
||||
'use-hosts': values.useHosts,
|
||||
'use-system-hosts': values.useSystemHosts,
|
||||
ipv6: values.ipv6,
|
||||
'fake-ip-filter': parseList(values.fakeIpFilter),
|
||||
'default-nameserver': parseList(values.defaultNameserver),
|
||||
nameserver: parseList(values.nameserver),
|
||||
'nameserver-policy': parseNameserverPolicy(values.nameserverPolicy),
|
||||
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
|
||||
'fallback-filter': {
|
||||
geoip: values.fallbackGeoip,
|
||||
'geoip-code': values.fallbackGeoipCode,
|
||||
ipcidr: parseList(values.fallbackIpcidr),
|
||||
domain: parseList(values.fallbackDomain),
|
||||
},
|
||||
|
||||
fallback: parseList(values.fallback),
|
||||
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
|
||||
'direct-nameserver': parseList(values.directNameserver),
|
||||
}
|
||||
|
||||
return dnsConfig
|
||||
}, [values])
|
||||
|
||||
const updateYamlFromValues = useCallback(() => {
|
||||
const config: Record<string, any> = {}
|
||||
|
||||
const dnsConfig = generateDnsConfig()
|
||||
if (Object.keys(dnsConfig).length > 0) {
|
||||
config.dns = dnsConfig
|
||||
}
|
||||
|
||||
config.hosts = parseHosts(values.hosts)
|
||||
|
||||
setYamlContent(yaml.dump(config, { forceQuotes: true }))
|
||||
}, [generateDnsConfig, values.hosts])
|
||||
|
||||
const resetToDefaults = useCallback(() => {
|
||||
setValues({
|
||||
enable: DEFAULT_DNS_CONFIG.enable,
|
||||
listen: DEFAULT_DNS_CONFIG.listen,
|
||||
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
|
||||
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
|
||||
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
|
||||
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
|
||||
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
|
||||
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
|
||||
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
|
||||
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
|
||||
ipv6: DEFAULT_DNS_CONFIG.ipv6,
|
||||
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
|
||||
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
|
||||
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
|
||||
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
|
||||
proxyServerNameserver:
|
||||
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
|
||||
directNameserver:
|
||||
DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
|
||||
directNameserverFollowPolicy:
|
||||
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
|
||||
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
|
||||
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
|
||||
fallbackIpcidr:
|
||||
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
|
||||
fallbackDomain:
|
||||
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
|
||||
nameserverPolicy: '',
|
||||
hosts: '',
|
||||
})
|
||||
|
||||
updateYamlFromValues()
|
||||
}, [updateYamlFromValues])
|
||||
setValues(DEFAULT_FORM_VALUES)
|
||||
setYamlContent(buildDnsYaml(DEFAULT_FORM_VALUES))
|
||||
}, [])
|
||||
|
||||
const updateValuesFromYaml = useCallback(() => {
|
||||
try {
|
||||
const parsedYaml = yaml.load(yamlContent) as any
|
||||
if (!parsedYaml || parsedYaml.isInitValue) return
|
||||
|
||||
skipYamlSyncRef.current = true
|
||||
updateValuesFromConfig(parsedYaml)
|
||||
} catch {
|
||||
showNotice.error('settings.modals.dns.errors.invalidYaml')
|
||||
}
|
||||
}, [yamlContent, updateValuesFromConfig])
|
||||
|
||||
useEffect(() => {
|
||||
if (skipYamlSyncRef.current) {
|
||||
skipYamlSyncRef.current = false
|
||||
return
|
||||
}
|
||||
updateYamlFromValues()
|
||||
}, [updateYamlFromValues])
|
||||
|
||||
const latestUpdateValuesFromYamlRef = useRef(updateValuesFromYaml)
|
||||
const latestUpdateYamlFromValuesRef = useRef(updateYamlFromValues)
|
||||
|
||||
useEffect(() => {
|
||||
latestUpdateValuesFromYamlRef.current = updateValuesFromYaml
|
||||
latestUpdateYamlFromValuesRef.current = updateYamlFromValues
|
||||
}, [updateValuesFromYaml, updateYamlFromValues])
|
||||
|
||||
useEffect(() => {
|
||||
if (visualization) {
|
||||
latestUpdateValuesFromYamlRef.current()
|
||||
} else {
|
||||
latestUpdateYamlFromValuesRef.current()
|
||||
}
|
||||
}, [visualization])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
editorRef.current?.dispose()
|
||||
@@ -470,34 +415,33 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}, [])
|
||||
|
||||
const initDnsConfig = useCallback(async () => {
|
||||
setInitializing(true)
|
||||
try {
|
||||
const dnsConfig = await getDnsConfigContent()
|
||||
|
||||
if (dnsConfig !== null) {
|
||||
const config = yaml.load(dnsConfig) as any
|
||||
|
||||
updateValuesFromConfig(config)
|
||||
setYamlContent(dnsConfig)
|
||||
const loaded = updateValuesFromConfig(config)
|
||||
setYamlContent(loaded ? buildDnsYaml(loaded) : dnsConfig)
|
||||
} else {
|
||||
resetToDefaults()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize DNS config', err)
|
||||
resetToDefaults()
|
||||
} finally {
|
||||
setInitializing(false)
|
||||
}
|
||||
}, [resetToDefaults, updateValuesFromConfig])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
open: () => {
|
||||
setOpen(true)
|
||||
void initDnsConfig()
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}),
|
||||
[initDnsConfig],
|
||||
)
|
||||
useSettingDialog(ref, 'dns-viewer', open, () => ({
|
||||
open: () => {
|
||||
setOpen(true)
|
||||
void initDnsConfig()
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}))
|
||||
|
||||
const onSave = useLockFn(async () => {
|
||||
try {
|
||||
@@ -506,7 +450,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
if (visualization) {
|
||||
config = {}
|
||||
|
||||
const dnsConfig = generateDnsConfig()
|
||||
const dnsConfig = buildDnsConfig(values)
|
||||
if (Object.keys(dnsConfig).length > 0) {
|
||||
config.dns = dnsConfig
|
||||
}
|
||||
@@ -586,20 +530,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
? event.target.checked
|
||||
: event.target.value
|
||||
|
||||
setValues((prev) => {
|
||||
const newValues = {
|
||||
...prev,
|
||||
[field]: value,
|
||||
}
|
||||
|
||||
if (visualization) {
|
||||
setTimeout(() => {
|
||||
updateYamlFromValues()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return newValues
|
||||
})
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -617,6 +551,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
{t('settings.modals.dns.dialog.title')}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Button
|
||||
data-setting-target="dns-viewer-reset"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
color="warning"
|
||||
@@ -628,8 +563,14 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
data-setting-target="dns-mode"
|
||||
onClick={() => {
|
||||
setVisualization((prev) => !prev)
|
||||
if (visualization) {
|
||||
setYamlContent(buildDnsYaml(values))
|
||||
} else {
|
||||
updateValuesFromYaml()
|
||||
}
|
||||
setVisualization(!visualization)
|
||||
}}
|
||||
>
|
||||
{visualization
|
||||
@@ -652,6 +593,15 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<span
|
||||
data-settings-dialog="dns-viewer"
|
||||
data-search-ready={!initializing}
|
||||
/>
|
||||
{initializing && (
|
||||
<Typography role="status" data-settings-loading tabIndex={-1}>
|
||||
{t('shared.statuses.loading')}
|
||||
</Typography>
|
||||
)}
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
{t('settings.modals.dns.dialog.profileScope')}
|
||||
</Alert>
|
||||
@@ -672,7 +622,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
{t('settings.modals.dns.sections.general')}
|
||||
</Typography>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.enable">
|
||||
<ListItemText primary={t('settings.modals.dns.fields.enable')} />
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -681,7 +631,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.listen">
|
||||
<ListItemText primary={t('settings.modals.dns.fields.listen')} />
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -694,7 +644,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.enhancedMode">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.enhancedMode')}
|
||||
/>
|
||||
@@ -709,7 +659,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</FormControl>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.fakeIpRange">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.fakeIpRange')}
|
||||
/>
|
||||
@@ -724,7 +674,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.fakeIpRange6">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.fakeIpRange6')}
|
||||
/>
|
||||
@@ -739,7 +689,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.fakeIpFilterMode">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.fakeIpFilterMode')}
|
||||
/>
|
||||
@@ -754,7 +704,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</FormControl>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.ipv6.label">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.ipv6.label')}
|
||||
secondary={t('settings.modals.dns.fields.ipv6.description')}
|
||||
@@ -766,7 +716,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.preferH3.label">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.preferH3.label')}
|
||||
secondary={t('settings.modals.dns.fields.preferH3.description')}
|
||||
@@ -778,7 +728,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.respectRules.label">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.respectRules.label')}
|
||||
secondary={t(
|
||||
@@ -792,7 +742,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.useHosts.label">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.useHosts.label')}
|
||||
secondary={t('settings.modals.dns.fields.useHosts.description')}
|
||||
@@ -804,7 +754,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.useSystemHosts.label">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.useSystemHosts.label')}
|
||||
secondary={t(
|
||||
@@ -818,7 +768,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.directPolicy.label">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.directPolicy.label')}
|
||||
secondary={t(
|
||||
@@ -832,7 +782,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.defaultNameserver.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.defaultNameserver.label')}
|
||||
secondary={t(
|
||||
@@ -852,7 +805,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.nameserver.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.nameserver.label')}
|
||||
secondary={t('settings.modals.dns.fields.nameserver.description')}
|
||||
@@ -870,7 +826,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.fallback.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.fallback.label')}
|
||||
secondary={t('settings.modals.dns.fields.fallback.description')}
|
||||
@@ -888,7 +847,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.proxy.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.proxy.label')}
|
||||
secondary={t('settings.modals.dns.fields.proxy.description')}
|
||||
@@ -906,7 +868,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.directNameserver.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.directNameserver.label')}
|
||||
secondary={t(
|
||||
@@ -926,7 +891,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.fakeIpFilter.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.fakeIpFilter.label')}
|
||||
secondary={t(
|
||||
@@ -946,7 +914,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.nameserverPolicy.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.nameserverPolicy.label')}
|
||||
secondary={t(
|
||||
@@ -973,7 +944,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
{t('settings.modals.dns.sections.fallbackFilter')}
|
||||
</Typography>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.geoipFiltering.label">
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.geoipFiltering.label')}
|
||||
secondary={t(
|
||||
@@ -987,7 +958,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.modals.dns.fields.geoipCode">
|
||||
<ListItemText primary={t('settings.modals.dns.fields.geoipCode')} />
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -1000,7 +971,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.fallbackIpCidr.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.fallbackIpCidr.label')}
|
||||
secondary={t(
|
||||
@@ -1020,7 +994,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.fallbackDomain.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.fallbackDomain.label')}
|
||||
secondary={t(
|
||||
@@ -1047,7 +1024,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
{t('settings.modals.dns.sections.hosts')}
|
||||
</Typography>
|
||||
|
||||
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Item
|
||||
data-setting-target="settings.modals.dns.fields.hosts.label"
|
||||
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.dns.fields.hosts.label')}
|
||||
secondary={t('settings.modals.dns.fields.hosts.description')}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Delete as DeleteIcon } from '@mui/icons-material'
|
||||
import { Box, Button, Divider, List, ListItem, TextField } from '@mui/material'
|
||||
import { useLockFn, useRequest } from 'ahooks'
|
||||
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { forwardRef, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, Switch } from '@/components/base'
|
||||
@@ -10,6 +10,8 @@ import { restartCore } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
// Development origins must never be persisted into production configuration.
|
||||
const DEV_URLS = [
|
||||
'tauri://localhost',
|
||||
@@ -154,7 +156,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
},
|
||||
)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
useSettingDialog(ref, 'external-controller-cors', open, () => ({
|
||||
open: () => {
|
||||
const cors = clash?.['external-controller-cors']
|
||||
const origins = cors?.['allow-origins'] ?? []
|
||||
@@ -186,8 +188,12 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<span data-settings-dialog="external-controller-cors" />
|
||||
<List sx={{ width: '90%', padding: 2 }}>
|
||||
<ListItem sx={{ padding: '8px 0' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.sections.externalCors.fields.allowPrivateNetwork"
|
||||
sx={{ padding: '8px 0' }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -214,7 +220,10 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<ListItem sx={{ padding: '8px 0' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.sections.externalCors.fields.allowedOrigins"
|
||||
sx={{ padding: '8px 0' }}
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 'bold' }}>
|
||||
{t('settings.sections.externalCors.fields.allowedOrigins')}
|
||||
@@ -252,6 +261,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
data-setting-target="settings.sections.externalCors.actions.add"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleAddOrigin}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { styled, Typography } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react'
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, DialogRef, Switch } from '@/components/base'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
import { HotkeyInput } from './hotkey-input'
|
||||
|
||||
const ItemWrapper = styled('div')`
|
||||
@@ -51,7 +53,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
verge?.enable_global_hotkey ?? true,
|
||||
)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
useSettingDialog(ref, 'hotkey-viewer', open, () => ({
|
||||
open: () => {
|
||||
setOpen(true)
|
||||
|
||||
@@ -111,7 +113,11 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<ItemWrapper style={{ marginBottom: 16 }}>
|
||||
<span data-settings-dialog="hotkey-viewer" />
|
||||
<ItemWrapper
|
||||
data-setting-target="settings.modals.hotkey.toggles.enableGlobal"
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Typography>
|
||||
{t('settings.modals.hotkey.toggles.enableGlobal')}
|
||||
</Typography>
|
||||
@@ -123,7 +129,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</ItemWrapper>
|
||||
|
||||
{HOTKEY_FUNC.map((func) => (
|
||||
<ItemWrapper key={func}>
|
||||
<ItemWrapper key={func} data-setting-target={HOTKEY_FUNC_LABELS[func]}>
|
||||
<Typography>{t(HOTKEY_FUNC_LABELS[func])}</Typography>
|
||||
<HotkeyInput
|
||||
value={hotkeyMap[func] ?? []}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { join } from '@tauri-apps/api/path'
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
||||
import { exists } from '@tauri-apps/plugin-fs'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
|
||||
@@ -26,6 +26,8 @@ import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
import { GuardState } from './guard-state'
|
||||
|
||||
const OS = getSystem()
|
||||
@@ -97,7 +99,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
}
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
useSettingDialog(ref, 'layout-viewer', open, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}))
|
||||
@@ -107,7 +109,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
showNotice.error(err)
|
||||
}
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false)
|
||||
mutateVerge({ ...verge, ...patch })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -120,8 +122,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<span data-settings-dialog="layout-viewer" />
|
||||
<List>
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.preferSystemTitlebar">
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.components.verge.layout.fields.preferSystemTitlebar',
|
||||
@@ -140,7 +143,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.trafficGraph">
|
||||
<ListItemText
|
||||
primary={t('settings.components.verge.layout.fields.trafficGraph')}
|
||||
/>
|
||||
@@ -156,7 +159,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.memoryUsage">
|
||||
<ListItemText
|
||||
primary={t('settings.components.verge.layout.fields.memoryUsage')}
|
||||
/>
|
||||
@@ -172,7 +175,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.proxyGroupIcon">
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.components.verge.layout.fields.proxyGroupIcon',
|
||||
@@ -190,7 +193,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur">
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur',
|
||||
@@ -212,7 +215,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.toastPosition">
|
||||
<ListItemText
|
||||
primary={t('settings.components.verge.layout.fields.toastPosition')}
|
||||
/>
|
||||
@@ -248,7 +251,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.hoverNavigator">
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
@@ -276,7 +279,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.hoverNavigatorDelay">
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
@@ -335,7 +338,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.navIcon">
|
||||
<ListItemText
|
||||
primary={t('settings.components.verge.layout.fields.navIcon')}
|
||||
/>
|
||||
@@ -360,7 +363,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.collapseNavBar">
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.components.verge.layout.fields.collapseNavBar',
|
||||
@@ -379,7 +382,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</Item>
|
||||
|
||||
{OS === 'macos' && (
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.trayIcon">
|
||||
<ListItemText
|
||||
primary={t('settings.components.verge.layout.fields.trayIcon')}
|
||||
/>
|
||||
@@ -407,7 +410,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</Item>
|
||||
)}
|
||||
{OS === 'macos' && (
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.enableTraySpeed">
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.components.verge.layout.fields.enableTraySpeed',
|
||||
@@ -445,7 +448,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
)} */}
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.proxyGroupsDisplayMode">
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.components.verge.layout.fields.proxyGroupsDisplayMode',
|
||||
@@ -481,7 +484,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</Select>
|
||||
</GuardState>
|
||||
</Item>
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.showOutboundModesInline">
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.components.verge.layout.fields.showOutboundModesInline',
|
||||
@@ -499,7 +502,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.commonTrayIcon">
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.components.verge.layout.fields.commonTrayIcon',
|
||||
@@ -512,6 +515,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
onGuard={(e) => patchVerge({ common_tray_icon: e })}
|
||||
>
|
||||
<Button
|
||||
data-setting-target="settings.components.verge.basic.actions.browse"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={
|
||||
@@ -554,7 +558,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.systemProxyTrayIcon">
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.components.verge.layout.fields.systemProxyTrayIcon',
|
||||
@@ -609,7 +613,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Item data-setting-target="settings.components.verge.layout.fields.tunTrayIcon">
|
||||
<ListItemText
|
||||
primary={t('settings.components.verge.layout.fields.tunTrayIcon')}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import type { Ref } from 'react'
|
||||
import { useImperativeHandle, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
|
||||
@@ -17,6 +17,8 @@ import { entry_lightweight_mode } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { t } = useTranslation()
|
||||
const { verge, patchVerge } = useVerge()
|
||||
@@ -27,7 +29,7 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
autoEnterLiteModeDelay: 10, // 默认10分钟
|
||||
})
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
useSettingDialog(ref, 'lite-mode-viewer', open, () => ({
|
||||
open: () => {
|
||||
setOpen(true)
|
||||
setValues({
|
||||
@@ -61,8 +63,12 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<span data-settings-dialog="lite-mode-viewer" />
|
||||
<List>
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.liteMode.actions.enterNow"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.liteMode.actions.enterNow')}
|
||||
/>
|
||||
@@ -84,7 +90,10 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.liteMode.toggles.autoEnter"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.liteMode.toggles.autoEnter')}
|
||||
sx={{ maxWidth: 'fit-content' }}
|
||||
@@ -105,7 +114,10 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
{values.autoEnterLiteMode && (
|
||||
<>
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.liteMode.fields.delay"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.liteMode.fields.delay')}
|
||||
/>
|
||||
|
||||
@@ -8,51 +8,42 @@ import {
|
||||
TextField,
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react'
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
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, () => ({
|
||||
useSettingDialog(ref, 'misc-viewer', open, () => ({
|
||||
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),
|
||||
}))
|
||||
@@ -91,8 +82,12 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<span data-settings-dialog="misc-viewer" />
|
||||
<List>
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.appLogLevel"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.appLogLevel')}
|
||||
/>
|
||||
@@ -115,7 +110,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.appLogMaxSize"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.appLogMaxSize')}
|
||||
sx={{ maxWidth: 'fit-content' }}
|
||||
@@ -147,7 +145,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.appLogMaxCount"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.appLogMaxCount')}
|
||||
sx={{ maxWidth: 'fit-content' }}
|
||||
@@ -179,7 +180,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.autoCloseConnections"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.autoCloseConnections')}
|
||||
sx={{ maxWidth: 'fit-content' }}
|
||||
@@ -198,7 +202,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.autoCheckUpdate"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.autoCheckUpdate')}
|
||||
/>
|
||||
@@ -211,7 +218,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.enableBuiltinEnhanced"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.enableBuiltinEnhanced')}
|
||||
sx={{ maxWidth: 'fit-content' }}
|
||||
@@ -230,7 +240,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.proxyLayoutColumns"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.proxyLayoutColumns')}
|
||||
/>
|
||||
@@ -256,7 +269,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.autoLogClean"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.autoLogClean')}
|
||||
/>
|
||||
@@ -309,7 +325,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.autoDelayDetection"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.autoDelayDetection')}
|
||||
sx={{ maxWidth: 'fit-content' }}
|
||||
@@ -328,7 +347,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.autoDelayDetectionInterval"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t(
|
||||
'settings.modals.misc.fields.autoDelayDetectionInterval',
|
||||
@@ -366,7 +388,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.defaultLatencyTest"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.defaultLatencyTest')}
|
||||
sx={{ maxWidth: 'fit-content' }}
|
||||
@@ -390,7 +415,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItem
|
||||
data-setting-target="settings.modals.misc.fields.defaultLatencyTimeout"
|
||||
sx={{ padding: '5px 2px' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.misc.fields.defaultLatencyTimeout')}
|
||||
/>
|
||||
|
||||
@@ -2,19 +2,21 @@ import { ContentCopyRounded } from '@mui/icons-material'
|
||||
import { alpha, Box, Button, CircularProgress, IconButton } from '@mui/material'
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||
import type { Ref } from 'react'
|
||||
import { useImperativeHandle, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, BaseEmpty, DialogRef } from '@/components/base'
|
||||
import { useNetworkInterfaces } from '@/hooks/use-network'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import { useSettingDialog } from '../settings-search-context'
|
||||
|
||||
export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isV4, setIsV4] = useState(true)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
useSettingDialog(ref, 'network-interface-viewer', open, () => ({
|
||||
open: () => {
|
||||
setOpen(true)
|
||||
},
|
||||
@@ -51,41 +53,49 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
{loading && isEmpty ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress size={24} />
|
||||
</Box>
|
||||
) : isEmpty ? (
|
||||
<Box sx={{ minHeight: 160 }}>
|
||||
<BaseEmpty />
|
||||
</Box>
|
||||
) : (
|
||||
networkInterfaces.map((item) => (
|
||||
<Box key={item.name}>
|
||||
<h4>{item.name}</h4>
|
||||
<Box>
|
||||
{item.addr.map((address) => {
|
||||
const ip = getAddressIp(address)
|
||||
return (
|
||||
ip && (
|
||||
<AddressDisplay
|
||||
key={ip}
|
||||
label={t(
|
||||
'settings.modals.networkInterface.fields.ipAddress',
|
||||
)}
|
||||
content={ip}
|
||||
/>
|
||||
)
|
||||
)
|
||||
})}
|
||||
<AddressDisplay
|
||||
label={t('settings.modals.networkInterface.fields.macAddress')}
|
||||
content={item.mac_addr ?? ''}
|
||||
/>
|
||||
</Box>
|
||||
<div
|
||||
data-settings-dialog="network-interface-viewer"
|
||||
data-setting-target="network-interface-viewer"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{loading && isEmpty ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress size={24} />
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
) : isEmpty ? (
|
||||
<Box sx={{ minHeight: 160 }}>
|
||||
<BaseEmpty />
|
||||
</Box>
|
||||
) : (
|
||||
networkInterfaces.map((item) => (
|
||||
<Box key={item.name}>
|
||||
<h4>{item.name}</h4>
|
||||
<Box>
|
||||
{item.addr.map((address) => {
|
||||
const ip = getAddressIp(address)
|
||||
return (
|
||||
ip && (
|
||||
<AddressDisplay
|
||||
key={ip}
|
||||
label={t(
|
||||
'settings.modals.networkInterface.fields.ipAddress',
|
||||
)}
|
||||
content={ip}
|
||||
/>
|
||||
)
|
||||
)
|
||||
})}
|
||||
<AddressDisplay
|
||||
label={t(
|
||||
'settings.modals.networkInterface.fields.macAddress',
|
||||
)}
|
||||
content={item.mac_addr ?? ''}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import React, { ReactNode, useState } from 'react'
|
||||
import isAsyncFunction from '@/utils/is-async-function'
|
||||
|
||||
interface ItemProps {
|
||||
'data-setting-target'?: string
|
||||
label: ReactNode
|
||||
extra?: ReactNode
|
||||
children?: ReactNode
|
||||
@@ -21,6 +22,7 @@ interface ItemProps {
|
||||
}
|
||||
|
||||
export const SettingItem: React.FC<ItemProps> = ({
|
||||
'data-setting-target': searchTarget,
|
||||
label,
|
||||
extra,
|
||||
children,
|
||||
@@ -49,7 +51,7 @@ export const SettingItem: React.FC<ItemProps> = ({
|
||||
}
|
||||
|
||||
return clickable ? (
|
||||
<ListItem disablePadding>
|
||||
<ListItem data-setting-target={searchTarget} disablePadding>
|
||||
<ListItemButton onClick={handleClick} disabled={isLoading}>
|
||||
<ListItemText primary={primary} secondary={secondary} />
|
||||
{isLoading ? (
|
||||
@@ -60,7 +62,7 @@ export const SettingItem: React.FC<ItemProps> = ({
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
) : (
|
||||
<ListItem sx={{ pt: '5px', pb: '5px' }}>
|
||||
<ListItem data-setting-target={searchTarget} sx={{ pt: '5px', pb: '5px' }}>
|
||||
<ListItemText primary={primary} secondary={secondary} />
|
||||
{children}
|
||||
</ListItem>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user