mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 07:14:40 +08:00
Compare commits
151 Commits
v2.4.4
...
c57a962109
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c57a962109 | ||
|
|
36926df26c | ||
|
|
9d81a13c58 | ||
|
|
511fab9a9d | ||
|
|
88529af8c8 | ||
|
|
425096e8af | ||
|
|
8a4e2327c1 | ||
|
|
74b1687be9 | ||
|
|
6477dd61c3 | ||
|
|
6ded9bdcde | ||
|
|
13dc3feb9f | ||
|
|
c7462716e5 | ||
|
|
bf189bb144 | ||
|
|
0c6631ebb0 | ||
|
|
93e7ac1bce | ||
|
|
b921098182 | ||
|
|
440f95f617 | ||
|
|
b9667ad349 | ||
|
|
4e7cdbfcc0 | ||
|
|
966fd68087 | ||
|
|
334cec3bde | ||
|
|
6e16133393 | ||
|
|
5e976c2fe1 | ||
|
|
d81aa5f233 | ||
|
|
e5fc0de39a | ||
|
|
6c62350cc3 | ||
|
|
d1649e3017 | ||
|
|
2869a35f1e | ||
|
|
98f12a9c72 | ||
|
|
6dc8a2f232 | ||
|
|
6511f3868e | ||
|
|
7da5a804f9 | ||
|
|
20ed7a3abe | ||
|
|
fd98caccd2 | ||
|
|
a5f494bda2 | ||
|
|
d4d8ef3849 | ||
|
|
b16cbd5379 | ||
|
|
9e6689ef08 | ||
|
|
e0c35c5ee3 | ||
|
|
670055aba1 | ||
|
|
a780e44e69 | ||
|
|
5c9b46f031 | ||
|
|
f5e75d5287 | ||
|
|
c2d8277a1a | ||
|
|
66e98518a7 | ||
|
|
089b73bbfd | ||
|
|
d2c52d09e1 | ||
|
|
84143ec761 | ||
|
|
f451a26f8c | ||
|
|
e1220a189b | ||
|
|
57d4149807 | ||
|
|
86c3b241b1 | ||
|
|
a49000712d | ||
|
|
35b2066d4c | ||
|
|
92e0762fc4 | ||
|
|
6b8630d357 | ||
|
|
a1e77070f0 | ||
|
|
6926744ca2 | ||
|
|
13855b9bc2 | ||
|
|
1889f18183 | ||
|
|
a981be80ef | ||
|
|
60d3a1927b | ||
|
|
620841592f | ||
|
|
2128e1f788 | ||
|
|
256a3f697b | ||
|
|
a701450362 | ||
|
|
9e4e0c81a4 | ||
|
|
421bbd090e | ||
|
|
4adf678480 | ||
|
|
a9a782d5c9 | ||
|
|
ee5e5ee8a6 | ||
|
|
a940445081 | ||
|
|
65653594c7 | ||
|
|
ac8f62bea2 | ||
|
|
eb8ba8b369 | ||
|
|
c18821288e | ||
|
|
7d40410dea | ||
|
|
349be20a6c | ||
|
|
1901a6c97c | ||
|
|
bb72b92ae9 | ||
|
|
8a1740d38b | ||
|
|
d75d3bd86e | ||
|
|
b277a1e760 | ||
|
|
522eccdd0e | ||
|
|
f3b9eedcf7 | ||
|
|
3bbcdbe5ca | ||
|
|
cceb0a6eb4 | ||
|
|
cb5a2e7ce3 | ||
|
|
609008f087 | ||
|
|
bae3576e93 | ||
|
|
9ce343fb45 | ||
|
|
cf08628200 | ||
|
|
c06c15450f | ||
|
|
772b87e733 | ||
|
|
c80c659180 | ||
|
|
0cde6cfce9 | ||
|
|
e6a0369036 | ||
|
|
ca50e35435 | ||
|
|
0193ba7bf9 | ||
|
|
c40cdf6b55 | ||
|
|
a82bcbe86e | ||
|
|
895e54f7ec | ||
|
|
c41db51f81 | ||
|
|
2c1303c2bd | ||
|
|
c8aeae3f83 | ||
|
|
1b477ed0b2 | ||
|
|
5aba848741 | ||
|
|
593751eda2 | ||
|
|
b53f54f3f4 | ||
|
|
bfb18cf003 | ||
|
|
9c6f5bc991 | ||
|
|
63cd4905f9 | ||
|
|
2417d064e1 | ||
|
|
d91e19e166 | ||
|
|
65b4d8713d | ||
|
|
a67abda72d | ||
|
|
8e27834e35 | ||
|
|
ee3f7df417 | ||
|
|
f9b8a658a1 | ||
|
|
1c044f053f | ||
|
|
712b8ff19b | ||
|
|
4ab2720ac4 | ||
|
|
af0e72d119 | ||
|
|
bd62a4ecc0 | ||
|
|
4ffb8b415f | ||
|
|
0992556b4a | ||
|
|
be6b53c760 | ||
|
|
797c0f90aa | ||
|
|
d52f00c1b1 | ||
|
|
f26abcd2a9 | ||
|
|
863a80df43 | ||
|
|
19accbd538 | ||
|
|
8e48e4ed10 | ||
|
|
eafa08066d | ||
|
|
231517b5db | ||
|
|
45193e017f | ||
|
|
2515deefed | ||
|
|
c84bb91f4a | ||
|
|
af094bfcd7 | ||
|
|
a5752f7b00 | ||
|
|
23e551e384 | ||
|
|
a0b12b8797 | ||
|
|
16c3dcc616 | ||
|
|
5afe11e55b | ||
|
|
6f61759a39 | ||
|
|
57b17ab8d3 | ||
|
|
fc84dc561c | ||
|
|
bd8eccdcea | ||
|
|
b4e25951b4 | ||
|
|
aa72fa9a42 | ||
|
|
f0ae631cb0 |
6
.github/workflows/alpha.yml
vendored
6
.github/workflows/alpha.yml
vendored
@@ -307,7 +307,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -377,7 +377,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -505,7 +505,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
40
.github/workflows/autobuild.yml
vendored
40
.github/workflows/autobuild.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
target: aarch64-apple-darwin
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- os: ubuntu-22.04
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -165,10 +165,19 @@ jobs:
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
if: matrix.os == 'ubuntu-24.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt install \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev
|
||||
|
||||
- name: Install x86 OpenSSL (macOS only)
|
||||
if: matrix.target == 'x86_64-apple-darwin'
|
||||
@@ -187,7 +196,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -244,6 +253,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# It should be ubuntu-22.04 to match the cross-compilation environment
|
||||
# ortherwise it is hard to resolve the dependencies
|
||||
- os: ubuntu-22.04
|
||||
target: aarch64-unknown-linux-gnu
|
||||
arch: arm64
|
||||
@@ -283,7 +294,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -302,8 +313,8 @@ jobs:
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: Setup for linux
|
||||
run: |
|
||||
- name: "Setup for linux"
|
||||
run: |-
|
||||
sudo ls -lR /etc/apt/
|
||||
|
||||
cat > /tmp/sources.list << EOF
|
||||
@@ -322,14 +333,9 @@ jobs:
|
||||
sudo mv /tmp/sources.list /etc/apt/sources.list
|
||||
|
||||
sudo dpkg --add-architecture ${{ matrix.arch }}
|
||||
sudo apt-get update -y
|
||||
sudo apt-get -f install -y
|
||||
sudo apt update
|
||||
|
||||
sudo apt-get install -y \
|
||||
linux-libc-dev:${{ matrix.arch }} \
|
||||
libc6-dev:${{ matrix.arch }}
|
||||
|
||||
sudo apt-get install -y \
|
||||
sudo apt install -y \
|
||||
libxslt1.1:${{ matrix.arch }} \
|
||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||
@@ -433,7 +439,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -535,7 +541,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
2
.github/workflows/cross_check.yaml
vendored
2
.github/workflows/cross_check.yaml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
2
.github/workflows/dev.yml
vendored
2
.github/workflows/dev.yml
vendored
@@ -103,7 +103,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm Cache
|
||||
|
||||
2
.github/workflows/frontend-check.yml
vendored
2
.github/workflows/frontend-check.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Restore pnpm cache
|
||||
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -197,7 +197,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -218,7 +218,7 @@ jobs:
|
||||
|
||||
- name: Tauri build
|
||||
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
||||
uses: tauri-apps/tauri-action@v0.6.0
|
||||
uses: tauri-apps/tauri-action@v0.6.1
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -420,7 +420,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -448,7 +448,7 @@ jobs:
|
||||
|
||||
- name: Tauri build
|
||||
id: build
|
||||
uses: tauri-apps/tauri-action@v0.6.0
|
||||
uses: tauri-apps/tauri-action@v0.6.1
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -505,7 +505,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -531,7 +531,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -593,7 +593,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
4
.github/workflows/updater.yml
vendored
4
.github/workflows/updater.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.12.0"
|
||||
node-version: "24.13.0"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,4 +11,5 @@ scripts/_env.sh
|
||||
.idea
|
||||
.old
|
||||
.eslintcache
|
||||
target
|
||||
.changelog_backups
|
||||
target
|
||||
|
||||
@@ -1,51 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT_DIR"
|
||||
if ! command -v "cargo-make" >/dev/null 2>&1; then
|
||||
echo "❌ cargo-make is required for pre-commit checks."
|
||||
cargo install --force cargo-make
|
||||
fi
|
||||
|
||||
if ! command -v pnpm >/dev/null 2>&1; then
|
||||
echo "❌ pnpm is required for pre-commit checks."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOCALE_DIFF="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src/locales/' || true)"
|
||||
if [ -n "$LOCALE_DIFF" ]; then
|
||||
echo "[pre-commit] Locale changes detected. Regenerating i18n types..."
|
||||
pnpm i18n:types
|
||||
if [ -d src/types/generated ]; then
|
||||
echo "[pre-commit] Staging regenerated i18n type artifacts..."
|
||||
git add src/types/generated
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[pre-commit] Running pnpm format before lint..."
|
||||
pnpm format
|
||||
|
||||
echo "[pre-commit] Running lint-staged for JS/TS files..."
|
||||
pnpm exec lint-staged
|
||||
|
||||
RUST_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src-tauri/.*\.rs$' || true)"
|
||||
if [ -n "$RUST_FILES" ]; then
|
||||
echo "[pre-commit] Formatting Rust changes with cargo fmt..."
|
||||
cargo fmt
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] && git add "$file"
|
||||
done <<< "$RUST_FILES"
|
||||
|
||||
echo "[pre-commit] Linting Rust changes with cargo clippy..."
|
||||
cargo clippy-all
|
||||
if ! command -v clash-verge-logging-check >/dev/null 2>&1; then
|
||||
echo "[pre-commit] Installing clash-verge-logging-check..."
|
||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
|
||||
fi
|
||||
clash-verge-logging-check
|
||||
fi
|
||||
|
||||
TS_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ts|tsx)$' || true)"
|
||||
if [ -n "$TS_FILES" ]; then
|
||||
echo "[pre-commit] Running TypeScript type check..."
|
||||
pnpm typecheck
|
||||
fi
|
||||
|
||||
echo "[pre-commit] All checks completed successfully."
|
||||
cargo make pre-commit
|
||||
|
||||
@@ -1,36 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
remote_name="${1:-origin}"
|
||||
remote_url="${2:-unknown}"
|
||||
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if ! command -v pnpm >/dev/null 2>&1; then
|
||||
echo "❌ pnpm is required for pre-push checks."
|
||||
exit 1
|
||||
if ! command -v "cargo-make" >/dev/null 2>&1; then
|
||||
echo "❌ cargo-make is required for pre-push checks."
|
||||
cargo install --force cargo-make
|
||||
fi
|
||||
|
||||
echo "[pre-push] Preparing to push to '$remote_name' ($remote_url). Running full validation..."
|
||||
|
||||
echo "[pre-push] Checking Prettier formatting..."
|
||||
pnpm format:check
|
||||
|
||||
echo "[pre-push] Running ESLint..."
|
||||
pnpm lint
|
||||
|
||||
echo "[pre-push] Running TypeScript type checking..."
|
||||
pnpm typecheck
|
||||
|
||||
if command -v cargo >/dev/null 2>&1; then
|
||||
echo "[pre-push] Verifying Rust formatting..."
|
||||
cargo fmt --check
|
||||
|
||||
echo "[pre-push] Running cargo clippy..."
|
||||
cargo clippy-all
|
||||
else
|
||||
echo "[pre-push] ⚠️ cargo not found; skipping Rust checks."
|
||||
fi
|
||||
|
||||
echo "[pre-push] All checks passed."
|
||||
cargo make pre-push
|
||||
|
||||
5
.mergify.yml
Normal file
5
.mergify.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
queue_rules:
|
||||
- name: LetMeMergeForYou
|
||||
batch_size: 3
|
||||
allow_queue_branch_edit: true
|
||||
queue_conditions: []
|
||||
@@ -2,6 +2,7 @@
|
||||
# Changelog.md
|
||||
# CONTRIBUTING.md
|
||||
|
||||
.changelog_backups
|
||||
pnpm-lock.yaml
|
||||
|
||||
src-tauri/target/
|
||||
|
||||
1753
Cargo.lock
generated
1753
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@@ -1,11 +1,11 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"src-tauri",
|
||||
"crates/clash-verge-draft",
|
||||
"crates/clash-verge-logging",
|
||||
"crates/clash-verge-signal",
|
||||
"crates/tauri-plugin-clash-verge-sysinfo",
|
||||
"crates/clash-verge-types",
|
||||
"src-tauri",
|
||||
"crates/clash-verge-draft",
|
||||
"crates/clash-verge-logging",
|
||||
"crates/clash-verge-signal",
|
||||
"crates/tauri-plugin-clash-verge-sysinfo",
|
||||
"crates/clash-verge-i18n",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@@ -43,34 +43,39 @@ strip = false
|
||||
clash-verge-draft = { path = "crates/clash-verge-draft" }
|
||||
clash-verge-logging = { path = "crates/clash-verge-logging" }
|
||||
clash-verge-signal = { path = "crates/clash-verge-signal" }
|
||||
clash-verge-types = { path = "crates/clash-verge-types" }
|
||||
clash-verge-i18n = { path = "crates/clash-verge-i18n" }
|
||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||
|
||||
tauri = { version = "2.9.5" }
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||
anyhow = "1.0.100"
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
tokio = { version = "1.48.0", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
criterion = { version = "0.8.1", features = ["async_tokio"] }
|
||||
tokio = { version = "1.49.0", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
flexi_logger = "0.31.7"
|
||||
flexi_logger = "0.31.8"
|
||||
log = "0.4.29"
|
||||
|
||||
smartstring = { version = "1.0.1" }
|
||||
compact_str = { version = "0.9.0", features = ["serde"] }
|
||||
|
||||
serde = { version = "1.0.228" }
|
||||
serde_json = { version = "1.0.145" }
|
||||
serde_json = { version = "1.0.149" }
|
||||
serde_yaml_ng = { version = "0.10.0" }
|
||||
bitflags = { version = "2.10.0" }
|
||||
|
||||
# *** For Windows platform only ***
|
||||
deelevate = "0.2.0"
|
||||
# *********************************
|
||||
|
||||
[patch.crates-io]
|
||||
# Patches until https://github.com/tauri-apps/tao/pull/1167 is merged.
|
||||
tao = { git = "https://github.com/tauri-apps/tao" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
suspicious = { level = "deny", priority = -1 }
|
||||
|
||||
289
Changelog.md
289
Changelog.md
@@ -1,296 +1,23 @@
|
||||
## v2.4.4
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.17**
|
||||
## v(2.4.6)
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- Linux 无法切换 TUN 堆栈
|
||||
- macOS service 启动项显示名称(试验性修改)
|
||||
- macOS 非预期 Tproxy 端口设置
|
||||
- 流量图缩放异常
|
||||
- PAC 自动代理脚本内容无法动态调整
|
||||
- 兼容从旧版服务模式升级
|
||||
- Monaco 编辑器的行数上限
|
||||
- 已删除节点在手动分组中导致配置无法加载
|
||||
- 仪表盘与托盘状态不同步
|
||||
- 彻底修复 macOS 连接页面显示异常
|
||||
- windows 端监听关机信号失败
|
||||
- 修复代理按钮和高亮状态不同步
|
||||
- 修复侧边栏可能的未能正确跳转
|
||||
- 修复解锁测试部分地区图标编码不正确
|
||||
- 修复 IP 检测切页后强制刷新,改为仅在必要时更新
|
||||
- 修复在搜索框输入不完整正则直接崩溃
|
||||
- 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁
|
||||
- 修复更新时加载进度条异常
|
||||
- 升级内核失败导致内核不可用问题
|
||||
- 修复 macOS 在安装和卸载服务时提示与操作不匹配
|
||||
- 修复菜单排序模式拖拽异常
|
||||
- 修复托盘菜单代理组前的异常勾选状态
|
||||
- 修复 Windows 下自定义标题栏按钮在最小化 / 关闭后 hover 状态残留
|
||||
- 修复直接覆盖 `config.yaml` 使用时无法展开代理组
|
||||
- 修复 macOS 下应用启动时系统托盘图标颜色闪烁
|
||||
- 修复应用静默启动模式下非全局热键一直抢占其他应用按键问题
|
||||
- 修复首页当前节点卡片按延迟排序时,打开节点列表后,`timeout` 节点被排在正常节点前的问题
|
||||
- 修复首次启动时代理信息刷新缓慢
|
||||
- 修复无网络时无限请求 IP 归属查询
|
||||
- 修复 WebDAV 页面重试逻辑
|
||||
- 修复 Linux 通过 GUI 安装服务模式权限不符合预期
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 支持连接页面各个项目的排序
|
||||
- 实现可选的自动备份
|
||||
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
|
||||
- 日志页面支持按时间倒序
|
||||
- 增加「重新激活订阅」的全局快捷键
|
||||
- WebView2 Runtime 修复构建升级到 133.0.3065.92
|
||||
- 侧边栏右键新增「恢复默认排序」
|
||||
- Linux 下新增对 TUN 「自动重定向」(`auto-redirect` 字段)的配置支持,默认关闭
|
||||
- 支持订阅设置自动延时监测间隔
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 网络请求改为使用 rustls,提升 TLS 兼容性
|
||||
- rustls 避免因服务器证书链配置问题或较新 TLS 要求导致订阅无法导入
|
||||
- 替换前端信息编辑组件,提供更好性能
|
||||
- 优化后端内存和性能表现
|
||||
- 防止退出时可能的禁用 TUN 失败
|
||||
- 全新 i18n 支持方式
|
||||
- 优化备份设置布局
|
||||
- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停
|
||||
- 性能优化系统状态获取
|
||||
- 优化托盘菜单当前订阅检测逻辑
|
||||
- 优化连接页面表格渲染
|
||||
- 优化链式代理 UI 反馈
|
||||
- 优化重启应用的资源清理逻辑
|
||||
- 优化前端数据刷新
|
||||
- 优化流量采样和数据处理
|
||||
- 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间
|
||||
- 优化前端 WebSocket 连接机制
|
||||
- 改进旧版 Service 需要重新安装检测流程
|
||||
- 优化 macOS, Linux 和 Windows 系统信号处理
|
||||
- 链式代理仅显示 Selector 类型规则组
|
||||
- 优化 Windows 系统代理设置,不再依赖 `sysproxy.exe` 来设置代理
|
||||
- 后端性能优化
|
||||
- 前端性能优化
|
||||
|
||||
</details>
|
||||
|
||||
## v2.4.3
|
||||
|
||||
**发行代号:澜**
|
||||
代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。
|
||||
|
||||
特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 优化服务模式重装逻辑,避免不必要的重复检查
|
||||
- 修复轻量模式退出无响应的问题
|
||||
- 修复托盘轻量模式支持退出/进入
|
||||
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
|
||||
- macOS Tun/系统代理 模式下图标大小不统一
|
||||
- 托盘节点切换不再显示隐藏组
|
||||
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
|
||||
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
|
||||
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
|
||||
- 修复 Webdav 恢复备份不重启
|
||||
- 修复 Linux 开机后无法正常代理需要手动设置
|
||||
- 修复增加订阅或导入订阅文件时订阅页面无更新
|
||||
- 修复系统代理守卫功能不工作
|
||||
- 修复 KDE + Wayland 下多屏显示 UI 异常
|
||||
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
|
||||
- 修复静默启动不加载完整 WebView 的问题
|
||||
- 修复 Linux WebKit 网络进程的崩溃
|
||||
- 修复无法导入订阅
|
||||
- 修复实际导入成功但显示导入失败的问题
|
||||
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
|
||||
- 修复删除订阅时未能实际删除相关文件
|
||||
- 修复 macOS 连接界面显示异常
|
||||
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
|
||||
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
|
||||
- 修复自动更新使版本回退的问题
|
||||
- 修复首页自定义卡片在切换轻量模式时失效
|
||||
- 修复悬浮跳转导航失效
|
||||
- 修复小键盘热键映射错误
|
||||
- 修复前端无法及时刷新操作状态
|
||||
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
|
||||
- 修复 Linux 系统主题切换不生效
|
||||
- 修复 `允许自动更新` 字段使手动订阅刷新失效
|
||||
- 修复轻量模式托盘状态不同步
|
||||
- 修复一键导入订阅导致应用卡死崩溃的问题
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.15**
|
||||
- 支持前端修改日志(最大文件大小、最大保留数量)
|
||||
- 新增链式代理图形化设置功能
|
||||
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
|
||||
- 监听关机事件,自动关闭系统代理
|
||||
- 主界面“当前节点”卡片新增“延迟测试”按钮
|
||||
- 新增批量选择配置文件功能
|
||||
- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置
|
||||
- 新增本地备份功能
|
||||
- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭)
|
||||
- 允许独立控制订阅自动更新
|
||||
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
|
||||
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
|
||||
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 重构并简化服务模式启动检测流程,消除重复检测
|
||||
- 重构并简化窗口创建流程
|
||||
- 重构日志系统,单个日志默认最大 10 MB
|
||||
- 优化前端资源占用
|
||||
- 改进 macos 下系统代理设置的方法
|
||||
- 优化 TUN 模式可用性的判断
|
||||
- 移除流媒体检测的系统级提示(使用软件内通知)
|
||||
- 优化后端 i18n 资源占用
|
||||
- 改进 Linux 托盘支持并添加 `--no-tray` 选项
|
||||
- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式
|
||||
- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL
|
||||
- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端
|
||||
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
|
||||
- 修改内核默认日志级别为 Info
|
||||
- 支持通过桌面快捷方式重新打开应用
|
||||
- 支持订阅界面输入链接后回车导入
|
||||
- 选择按延迟排序时每次延迟测试自动刷新节点顺序
|
||||
- 配置重载失败时自动重启核心
|
||||
- 启用 TUN 前等待服务就绪
|
||||
- 卸载 TUN 时会先关闭
|
||||
- 优化应用启动页
|
||||
- 优化首页当前节点对MATCH规则的支持
|
||||
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
|
||||
- 添加热键绑定错误的提示信息
|
||||
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
|
||||
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
|
||||
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
|
||||
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
|
||||
|
||||
</details>
|
||||
|
||||
## v2.4.2
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- 增加托盘节点选择
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 优化前端首页加载速度
|
||||
- 优化前端未使用 i18n 文件缓存
|
||||
- 优化后端内存占用
|
||||
- 优化后端启动速度
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复首页节点切换失效的问题
|
||||
- 修复和优化服务检查流程
|
||||
- 修复2.4.1引入的订阅地址重定向报错问题
|
||||
- 修复 rpm/deb 包名称问题
|
||||
- 修复托盘轻量模式状态检测异常
|
||||
- 修复通过 scheme 导入订阅崩溃
|
||||
- 修复单例检测实效
|
||||
- 修复启动阶段可能导致的无法连接内核
|
||||
- 修复导入订阅无法 Auth Basic
|
||||
|
||||
### 👙 界面样式
|
||||
|
||||
- 简化和改进代理设置样式
|
||||
|
||||
## v2.4.1
|
||||
|
||||
### 🏆 重大改进
|
||||
|
||||
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.13**
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 优化热键响应速度,提升快捷键操作体验
|
||||
- 改进服务管理响应性,减少系统服务操作等待时间
|
||||
- 提升文件和配置处理性能
|
||||
- 优化任务管理和日志记录效率
|
||||
- 优化异步内存管理,减少内存占用并提升多任务处理效率
|
||||
- 优化启动阶段初始化性能
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复应用在某些操作中可能出现的响应延迟问题
|
||||
- 修复任务管理中的潜在并发问题
|
||||
- 修复通过托盘重启应用无法恢复
|
||||
- 修复订阅在某些情况下无法导入
|
||||
- 修复无法新建订阅时使用远程链接
|
||||
- 修复卸载服务后的 tun 开关状态问题
|
||||
- 修复页面快速切换订阅时导致崩溃
|
||||
- 修复丢失工作目录时无法恢复环境
|
||||
- 修复从轻量模式恢复导致崩溃
|
||||
|
||||
### 👙 界面样式
|
||||
|
||||
- 统一代理设置样式
|
||||
|
||||
### 🗑️ 移除内容
|
||||
|
||||
- 移除启动阶段自动清理过期订阅
|
||||
|
||||
## v2.4.0
|
||||
|
||||
**发行代号:融**
|
||||
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
|
||||
|
||||
### 🏆 重大改进
|
||||
|
||||
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
|
||||
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
|
||||
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.12**
|
||||
- 新增版本信息复制按钮
|
||||
- 增强型流量监控,支持更详细的数据分析
|
||||
- 新增流量图表多种显示模式
|
||||
- 新增强制刷新配置和节点缓存功能
|
||||
- 首页流量统计支持查看刻度线详情
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 全面提升数据传输和处理效率
|
||||
- 优化内存使用,减少系统资源消耗
|
||||
- 改进流量图表渲染性能
|
||||
- 优化配置和节点刷新策略,从5秒延长到60秒
|
||||
- 改进数据缓存机制,减少重复请求
|
||||
- 优化异步程序性能
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复系统代理状态检测和显示不一致问题
|
||||
- 修复系统主题窗口颜色不一致问题
|
||||
- 修复特殊字符 URL 处理问题
|
||||
- 修复配置修改后缓存不同步问题
|
||||
- 修复 Windows 安装器自启设置问题
|
||||
- 修复 macOS 下 Dock 图标恢复窗口问题
|
||||
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
|
||||
- 修复架构升级后节点测速功能异常
|
||||
- 修复架构升级后流量统计功能异常
|
||||
- 修复架构升级后日志功能异常
|
||||
- 修复外部控制器跨域配置保存问题
|
||||
- 修复首页端口显示不一致问题
|
||||
- 修复首页流量统计刻度线显示问题
|
||||
- 修复日志页面按钮功能混淆问题
|
||||
- 修复日志等级设置保存问题
|
||||
- 修复日志等级异常过滤
|
||||
- 修复清理日志天数功能异常
|
||||
- 修复偶发性启动卡死问题
|
||||
- 修复首页虚拟网卡开关在管理模式下的状态问题
|
||||
|
||||
### 🔧 技术改进
|
||||
|
||||
- 统一使用新的内核通信方式
|
||||
- 新增外部控制器配置界面
|
||||
- 改进跨平台兼容性支持
|
||||
|
||||
86
Makefile.toml
Normal file
86
Makefile.toml
Normal file
@@ -0,0 +1,86 @@
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
skip_git_env_info = true
|
||||
skip_rust_env_info = true
|
||||
skip_crate_env_info = true
|
||||
|
||||
# --- Backend ---
|
||||
|
||||
[tasks.rust-format]
|
||||
install_crate = "rustfmt"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--", "--emit=files"]
|
||||
|
||||
[tasks.rust-clippy]
|
||||
description = "Run cargo clippy to lint the code"
|
||||
command = "cargo"
|
||||
args = ["clippy", "--all-targets", "--all-features", "--", "-D", "warnings"]
|
||||
|
||||
# --- Frontend ---
|
||||
|
||||
[tasks.eslint]
|
||||
description = "Run ESLint to lint the code"
|
||||
command = "pnpm"
|
||||
args = ["lint"]
|
||||
[tasks.eslint.windows]
|
||||
command = "pnpm.cmd"
|
||||
|
||||
[tasks.typecheck]
|
||||
description = "Run type checks"
|
||||
command = "pnpm"
|
||||
args = ["typecheck"]
|
||||
[tasks.typecheck.windows]
|
||||
command = "pnpm.cmd"
|
||||
|
||||
[tasks.lint-staged]
|
||||
description = "Run lint-staged for staged files"
|
||||
command = "pnpm"
|
||||
args = ["exec", "lint-staged"]
|
||||
[tasks.lint-staged.windows]
|
||||
command = "pnpm.cmd"
|
||||
|
||||
# --- Jobs ---
|
||||
|
||||
# Rust format (for pre-commit)
|
||||
[tasks.rust-format-check]
|
||||
description = "Check Rust code formatting"
|
||||
dependencies = ["rust-format"]
|
||||
[tasks.rust-format-check.condition]
|
||||
files_modified.input = [
|
||||
"./src-tauri/**/*.rs",
|
||||
"./crates/**/*.rs",
|
||||
"**/Cargo.toml",
|
||||
]
|
||||
files_modified.output = ["./target/debug/*", "./target/release/*"]
|
||||
|
||||
# Rust lint (for pre-push)
|
||||
[tasks.rust-lint]
|
||||
description = "Run Rust linting"
|
||||
dependencies = ["rust-clippy"]
|
||||
[tasks.rust-lint.condition]
|
||||
files_modified.input = [
|
||||
"./src-tauri/**/*.rs",
|
||||
"./crates/**/*.rs",
|
||||
"**/Cargo.toml",
|
||||
]
|
||||
files_modified.output = ["./target/debug/*", "./target/release/*"]
|
||||
|
||||
# Frontend format (for pre-commit)
|
||||
[tasks.frontend-format]
|
||||
description = "Frontend format checks"
|
||||
dependencies = ["lint-staged"]
|
||||
|
||||
# Frontend lint (for pre-push)
|
||||
[tasks.frontend-lint]
|
||||
description = "Frontend linting and type checking"
|
||||
dependencies = ["eslint", "typecheck"]
|
||||
|
||||
# --- Git Hooks ---
|
||||
|
||||
[tasks.pre-commit]
|
||||
description = "Pre-commit checks: format only"
|
||||
dependencies = ["rust-format-check", "frontend-format"]
|
||||
|
||||
[tasks.pre-push]
|
||||
description = "Pre-push checks: lint and typecheck"
|
||||
dependencies = ["rust-lint", "frontend-lint"]
|
||||
13
README.md
13
README.md
@@ -66,19 +66,6 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
🌐 官网:👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
|
||||
|
||||
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
|
||||
|
||||
🧩 YXVM 独立服务器优势:
|
||||
|
||||
- 🌎 优质网络,回程优化,下载快到飞起
|
||||
- 🔧 物理机独享资源,非VPS可比,性能拉满
|
||||
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
|
||||
- 💡 支持即开即用,多机房选择,CN2 / IEPL 可选
|
||||
- 📦 本项目使用配置已在售,欢迎同款入手!
|
||||
- 🎯 想要同款构建体验?[立即下单 YXVM 独立服务器!](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## Features
|
||||
|
||||
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
||||
|
||||
@@ -104,8 +104,7 @@ pub fn bench_draft(c: &mut Criterion) {
|
||||
let draft = black_box(make_draft());
|
||||
let _: Result<(), anyhow::Error> = draft
|
||||
.with_data_modify::<_, _, _>(|mut box_data| async move {
|
||||
box_data.enable_auto_launch =
|
||||
Some(!box_data.enable_auto_launch.unwrap_or(false));
|
||||
box_data.enable_auto_launch = Some(!box_data.enable_auto_launch.unwrap_or(false));
|
||||
Ok((box_data, ()))
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type SharedBox<T> = Arc<Box<T>>;
|
||||
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
|
||||
pub type SharedDraft<T> = Arc<T>;
|
||||
type DraftInner<T> = (SharedDraft<T>, Option<SharedDraft<T>>);
|
||||
|
||||
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
|
||||
// (committed_snapshot, optional_draft_snapshot)
|
||||
@@ -15,12 +15,12 @@ impl<T: Clone> Draft<T> {
|
||||
#[inline]
|
||||
pub fn new(data: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
|
||||
inner: Arc::new(RwLock::new((Arc::new(data), None))),
|
||||
}
|
||||
}
|
||||
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
|
||||
#[inline]
|
||||
pub fn data_arc(&self) -> SharedBox<T> {
|
||||
pub fn data_arc(&self) -> SharedDraft<T> {
|
||||
let guard = self.inner.read();
|
||||
Arc::clone(&guard.0)
|
||||
}
|
||||
@@ -28,7 +28,7 @@ impl<T: Clone> Draft<T> {
|
||||
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
|
||||
/// 这也是零拷贝:只 clone Arc,不 clone T
|
||||
#[inline]
|
||||
pub fn latest_arc(&self) -> SharedBox<T> {
|
||||
pub fn latest_arc(&self) -> SharedDraft<T> {
|
||||
let guard = self.inner.read();
|
||||
guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0))
|
||||
}
|
||||
@@ -41,21 +41,11 @@ impl<T: Clone> Draft<T> {
|
||||
where
|
||||
F: FnOnce(&mut T) -> R,
|
||||
{
|
||||
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
|
||||
let mut guard = self.inner.write();
|
||||
let mut draft_arc = if guard.1.is_none() {
|
||||
Arc::clone(&guard.0)
|
||||
} else {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
guard.1.take().unwrap()
|
||||
};
|
||||
drop(guard);
|
||||
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
|
||||
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
|
||||
// 对 Box<T> 解引用得到 &mut T
|
||||
let result = f(&mut **boxed);
|
||||
// 恢复修改后的草稿 Arc
|
||||
self.inner.write().1 = Some(draft_arc);
|
||||
let mut draft_arc = guard.1.take().unwrap_or_else(|| Arc::clone(&guard.0));
|
||||
let data_mut = Arc::make_mut(&mut draft_arc);
|
||||
let result = f(data_mut);
|
||||
guard.1 = Some(draft_arc);
|
||||
result
|
||||
}
|
||||
|
||||
@@ -81,22 +71,22 @@ impl<T: Clone> Draft<T> {
|
||||
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: FnOnce(Box<T>) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
|
||||
F: FnOnce(T) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<(T, R), anyhow::Error>> + Send,
|
||||
{
|
||||
// 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
|
||||
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
|
||||
let local: Box<T> = {
|
||||
let (local, original_arc) = {
|
||||
let guard = self.inner.read();
|
||||
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
|
||||
(*guard.0).clone()
|
||||
let arc = Arc::clone(&guard.0);
|
||||
((*arc).clone(), arc)
|
||||
};
|
||||
|
||||
let (new_local, res) = f(local).await?;
|
||||
|
||||
// 将新的 Box<T> 放到已提交位置(包进 Arc)
|
||||
self.inner.write().0 = Arc::new(new_local);
|
||||
|
||||
let mut guard = self.inner.write();
|
||||
if !Arc::ptr_eq(&guard.0, &original_arc) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Optimistic lock failed: Committed data has changed during async operation"
|
||||
));
|
||||
}
|
||||
guard.0 = Arc::from(new_local);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,10 +133,7 @@ mod tests {
|
||||
let prev_draft_ptr = std::sync::Arc::as_ptr(&draft_after_first_edit);
|
||||
draft.apply();
|
||||
let committed_after_apply = draft.data_arc();
|
||||
assert_eq!(
|
||||
std::sync::Arc::as_ptr(&committed_after_apply),
|
||||
prev_draft_ptr
|
||||
);
|
||||
assert_eq!(std::sync::Arc::as_ptr(&committed_after_apply), prev_draft_ptr);
|
||||
|
||||
// 第二次编辑:此时草稿唯一持有(无其它引用),不应再克隆
|
||||
// 获取草稿 Arc 的指针并立即丢弃本地引用,避免增加 strong_count
|
||||
@@ -198,7 +195,7 @@ mod tests {
|
||||
// 使用 with_data_modify 异步(立即就绪)地更新 committed
|
||||
let res = block_on_ready(draft.with_data_modify(|mut v| async move {
|
||||
v.enable_auto_launch = Some(true);
|
||||
Ok((Box::new(*v), "done")) // Dereference v to get Box<T>
|
||||
Ok((v, "done"))
|
||||
}));
|
||||
assert_eq!(
|
||||
{
|
||||
@@ -218,11 +215,8 @@ mod tests {
|
||||
let draft = Draft::new(IVerge::default());
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let err = block_on_ready(draft.with_data_modify(|v| async move {
|
||||
drop(v);
|
||||
Err::<(Box<IVerge>, ()), _>(anyhow!("boom"))
|
||||
}))
|
||||
.unwrap_err();
|
||||
let err = block_on_ready(draft.with_data_modify(|_v| async move { Err::<(IVerge, ()), _>(anyhow!("boom")) }))
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(format!("{err}"), "boom");
|
||||
}
|
||||
@@ -246,7 +240,7 @@ mod tests {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
block_on_ready(draft.with_data_modify(|mut v| async move {
|
||||
v.enable_auto_launch = Some(false); // 与草稿不同
|
||||
Ok((Box::new(*v), ())) // Dereference v to get Box<T>
|
||||
Ok((v, ()))
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
|
||||
11
crates/clash-verge-i18n/Cargo.toml
Normal file
11
crates/clash-verge-i18n/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "clash-verge-i18n"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rust-i18n = "3.1.5"
|
||||
sys-locale = "0.3.2"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
60
crates/clash-verge-i18n/locales/ar.yml
Normal file
60
crates/clash-verge-i18n/locales/ar.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: لوحة التحكم
|
||||
body: تم تحديث حالة عرض لوحة التحكم.
|
||||
clashModeChanged:
|
||||
title: تبديل الوضع
|
||||
body: تم التبديل إلى {mode}.
|
||||
systemProxyToggled:
|
||||
title: وكيل النظام
|
||||
body: تم تحديث حالة وكيل النظام.
|
||||
tunModeToggled:
|
||||
title: وضع TUN
|
||||
body: تم تحديث حالة وضع TUN.
|
||||
lightweightModeEntered:
|
||||
title: الوضع الخفيف
|
||||
body: تم الدخول إلى الوضع الخفيف.
|
||||
profilesReactivated:
|
||||
title: الملفات التعريفية
|
||||
body: تمت إعادة تفعيل الملف التعريفي.
|
||||
appQuit:
|
||||
title: على وشك الخروج
|
||||
body: Clash Verge على وشك الخروج.
|
||||
appHidden:
|
||||
title: تم إخفاء التطبيق
|
||||
body: Clash Verge يعمل في الخلفية.
|
||||
service:
|
||||
adminInstallPrompt: يتطلب تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||
adminUninstallPrompt: يتطلب إلغاء تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||
tray:
|
||||
dashboard: لوحة التحكم
|
||||
ruleMode: وضع القواعد
|
||||
globalMode: الوضع العام
|
||||
directMode: الوضع المباشر
|
||||
outboundModes: أوضاع الخروج
|
||||
rule: قاعدة
|
||||
direct: مباشر
|
||||
global: عام
|
||||
profiles: الملفات التعريفية
|
||||
proxies: وكلاء
|
||||
systemProxy: وكيل النظام
|
||||
tunMode: وضع TUN
|
||||
closeAllConnections: إغلاق كل الاتصالات
|
||||
lightweightMode: الوضع الخفيف
|
||||
copyEnv: نسخ متغيرات البيئة
|
||||
confDir: دليل الإعدادات
|
||||
coreDir: دليل النواة
|
||||
logsDir: دليل السجلات
|
||||
openDir: فتح الدليل
|
||||
appLog: سجل التطبيق
|
||||
coreLog: سجل النواة
|
||||
restartClash: إعادة تشغيل نواة Clash
|
||||
restartApp: إعادة تشغيل التطبيق
|
||||
vergeVersion: إصدار Verge
|
||||
more: المزيد
|
||||
exit: خروج
|
||||
tooltip:
|
||||
systemProxy: وكيل النظام
|
||||
tun: TUN
|
||||
profile: ملف تعريفي
|
||||
60
crates/clash-verge-i18n/locales/de.yml
Normal file
60
crates/clash-verge-i18n/locales/de.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Übersicht
|
||||
body: Die Sichtbarkeit der Übersicht wurde aktualisiert.
|
||||
clashModeChanged:
|
||||
title: Moduswechsel
|
||||
body: Auf {mode} umgeschaltet.
|
||||
systemProxyToggled:
|
||||
title: Systemproxy
|
||||
body: Der Status des Systemproxys wurde aktualisiert.
|
||||
tunModeToggled:
|
||||
title: TUN-Modus
|
||||
body: Der Status des TUN-Modus wurde aktualisiert.
|
||||
lightweightModeEntered:
|
||||
title: Leichtmodus
|
||||
body: Leichtmodus aktiviert.
|
||||
profilesReactivated:
|
||||
title: Profile
|
||||
body: Profil reaktiviert.
|
||||
appQuit:
|
||||
title: Beenden steht bevor
|
||||
body: Clash Verge wird gleich beendet.
|
||||
appHidden:
|
||||
title: Anwendung ausgeblendet
|
||||
body: Clash Verge läuft im Hintergrund.
|
||||
service:
|
||||
adminInstallPrompt: Für die Installation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
||||
adminUninstallPrompt: Für die Deinstallation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
||||
tray:
|
||||
dashboard: Übersicht
|
||||
ruleMode: Regelmodus
|
||||
globalMode: Globaler Modus
|
||||
directMode: Direktmodus
|
||||
outboundModes: Ausgangsmodi
|
||||
rule: Regel
|
||||
direct: Direkt
|
||||
global: Global
|
||||
profiles: Profile
|
||||
proxies: Proxy
|
||||
systemProxy: Systemproxy
|
||||
tunMode: TUN-Modus
|
||||
closeAllConnections: Alle Verbindungen schließen
|
||||
lightweightMode: Leichtmodus
|
||||
copyEnv: Umgebungsvariablen kopieren
|
||||
confDir: Konfigurationsverzeichnis
|
||||
coreDir: Core-Verzeichnis
|
||||
logsDir: Log-Verzeichnis
|
||||
openDir: Verzeichnis öffnen
|
||||
appLog: Anwendungslog
|
||||
coreLog: Core-Log
|
||||
restartClash: Clash-Core neu starten
|
||||
restartApp: Anwendung neu starten
|
||||
vergeVersion: Verge-Version
|
||||
more: Mehr
|
||||
exit: Beenden
|
||||
tooltip:
|
||||
systemProxy: Systemproxy
|
||||
tun: TUN
|
||||
profile: Profil
|
||||
@@ -25,9 +25,8 @@ notifications:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminInstallPrompt: Installing the service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the service requires administrator privileges.
|
||||
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
60
crates/clash-verge-i18n/locales/es.yml
Normal file
60
crates/clash-verge-i18n/locales/es.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Panel
|
||||
body: La visibilidad del panel se ha actualizado.
|
||||
clashModeChanged:
|
||||
title: Cambio de modo
|
||||
body: Cambiado a {mode}.
|
||||
systemProxyToggled:
|
||||
title: Proxy del sistema
|
||||
body: El estado del proxy del sistema se ha actualizado.
|
||||
tunModeToggled:
|
||||
title: Modo TUN
|
||||
body: El estado del modo TUN se ha actualizado.
|
||||
lightweightModeEntered:
|
||||
title: Modo ligero
|
||||
body: Se ha entrado en el modo ligero.
|
||||
profilesReactivated:
|
||||
title: Perfiles
|
||||
body: Perfil reactivado.
|
||||
appQuit:
|
||||
title: A punto de salir
|
||||
body: Clash Verge está a punto de salir.
|
||||
appHidden:
|
||||
title: Aplicación oculta
|
||||
body: Clash Verge se está ejecutando en segundo plano.
|
||||
service:
|
||||
adminInstallPrompt: Instalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||
adminUninstallPrompt: Desinstalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||
tray:
|
||||
dashboard: Panel
|
||||
ruleMode: Modo de reglas
|
||||
globalMode: Modo global
|
||||
directMode: Modo directo
|
||||
outboundModes: Modos de salida
|
||||
rule: Regla
|
||||
direct: Directo
|
||||
global: Global
|
||||
profiles: Perfiles
|
||||
proxies: Proxies
|
||||
systemProxy: Proxy del sistema
|
||||
tunMode: Modo TUN
|
||||
closeAllConnections: Cerrar todas las conexiones
|
||||
lightweightMode: Modo ligero
|
||||
copyEnv: Copiar variables de entorno
|
||||
confDir: Directorio de configuración
|
||||
coreDir: Directorio del núcleo
|
||||
logsDir: Directorio de registros
|
||||
openDir: Abrir directorio
|
||||
appLog: Registro de la aplicación
|
||||
coreLog: Registro del núcleo
|
||||
restartClash: Reiniciar el núcleo de Clash
|
||||
restartApp: Reiniciar aplicación
|
||||
vergeVersion: Versión de Verge
|
||||
more: Más
|
||||
exit: Salir
|
||||
tooltip:
|
||||
systemProxy: Proxy del sistema
|
||||
tun: TUN
|
||||
profile: Perfil
|
||||
60
crates/clash-verge-i18n/locales/fa.yml
Normal file
60
crates/clash-verge-i18n/locales/fa.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: داشبورد
|
||||
body: وضعیت نمایش داشبورد بهروزرسانی شد.
|
||||
clashModeChanged:
|
||||
title: تغییر حالت
|
||||
body: به {mode} تغییر کرد.
|
||||
systemProxyToggled:
|
||||
title: پروکسی سیستم
|
||||
body: وضعیت پروکسی سیستم بهروزرسانی شد.
|
||||
tunModeToggled:
|
||||
title: حالت TUN
|
||||
body: وضعیت حالت TUN بهروزرسانی شد.
|
||||
lightweightModeEntered:
|
||||
title: حالت سبک
|
||||
body: به حالت سبک وارد شد.
|
||||
profilesReactivated:
|
||||
title: پروفایلها
|
||||
body: پروفایل دوباره فعال شد.
|
||||
appQuit:
|
||||
title: در آستانه خروج
|
||||
body: Clash Verge در آستانه خروج است.
|
||||
appHidden:
|
||||
title: برنامه پنهان شد
|
||||
body: Clash Verge در پسزمینه در حال اجراست.
|
||||
service:
|
||||
adminInstallPrompt: نصب سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||
adminUninstallPrompt: حذف سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||
tray:
|
||||
dashboard: داشبورد
|
||||
ruleMode: حالت قوانین
|
||||
globalMode: حالت سراسری
|
||||
directMode: حالت مستقیم
|
||||
outboundModes: حالتهای خروجی
|
||||
rule: قانون
|
||||
direct: مستقیم
|
||||
global: سراسری
|
||||
profiles: پروفایلها
|
||||
proxies: پروکسیها
|
||||
systemProxy: پروکسی سیستم
|
||||
tunMode: حالت TUN
|
||||
closeAllConnections: بستن همه اتصالها
|
||||
lightweightMode: حالت سبک
|
||||
copyEnv: کپی متغیرهای محیطی
|
||||
confDir: پوشه پیکربندی
|
||||
coreDir: پوشه هسته
|
||||
logsDir: پوشه گزارشها
|
||||
openDir: باز کردن پوشه
|
||||
appLog: گزارش برنامه
|
||||
coreLog: گزارش هسته
|
||||
restartClash: راهاندازی مجدد هسته Clash
|
||||
restartApp: راهاندازی مجدد برنامه
|
||||
vergeVersion: نسخه Verge
|
||||
more: بیشتر
|
||||
exit: خروج
|
||||
tooltip:
|
||||
systemProxy: پروکسی سیستم
|
||||
tun: TUN
|
||||
profile: پروفایل
|
||||
60
crates/clash-verge-i18n/locales/id.yml
Normal file
60
crates/clash-verge-i18n/locales/id.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dasbor
|
||||
body: Visibilitas dasbor telah diperbarui.
|
||||
clashModeChanged:
|
||||
title: Peralihan Mode
|
||||
body: Beralih ke {mode}.
|
||||
systemProxyToggled:
|
||||
title: Proksi Sistem
|
||||
body: Status proksi sistem telah diperbarui.
|
||||
tunModeToggled:
|
||||
title: Mode TUN
|
||||
body: Status mode TUN telah diperbarui.
|
||||
lightweightModeEntered:
|
||||
title: Mode Ringan
|
||||
body: Masuk ke mode ringan.
|
||||
profilesReactivated:
|
||||
title: Profil
|
||||
body: Profil diaktifkan kembali.
|
||||
appQuit:
|
||||
title: Akan Keluar
|
||||
body: Clash Verge akan keluar.
|
||||
appHidden:
|
||||
title: Aplikasi Disembunyikan
|
||||
body: Clash Verge berjalan di latar belakang.
|
||||
service:
|
||||
adminInstallPrompt: Menginstal layanan Clash Verge memerlukan hak administrator.
|
||||
adminUninstallPrompt: Menghapus instalasi layanan Clash Verge memerlukan hak administrator.
|
||||
tray:
|
||||
dashboard: Dasbor
|
||||
ruleMode: Mode Aturan
|
||||
globalMode: Mode Global
|
||||
directMode: Mode Langsung
|
||||
outboundModes: Mode Keluar
|
||||
rule: Aturan
|
||||
direct: Langsung
|
||||
global: Global
|
||||
profiles: Profil
|
||||
proxies: Proksi
|
||||
systemProxy: Proksi Sistem
|
||||
tunMode: Mode TUN
|
||||
closeAllConnections: Tutup Semua Koneksi
|
||||
lightweightMode: Mode Ringan
|
||||
copyEnv: Salin Variabel Lingkungan
|
||||
confDir: Direktori Konfigurasi
|
||||
coreDir: Direktori Core
|
||||
logsDir: Direktori Log
|
||||
openDir: Buka Direktori
|
||||
appLog: Log Aplikasi
|
||||
coreLog: Log Core
|
||||
restartClash: Mulai Ulang Core Clash
|
||||
restartApp: Mulai Ulang Aplikasi
|
||||
vergeVersion: Versi Verge
|
||||
more: Lainnya
|
||||
exit: Keluar
|
||||
tooltip:
|
||||
systemProxy: Proksi Sistem
|
||||
tun: TUN
|
||||
profile: Profil
|
||||
60
crates/clash-verge-i18n/locales/jp.yml
Normal file
60
crates/clash-verge-i18n/locales/jp.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: ダッシュボード
|
||||
body: ダッシュボードの表示状態が更新されました。
|
||||
clashModeChanged:
|
||||
title: モード切り替え
|
||||
body: "{mode} に切り替えました。"
|
||||
systemProxyToggled:
|
||||
title: システムプロキシ
|
||||
body: システムプロキシの状態が更新されました。
|
||||
tunModeToggled:
|
||||
title: TUN モード
|
||||
body: TUN モードの状態が更新されました。
|
||||
lightweightModeEntered:
|
||||
title: 軽量モード
|
||||
body: 軽量モードに入りました。
|
||||
profilesReactivated:
|
||||
title: プロファイル
|
||||
body: プロファイルが再有効化されました。
|
||||
appQuit:
|
||||
title: 終了間近
|
||||
body: Clash Verge はまもなく終了します。
|
||||
appHidden:
|
||||
title: アプリが非表示
|
||||
body: Clash Verge はバックグラウンドで実行中です。
|
||||
service:
|
||||
adminInstallPrompt: Clash Verge サービスのインストールには管理者権限が必要です。
|
||||
adminUninstallPrompt: Clash Verge サービスのアンインストールには管理者権限が必要です。
|
||||
tray:
|
||||
dashboard: ダッシュボード
|
||||
ruleMode: ルールモード
|
||||
globalMode: グローバルモード
|
||||
directMode: ダイレクトモード
|
||||
outboundModes: アウトバウンドモード
|
||||
rule: ルール
|
||||
direct: ダイレクト
|
||||
global: グローバル
|
||||
profiles: プロファイル
|
||||
proxies: プロキシ
|
||||
systemProxy: システムプロキシ
|
||||
tunMode: TUN モード
|
||||
closeAllConnections: すべての接続を閉じる
|
||||
lightweightMode: 軽量モード
|
||||
copyEnv: 環境変数をコピー
|
||||
confDir: 設定ディレクトリ
|
||||
coreDir: コアディレクトリ
|
||||
logsDir: ログディレクトリ
|
||||
openDir: ディレクトリを開く
|
||||
appLog: アプリケーションログ
|
||||
coreLog: コアログ
|
||||
restartClash: Clash コアを再起動
|
||||
restartApp: アプリケーションを再起動
|
||||
vergeVersion: Verge バージョン
|
||||
more: その他
|
||||
exit: 終了
|
||||
tooltip:
|
||||
systemProxy: システムプロキシ
|
||||
tun: TUN
|
||||
profile: プロファイル
|
||||
@@ -16,8 +16,8 @@ notifications:
|
||||
title: 경량 모드
|
||||
body: 경량 모드에 진입했습니다.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: 프로필
|
||||
body: 프로필이 다시 활성화되었습니다.
|
||||
appQuit:
|
||||
title: 곧 종료
|
||||
body: Clash Verge가 곧 종료됩니다.
|
||||
@@ -25,13 +25,14 @@ notifications:
|
||||
title: 앱이 숨겨짐
|
||||
body: Clash Verge가 백그라운드에서 실행 중입니다.
|
||||
service:
|
||||
adminPrompt: 서비스를 설치하려면 관리자 권한이 필요합니다.
|
||||
adminInstallPrompt: Clash Verge 서비스 설치에는 관리자 권한이 필요합니다.
|
||||
adminUninstallPrompt: Clash Verge 서비스 제거에는 관리자 권한이 필요합니다.
|
||||
tray:
|
||||
dashboard: 대시보드
|
||||
ruleMode: 규칙 모드
|
||||
globalMode: 전역 모드
|
||||
directMode: 직접 모드
|
||||
outboundModes: Outbound Modes
|
||||
outboundModes: 아웃바운드 모드
|
||||
rule: 규칙
|
||||
direct: 직접
|
||||
global: 글로벌
|
||||
60
crates/clash-verge-i18n/locales/ru.yml
Normal file
60
crates/clash-verge-i18n/locales/ru.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Панель
|
||||
body: Видимость панели обновлена.
|
||||
clashModeChanged:
|
||||
title: Смена режима
|
||||
body: Переключено на {mode}.
|
||||
systemProxyToggled:
|
||||
title: Системный прокси
|
||||
body: Статус системного прокси обновлен.
|
||||
tunModeToggled:
|
||||
title: Режим TUN
|
||||
body: Статус режима TUN обновлен.
|
||||
lightweightModeEntered:
|
||||
title: Легкий режим
|
||||
body: Включен легкий режим.
|
||||
profilesReactivated:
|
||||
title: Профили
|
||||
body: Профиль повторно активирован.
|
||||
appQuit:
|
||||
title: Скорый выход
|
||||
body: Clash Verge скоро завершит работу.
|
||||
appHidden:
|
||||
title: Приложение скрыто
|
||||
body: Clash Verge работает в фоновом режиме.
|
||||
service:
|
||||
adminInstallPrompt: Для установки службы Clash Verge требуются права администратора.
|
||||
adminUninstallPrompt: Для удаления службы Clash Verge требуются права администратора.
|
||||
tray:
|
||||
dashboard: Панель
|
||||
ruleMode: Режим правил
|
||||
globalMode: Глобальный режим
|
||||
directMode: Прямой режим
|
||||
outboundModes: Исходящие режимы
|
||||
rule: Правило
|
||||
direct: Прямой
|
||||
global: Глобальный
|
||||
profiles: Профили
|
||||
proxies: Прокси
|
||||
systemProxy: Системный прокси
|
||||
tunMode: Режим TUN
|
||||
closeAllConnections: Закрыть все соединения
|
||||
lightweightMode: Легкий режим
|
||||
copyEnv: Копировать переменные среды
|
||||
confDir: Каталог конфигурации
|
||||
coreDir: Каталог ядра
|
||||
logsDir: Каталог журналов
|
||||
openDir: Открыть каталог
|
||||
appLog: Журнал приложения
|
||||
coreLog: Журнал ядра
|
||||
restartClash: Перезапустить ядро Clash
|
||||
restartApp: Перезапустить приложение
|
||||
vergeVersion: Версия Verge
|
||||
more: Еще
|
||||
exit: Выход
|
||||
tooltip:
|
||||
systemProxy: Системный прокси
|
||||
tun: TUN
|
||||
profile: Профиль
|
||||
60
crates/clash-verge-i18n/locales/tr.yml
Normal file
60
crates/clash-verge-i18n/locales/tr.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Gösterge Paneli
|
||||
body: Gösterge panelinin görünürlüğü güncellendi.
|
||||
clashModeChanged:
|
||||
title: Mod Değişimi
|
||||
body: "{mode} moduna geçildi."
|
||||
systemProxyToggled:
|
||||
title: Sistem Vekil'i
|
||||
body: Sistem vekil'i durumu güncellendi.
|
||||
tunModeToggled:
|
||||
title: TUN Modu
|
||||
body: TUN modu durumu güncellendi.
|
||||
lightweightModeEntered:
|
||||
title: Hafif Mod
|
||||
body: Hafif moda geçildi.
|
||||
profilesReactivated:
|
||||
title: Profiller
|
||||
body: Profil yeniden etkinleştirildi.
|
||||
appQuit:
|
||||
title: Çıkış Yapılmak Üzere
|
||||
body: Clash Verge kapanmak üzere.
|
||||
appHidden:
|
||||
title: Uygulama Gizlendi
|
||||
body: Clash Verge arka planda çalışıyor.
|
||||
service:
|
||||
adminInstallPrompt: Clash Verge hizmetini kurmak için yönetici ayrıcalıkları gerekir.
|
||||
adminUninstallPrompt: Clash Verge hizmetini kaldırmak için yönetici ayrıcalıkları gerekir.
|
||||
tray:
|
||||
dashboard: Gösterge Paneli
|
||||
ruleMode: Kural Modu
|
||||
globalMode: Küresel Mod
|
||||
directMode: Doğrudan Mod
|
||||
outboundModes: Giden Modlar
|
||||
rule: Kural
|
||||
direct: Doğrudan
|
||||
global: Küresel
|
||||
profiles: Profiller
|
||||
proxies: Vekil'ler
|
||||
systemProxy: Sistem Vekil'i
|
||||
tunMode: TUN Modu
|
||||
closeAllConnections: Tüm Bağlantıları Kapat
|
||||
lightweightMode: Hafif Mod
|
||||
copyEnv: Ortam Değişkenlerini Kopyala
|
||||
confDir: Yapılandırma Dizini
|
||||
coreDir: Çekirdek Dizini
|
||||
logsDir: Günlük Dizini
|
||||
openDir: Dizini Aç
|
||||
appLog: Uygulama Günlüğü
|
||||
coreLog: Çekirdek Günlüğü
|
||||
restartClash: Clash Çekirdeğini Yeniden Başlat
|
||||
restartApp: Uygulamayı Yeniden Başlat
|
||||
vergeVersion: Verge Sürümü
|
||||
more: Daha Fazla
|
||||
exit: Çıkış
|
||||
tooltip:
|
||||
systemProxy: Sistem Vekil'i
|
||||
tun: TUN
|
||||
profile: Profil
|
||||
60
crates/clash-verge-i18n/locales/tt.yml
Normal file
60
crates/clash-verge-i18n/locales/tt.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Идарә панеле
|
||||
body: Идарә панеленең күренеше яңартылды.
|
||||
clashModeChanged:
|
||||
title: Режим алыштыру
|
||||
body: "{mode} режимына күчтел."
|
||||
systemProxyToggled:
|
||||
title: Системалы прокси
|
||||
body: Системалы прокси хәле яңартылды.
|
||||
tunModeToggled:
|
||||
title: TUN режимы
|
||||
body: TUN режимы хәле яңартылды.
|
||||
lightweightModeEntered:
|
||||
title: Җиңел режим
|
||||
body: Җиңел режимга күчелде.
|
||||
profilesReactivated:
|
||||
title: Профильләр
|
||||
body: Профиль яңадан активлаштырылды.
|
||||
appQuit:
|
||||
title: Чыгар алдыннан
|
||||
body: Clash Verge чыгарга җыена.
|
||||
appHidden:
|
||||
title: Кушымта яшерелде
|
||||
body: Clash Verge фон режимында эшли.
|
||||
service:
|
||||
adminInstallPrompt: Clash Verge хезмәтен урнаштыру өчен администратор хокуклары кирәк.
|
||||
adminUninstallPrompt: Clash Verge хезмәтен бетерү өчен администратор хокуклары кирәк.
|
||||
tray:
|
||||
dashboard: Идарә панеле
|
||||
ruleMode: Кагыйдә режимы
|
||||
globalMode: Глобаль режим
|
||||
directMode: Турыдан-туры режим
|
||||
outboundModes: Чыгыш режимнары
|
||||
rule: Кагыйдә
|
||||
direct: Турыдан-туры
|
||||
global: Глобаль
|
||||
profiles: Профильләр
|
||||
proxies: Проксилар
|
||||
systemProxy: Системалы прокси
|
||||
tunMode: TUN режимы
|
||||
closeAllConnections: Барлык тоташуларны ябу
|
||||
lightweightMode: Җиңел режим
|
||||
copyEnv: Мохит үзгәрүчәннәрен күчерү
|
||||
confDir: Конфигурация каталогы
|
||||
coreDir: Ядро каталогы
|
||||
logsDir: Журнал каталогы
|
||||
openDir: Каталогны ачу
|
||||
appLog: Кушымта журналы
|
||||
coreLog: Ядро журналы
|
||||
restartClash: Clash ядрәсен кабат җибәрү
|
||||
restartApp: Кушымтаны кабат җибәрү
|
||||
vergeVersion: Verge версиясе
|
||||
more: Күбрәк
|
||||
exit: Чыгу
|
||||
tooltip:
|
||||
systemProxy: Системалы прокси
|
||||
tun: TUN
|
||||
profile: Профиль
|
||||
@@ -25,8 +25,8 @@ notifications:
|
||||
title: 應用已隱藏
|
||||
body: Clash Verge 正在背景執行。
|
||||
service:
|
||||
adminInstallPrompt: 安裝服務需要管理員權限
|
||||
adminUninstallPrompt: 卸载服務需要管理員權限
|
||||
adminInstallPrompt: 安裝 Clash Verge 服務需要管理員權限
|
||||
adminUninstallPrompt: 卸载 Clash Verge 服務需要管理員權限
|
||||
tray:
|
||||
dashboard: 儀表板
|
||||
ruleMode: 規則模式
|
||||
103
crates/clash-verge-i18n/src/lib.rs
Normal file
103
crates/clash-verge-i18n/src/lib.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use rust_i18n::i18n;
|
||||
|
||||
const DEFAULT_LANGUAGE: &str = "zh";
|
||||
i18n!("locales", fallback = "zh");
|
||||
|
||||
#[inline]
|
||||
fn locale_alias(locale: &str) -> Option<&'static str> {
|
||||
match locale {
|
||||
"ja" | "ja-jp" | "jp" => Some("jp"),
|
||||
"zh" | "zh-cn" | "zh-hans" | "zh-sg" | "zh-my" | "zh-chs" => Some("zh"),
|
||||
"zh-tw" | "zh-hk" | "zh-hant" | "zh-mo" | "zh-cht" => Some("zhtw"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn resolve_supported_language(language: &str) -> Option<&'static str> {
|
||||
if language.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let normalized = language.to_lowercase().replace('_', "-");
|
||||
let segments: Vec<&str> = normalized.split('-').collect();
|
||||
let supported = rust_i18n::available_locales!();
|
||||
for i in (1..=segments.len()).rev() {
|
||||
let prefix = segments[..i].join("-");
|
||||
if let Some(alias) = locale_alias(&prefix)
|
||||
&& let Some(&found) = supported.iter().find(|&&l| l.eq_ignore_ascii_case(alias))
|
||||
{
|
||||
return Some(found);
|
||||
}
|
||||
if let Some(&found) = supported.iter().find(|&&l| l.eq_ignore_ascii_case(&prefix)) {
|
||||
return Some(found);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn current_language(language: Option<&str>) -> &str {
|
||||
language
|
||||
.as_ref()
|
||||
.filter(|lang| !lang.is_empty())
|
||||
.and_then(|lang| resolve_supported_language(lang))
|
||||
.unwrap_or_else(system_language)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn system_language() -> &'static str {
|
||||
sys_locale::get_locale()
|
||||
.as_deref()
|
||||
.and_then(resolve_supported_language)
|
||||
.unwrap_or(DEFAULT_LANGUAGE)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn sync_locale(language: Option<&str>) {
|
||||
let language = current_language(language);
|
||||
set_locale(language);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_locale(language: &str) {
|
||||
let lang = resolve_supported_language(language).unwrap_or(DEFAULT_LANGUAGE);
|
||||
rust_i18n::set_locale(lang);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn translate(key: &str) -> Cow<'_, str> {
|
||||
rust_i18n::t!(key)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! t {
|
||||
($key:expr) => {
|
||||
$crate::translate(&$key)
|
||||
};
|
||||
($key:expr, $($arg_name:ident = $arg_value:expr),*) => {
|
||||
{
|
||||
let mut _text = $crate::translate(&$key);
|
||||
$(
|
||||
_text = _text.replace(&format!("{{{}}}", stringify!($arg_name)), &$arg_value);
|
||||
)*
|
||||
_text
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::resolve_supported_language;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_supported_language() {
|
||||
assert_eq!(resolve_supported_language("en"), Some("en"));
|
||||
assert_eq!(resolve_supported_language("en-US"), Some("en"));
|
||||
assert_eq!(resolve_supported_language("zh"), Some("zh"));
|
||||
assert_eq!(resolve_supported_language("zh-CN"), Some("zh"));
|
||||
assert_eq!(resolve_supported_language("zh-Hant"), Some("zhtw"));
|
||||
assert_eq!(resolve_supported_language("jp"), Some("jp"));
|
||||
assert_eq!(resolve_supported_language("ja-JP"), Some("jp"));
|
||||
assert_eq!(resolve_supported_language("fr"), None);
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,3 @@ flexi_logger = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
tauri-dev = []
|
||||
@@ -1,6 +1,5 @@
|
||||
use compact_str::CompactString;
|
||||
use flexi_logger::DeferredNow;
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
use flexi_logger::filter::LogLineFilter;
|
||||
use flexi_logger::writers::FileLogWriter;
|
||||
use flexi_logger::writers::LogWriter as _;
|
||||
@@ -93,27 +92,19 @@ pub fn write_sidecar_log(
|
||||
) {
|
||||
let args = format_args!("{}", message);
|
||||
|
||||
let record = Record::builder()
|
||||
.args(args)
|
||||
.level(level)
|
||||
.target("sidecar")
|
||||
.build();
|
||||
let record = Record::builder().args(args).level(level).target("sidecar").build();
|
||||
|
||||
let _ = writer.write(now, &record);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
pub struct NoModuleFilter<'a>(pub &'a [&'a str]);
|
||||
pub struct NoModuleFilter<'a>(pub Vec<&'a str>);
|
||||
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
impl<'a> NoModuleFilter<'a> {
|
||||
#[inline]
|
||||
pub fn filter(&self, record: &Record) -> bool {
|
||||
if let Some(module) = record.module_path() {
|
||||
for blocked in self.0 {
|
||||
if module.len() >= blocked.len()
|
||||
&& module.as_bytes()[..blocked.len()] == blocked.as_bytes()[..]
|
||||
{
|
||||
for blocked in self.0.iter() {
|
||||
if module.len() >= blocked.len() && module.as_bytes()[..blocked.len()] == blocked.as_bytes()[..] {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -122,7 +113,6 @@ impl<'a> NoModuleFilter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
impl<'a> LogLineFilter for NoModuleFilter<'a> {
|
||||
#[inline]
|
||||
fn write(
|
||||
|
||||
@@ -17,36 +17,21 @@ where
|
||||
let mut sigterm = match signal(SignalKind::terminate()) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"Failed to register SIGTERM: {}",
|
||||
e
|
||||
);
|
||||
logging!(error, Type::SystemSignal, "Failed to register SIGTERM: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut sigint = match signal(SignalKind::interrupt()) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"Failed to register SIGINT: {}",
|
||||
e
|
||||
);
|
||||
logging!(error, Type::SystemSignal, "Failed to register SIGINT: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut sighup = match signal(SignalKind::hangup()) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"Failed to register SIGHUP: {}",
|
||||
e
|
||||
);
|
||||
logging!(error, Type::SystemSignal, "Failed to register SIGHUP: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,12 +17,7 @@ where
|
||||
let mut ctrl_c = match windows::ctrl_c() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"Failed to register Ctrl+C: {}",
|
||||
e
|
||||
);
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+C: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -30,12 +25,7 @@ where
|
||||
let mut ctrl_close = match windows::ctrl_close() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"Failed to register Ctrl+Close: {}",
|
||||
e
|
||||
);
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Close: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -43,12 +33,7 @@ where
|
||||
let mut ctrl_shutdown = match windows::ctrl_shutdown() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"Failed to register Ctrl+Shutdown: {}",
|
||||
e
|
||||
);
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Shutdown: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -56,12 +41,7 @@ where
|
||||
let mut ctrl_logoff = match windows::ctrl_logoff() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"Failed to register Ctrl+Logoff: {}",
|
||||
e
|
||||
);
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Logoff: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -94,12 +74,7 @@ where
|
||||
}
|
||||
IS_CLEANING_UP.store(true, Ordering::SeqCst);
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::SystemSignal,
|
||||
"Caught Windows signal: {}",
|
||||
signal_name
|
||||
);
|
||||
logging!(info, Type::SystemSignal, "Caught Windows signal: {}", signal_name);
|
||||
|
||||
f().await;
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "clash-verge-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.91"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
smartstring = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
pub mod runtime;
|
||||
@@ -8,10 +8,10 @@ rust-version = "1.91"
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
sysinfo = { version = "0.37.2", features = ["network", "system"] }
|
||||
sysinfo = { version = "0.38.0", features = ["network", "system"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc = "0.2.178"
|
||||
libc = "0.2.180"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
deelevate = { workspace = true }
|
||||
|
||||
@@ -13,13 +13,7 @@ pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<String, Err
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[command]
|
||||
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, Error> {
|
||||
Ok(state
|
||||
.inner()
|
||||
.read()
|
||||
.appinfo
|
||||
.app_startup_time
|
||||
.elapsed()
|
||||
.as_millis())
|
||||
Ok(state.inner().read().appinfo.app_startup_time.elapsed().as_millis())
|
||||
}
|
||||
|
||||
/// 检查应用是否以管理员身份运行
|
||||
|
||||
@@ -7,6 +7,8 @@ pub mod commands;
|
||||
|
||||
#[cfg(windows)]
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
#[cfg(unix)]
|
||||
pub use libc;
|
||||
use parking_lot::RwLock;
|
||||
use sysinfo::{Networks, System};
|
||||
use tauri::{
|
||||
@@ -118,6 +120,12 @@ fn is_binary_admin() -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(unix)]
|
||||
pub fn current_gid() -> u32 {
|
||||
unsafe { libc::getgid() }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn list_network_interfaces() -> Vec<String> {
|
||||
let mut networks = Networks::new();
|
||||
@@ -132,6 +140,13 @@ pub fn set_app_core_mode<R: Runtime>(app: &tauri::AppHandle<R>, mode: impl Into<
|
||||
spec.appinfo.app_core_mode = mode.into();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_app_uptime<R: Runtime>(app: &tauri::AppHandle<R>) -> Instant {
|
||||
let platform_spec = app.state::<RwLock<Platform>>();
|
||||
let spec = platform_spec.read();
|
||||
spec.appinfo.app_startup_time
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_current_app_handle_admin<R: Runtime>(app: &tauri::AppHandle<R>) -> bool {
|
||||
let platform_spec = app.state::<RwLock<Platform>>();
|
||||
|
||||
@@ -5,8 +5,8 @@ Thanks for helping localize Clash Verge Rev. This guide reflects the current arc
|
||||
## Quick workflow
|
||||
|
||||
- Update the language folder under `src/locales/<lang>/`; use `src/locales/en/` as the canonical reference for keys and intent.
|
||||
- Run `pnpm format:i18n` to align structure and `pnpm i18n:types` to refresh generated typings.
|
||||
- If you touch backend copy, edit the matching YAML file in `src-tauri/locales/<lang>.yml`.
|
||||
- Run `pnpm i18n:format` to align structure (frontend JSON + backend YAML) and `pnpm i18n:types` to refresh generated typings.
|
||||
- If you touch backend copy, edit the matching YAML file in `crates/clash-verge-i18n/locales/<lang>.yml`.
|
||||
- Preview UI changes with `pnpm dev` (desktop shell) or `pnpm web:dev` (web only).
|
||||
- Keep PRs focused and add screenshots whenever layout could be affected by text length.
|
||||
|
||||
@@ -33,29 +33,29 @@ src/locales/
|
||||
|
||||
Because backend translations now live in their own directory, you no longer need to run `pnpm prebuild` just to sync locales—the frontend folder is the sole source of truth for web bundles.
|
||||
|
||||
## Tooling for frontend contributors
|
||||
## Tooling for i18n contributors
|
||||
|
||||
- `pnpm format:i18n` → `node scripts/cleanup-unused-i18n.mjs --align --apply`. It aligns key ordering, removes unused entries, and keeps all locales in lock-step with English.
|
||||
- `pnpm node scripts/cleanup-unused-i18n.mjs` (without flags) performs a dry-run audit. Use it to inspect missing or extra keys before committing.
|
||||
- `pnpm i18n:format` → `node scripts/cleanup-unused-i18n.mjs --align --apply`. It aligns key ordering, removes unused entries, and keeps all locales in lock-step with English across both JSON and YAML bundles.
|
||||
- `pnpm i18n:check` performs a dry-run audit of frontend and backend keys. It scans TS/TSX usage plus Rust `t!(...)` calls in `src-tauri/` and `crates/` to spot missing or extra entries.
|
||||
- `pnpm i18n:types` regenerates `src/types/generated/i18n-keys.ts` and `src/types/generated/i18n-resources.ts`, ensuring TypeScript catches invalid key usage.
|
||||
- For dynamic keys that the analyzer cannot statically detect, add explicit references in code or update the script whitelist to avoid false positives.
|
||||
|
||||
## Backend (Tauri) locale bundles
|
||||
|
||||
Native UI strings (tray menu, notifications, dialogs) use `rust-i18n` with YAML bundles stored in `src-tauri/locales/<lang>.yml`. These files are completely independent from the frontend JSON modules.
|
||||
Native UI strings (tray menu, notifications, dialogs) use `rust-i18n` with YAML bundles stored in `crates/clash-verge-i18n/locales/<lang>.yml`. These files are completely independent from the frontend JSON modules.
|
||||
|
||||
- Keep `en.yml` semantically aligned with the Simplified Chinese baseline (`zh.yml`). Other locales may temporarily copy English if no translation is available yet.
|
||||
- When a backend feature introduces new strings, update every YAML file to keep the key set consistent. Missing keys fall back to the default language (`zh`), so catching gaps early avoids mixed-language output.
|
||||
- Rust code resolves the active language through `src-tauri/src/utils/i18n.rs`. No additional build step is required after editing YAML files; `tauri dev` and `tauri build` pick them up automatically.
|
||||
- The same `pnpm i18n:check` / `pnpm i18n:format` tooling now validates backend YAML keys against Rust usage, so run it after backend i18n edits.
|
||||
- Rust code resolves the active language through the `clash-verge-i18n` crate (`crates/clash-verge-i18n/src/lib.rs`). No additional build step is required after editing YAML files; `tauri dev` and `tauri build` pick them up automatically.
|
||||
|
||||
## Adding a new language
|
||||
|
||||
1. Duplicate `src/locales/en/` into `src/locales/<new-lang>/` and translate the JSON files while preserving key structure.
|
||||
2. Update the locale’s `index.ts` to import every namespace. Matching the English file is the easiest way to avoid missing exports.
|
||||
3. Append the language code to `supportedLanguages` in `src/services/i18n.ts`.
|
||||
4. If the backend should expose the language, create `src-tauri/locales/<new-lang>.yml` and translate the keys used in existing YAML files.
|
||||
5. Adjust `crowdin.yml` if the locale requires a special mapping for Crowdin.
|
||||
6. Run `pnpm format:i18n`, `pnpm i18n:types`, and (optionally) `pnpm node scripts/cleanup-unused-i18n.mjs` in dry-run mode to confirm structure.
|
||||
4. If the backend should expose the language, create `crates/clash-verge-i18n/<new-lang>.yml` and translate the keys used in existing YAML files.
|
||||
5. Run `pnpm i18n:format`, `pnpm i18n:types`, and (optionally) `pnpm i18n:check` in dry-run mode to confirm structure.
|
||||
|
||||
## Authoring guidelines
|
||||
|
||||
|
||||
@@ -1,3 +1,354 @@
|
||||
## v2.4.5
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.19**
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复 macOS 有线网络 DNS 劫持失败
|
||||
- 修复 Monaco 编辑器内右键菜单显示异常
|
||||
- 修复设置代理端口时检查端口占用
|
||||
- 修复 Monaco 编辑器初始化卡 Loading
|
||||
- 修复恢复备份时 `config.yaml` / `profiles.yaml` 文件内字段未正确恢复
|
||||
- 修复 Windows 下系统主题同步问题
|
||||
- 修复 URL Schemes 无法正常导入
|
||||
- 修复 Linux 下无法安装 TUN 服务
|
||||
- 修复可能的端口被占用误报
|
||||
- 修复设置允许外部控制来源不能立即生效
|
||||
- 修复前端性能回归问题
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 允许代理页面允许高级过滤搜索
|
||||
- 备份设置页面新增导入备份按钮
|
||||
- 允许修改通知弹窗位置
|
||||
- 支持收起导航栏(导航栏右键菜单 / 界面设置)
|
||||
- 允许将出站模式显示在托盘一级菜单
|
||||
- 允许禁用在托盘中显示代理组
|
||||
- 支持在「编辑节点」中直接导入 AnyTLS URI 配置
|
||||
- 支持关闭「验证代理绕过格式」
|
||||
- 新增系统代理绕过和 TUN 排除自定义网段的可视化编辑器
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 应用内更新日志支持解析并渲染 HTML 标签
|
||||
- 性能优化前后端在渲染流量图时的资源
|
||||
- 在 Linux NVIDIA 显卡环境下尝试禁用 WebKit DMABUF 渲染以规避潜在问题
|
||||
- Windows 下自启动改为计划任务实现
|
||||
- 改进托盘和窗口操作频率限制实现
|
||||
- 使用「编辑节点」添加节点时,自动将节点添加到第一个 `select` 类型的代理组的第一位
|
||||
- 隐藏侧边导航栏和悬浮跳转导航的滚动条
|
||||
- 完善对 AnyTLS / Mieru / Sudoku 的 GUI 支持
|
||||
- macOS 和 Linux 对服务 IPC 权限进一步限制
|
||||
- 移除 Windows 自启动计划任务中冗余的 3 秒延时
|
||||
- 右键错误通知可复制错误详情
|
||||
- 保存 TUN 设置时优化执行流程,避免界面卡顿
|
||||
- 补充 `deb` / `rpm` 依赖 `libayatana-appindicator`
|
||||
- 「连接」表格标题的排序点击区域扩展到整列宽度
|
||||
- 备份恢复时显示加载覆盖层,恢复过程无需再手动关闭对话框
|
||||
|
||||
</details>
|
||||
|
||||
## v2.4.4
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.17**
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- Linux 无法切换 TUN 堆栈
|
||||
- macOS service 启动项显示名称(试验性修改)
|
||||
- macOS 非预期 Tproxy 端口设置
|
||||
- 流量图缩放异常
|
||||
- PAC 自动代理脚本内容无法动态调整
|
||||
- 兼容从旧版服务模式升级
|
||||
- Monaco 编辑器的行数上限
|
||||
- 已删除节点在手动分组中导致配置无法加载
|
||||
- 仪表盘与托盘状态不同步
|
||||
- 彻底修复 macOS 连接页面显示异常
|
||||
- windows 端监听关机信号失败
|
||||
- 修复代理按钮和高亮状态不同步
|
||||
- 修复侧边栏可能的未能正确跳转
|
||||
- 修复解锁测试部分地区图标编码不正确
|
||||
- 修复 IP 检测切页后强制刷新,改为仅在必要时更新
|
||||
- 修复在搜索框输入不完整正则直接崩溃
|
||||
- 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁
|
||||
- 修复更新时加载进度条异常
|
||||
- 升级内核失败导致内核不可用问题
|
||||
- 修复 macOS 在安装和卸载服务时提示与操作不匹配
|
||||
- 修复菜单排序模式拖拽异常
|
||||
- 修复托盘菜单代理组前的异常勾选状态
|
||||
- 修复 Windows 下自定义标题栏按钮在最小化 / 关闭后 hover 状态残留
|
||||
- 修复直接覆盖 `config.yaml` 使用时无法展开代理组
|
||||
- 修复 macOS 下应用启动时系统托盘图标颜色闪烁
|
||||
- 修复应用静默启动模式下非全局热键一直抢占其他应用按键问题
|
||||
- 修复首页当前节点卡片按延迟排序时,打开节点列表后,`timeout` 节点被排在正常节点前的问题
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 支持连接页面各个项目的排序
|
||||
- 实现可选的自动备份
|
||||
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
|
||||
- 日志页面支持按时间倒序
|
||||
- 增加「重新激活订阅」的全局快捷键
|
||||
- WebView2 Runtime 修复构建升级到 133.0.3065.92
|
||||
- 侧边栏右键新增「恢复默认排序」
|
||||
- Linux 下新增对 TUN 「自动重定向」(`auto-redirect` 字段)的配置支持,默认关闭
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 网络请求改为使用 rustls,提升 TLS 兼容性
|
||||
- rustls 避免因服务器证书链配置问题或较新 TLS 要求导致订阅无法导入
|
||||
- 替换前端信息编辑组件,提供更好性能
|
||||
- 优化后端内存和性能表现
|
||||
- 防止退出时可能的禁用 TUN 失败
|
||||
- 全新 i18n 支持方式
|
||||
- 优化备份设置布局
|
||||
- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停
|
||||
- 性能优化系统状态获取
|
||||
- 优化托盘菜单当前订阅检测逻辑
|
||||
- 优化连接页面表格渲染
|
||||
- 优化链式代理 UI 反馈
|
||||
- 优化重启应用的资源清理逻辑
|
||||
- 优化前端数据刷新
|
||||
- 优化流量采样和数据处理
|
||||
- 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间
|
||||
- 优化前端 WebSocket 连接机制
|
||||
- 改进旧版 Service 需要重新安装检测流程
|
||||
- 优化 macOS, Linux 和 Windows 系统信号处理
|
||||
- 链式代理仅显示 Selector 类型规则组
|
||||
- 优化 Windows 系统代理设置,不再依赖 `sysproxy.exe` 来设置代理
|
||||
|
||||
</details>
|
||||
|
||||
## v2.4.3
|
||||
|
||||
**发行代号:澜**
|
||||
代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。
|
||||
|
||||
特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 优化服务模式重装逻辑,避免不必要的重复检查
|
||||
- 修复轻量模式退出无响应的问题
|
||||
- 修复托盘轻量模式支持退出/进入
|
||||
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
|
||||
- macOS Tun/系统代理 模式下图标大小不统一
|
||||
- 托盘节点切换不再显示隐藏组
|
||||
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
|
||||
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
|
||||
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
|
||||
- 修复 Webdav 恢复备份不重启
|
||||
- 修复 Linux 开机后无法正常代理需要手动设置
|
||||
- 修复增加订阅或导入订阅文件时订阅页面无更新
|
||||
- 修复系统代理守卫功能不工作
|
||||
- 修复 KDE + Wayland 下多屏显示 UI 异常
|
||||
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
|
||||
- 修复静默启动不加载完整 WebView 的问题
|
||||
- 修复 Linux WebKit 网络进程的崩溃
|
||||
- 修复无法导入订阅
|
||||
- 修复实际导入成功但显示导入失败的问题
|
||||
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
|
||||
- 修复删除订阅时未能实际删除相关文件
|
||||
- 修复 macOS 连接界面显示异常
|
||||
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
|
||||
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
|
||||
- 修复自动更新使版本回退的问题
|
||||
- 修复首页自定义卡片在切换轻量模式时失效
|
||||
- 修复悬浮跳转导航失效
|
||||
- 修复小键盘热键映射错误
|
||||
- 修复前端无法及时刷新操作状态
|
||||
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
|
||||
- 修复 Linux 系统主题切换不生效
|
||||
- 修复 `允许自动更新` 字段使手动订阅刷新失效
|
||||
- 修复轻量模式托盘状态不同步
|
||||
- 修复一键导入订阅导致应用卡死崩溃的问题
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.15**
|
||||
- 支持前端修改日志(最大文件大小、最大保留数量)
|
||||
- 新增链式代理图形化设置功能
|
||||
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
|
||||
- 监听关机事件,自动关闭系统代理
|
||||
- 主界面“当前节点”卡片新增“延迟测试”按钮
|
||||
- 新增批量选择配置文件功能
|
||||
- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置
|
||||
- 新增本地备份功能
|
||||
- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭)
|
||||
- 允许独立控制订阅自动更新
|
||||
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
|
||||
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
|
||||
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 重构并简化服务模式启动检测流程,消除重复检测
|
||||
- 重构并简化窗口创建流程
|
||||
- 重构日志系统,单个日志默认最大 10 MB
|
||||
- 优化前端资源占用
|
||||
- 改进 macos 下系统代理设置的方法
|
||||
- 优化 TUN 模式可用性的判断
|
||||
- 移除流媒体检测的系统级提示(使用软件内通知)
|
||||
- 优化后端 i18n 资源占用
|
||||
- 改进 Linux 托盘支持并添加 `--no-tray` 选项
|
||||
- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式
|
||||
- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL
|
||||
- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端
|
||||
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
|
||||
- 修改内核默认日志级别为 Info
|
||||
- 支持通过桌面快捷方式重新打开应用
|
||||
- 支持订阅界面输入链接后回车导入
|
||||
- 选择按延迟排序时每次延迟测试自动刷新节点顺序
|
||||
- 配置重载失败时自动重启核心
|
||||
- 启用 TUN 前等待服务就绪
|
||||
- 卸载 TUN 时会先关闭
|
||||
- 优化应用启动页
|
||||
- 优化首页当前节点对MATCH规则的支持
|
||||
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
|
||||
- 添加热键绑定错误的提示信息
|
||||
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
|
||||
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
|
||||
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
|
||||
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
|
||||
|
||||
</details>
|
||||
|
||||
## v2.4.2
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- 增加托盘节点选择
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 优化前端首页加载速度
|
||||
- 优化前端未使用 i18n 文件缓存
|
||||
- 优化后端内存占用
|
||||
- 优化后端启动速度
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复首页节点切换失效的问题
|
||||
- 修复和优化服务检查流程
|
||||
- 修复2.4.1引入的订阅地址重定向报错问题
|
||||
- 修复 rpm/deb 包名称问题
|
||||
- 修复托盘轻量模式状态检测异常
|
||||
- 修复通过 scheme 导入订阅崩溃
|
||||
- 修复单例检测实效
|
||||
- 修复启动阶段可能导致的无法连接内核
|
||||
- 修复导入订阅无法 Auth Basic
|
||||
|
||||
### 👙 界面样式
|
||||
|
||||
- 简化和改进代理设置样式
|
||||
|
||||
## v2.4.1
|
||||
|
||||
### 🏆 重大改进
|
||||
|
||||
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.13**
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 优化热键响应速度,提升快捷键操作体验
|
||||
- 改进服务管理响应性,减少系统服务操作等待时间
|
||||
- 提升文件和配置处理性能
|
||||
- 优化任务管理和日志记录效率
|
||||
- 优化异步内存管理,减少内存占用并提升多任务处理效率
|
||||
- 优化启动阶段初始化性能
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复应用在某些操作中可能出现的响应延迟问题
|
||||
- 修复任务管理中的潜在并发问题
|
||||
- 修复通过托盘重启应用无法恢复
|
||||
- 修复订阅在某些情况下无法导入
|
||||
- 修复无法新建订阅时使用远程链接
|
||||
- 修复卸载服务后的 tun 开关状态问题
|
||||
- 修复页面快速切换订阅时导致崩溃
|
||||
- 修复丢失工作目录时无法恢复环境
|
||||
- 修复从轻量模式恢复导致崩溃
|
||||
|
||||
### 👙 界面样式
|
||||
|
||||
- 统一代理设置样式
|
||||
|
||||
### 🗑️ 移除内容
|
||||
|
||||
- 移除启动阶段自动清理过期订阅
|
||||
|
||||
## v2.4.0
|
||||
|
||||
**发行代号:融**
|
||||
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
|
||||
|
||||
### 🏆 重大改进
|
||||
|
||||
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
|
||||
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
|
||||
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.12**
|
||||
- 新增版本信息复制按钮
|
||||
- 增强型流量监控,支持更详细的数据分析
|
||||
- 新增流量图表多种显示模式
|
||||
- 新增强制刷新配置和节点缓存功能
|
||||
- 首页流量统计支持查看刻度线详情
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 全面提升数据传输和处理效率
|
||||
- 优化内存使用,减少系统资源消耗
|
||||
- 改进流量图表渲染性能
|
||||
- 优化配置和节点刷新策略,从5秒延长到60秒
|
||||
- 改进数据缓存机制,减少重复请求
|
||||
- 优化异步程序性能
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复系统代理状态检测和显示不一致问题
|
||||
- 修复系统主题窗口颜色不一致问题
|
||||
- 修复特殊字符 URL 处理问题
|
||||
- 修复配置修改后缓存不同步问题
|
||||
- 修复 Windows 安装器自启设置问题
|
||||
- 修复 macOS 下 Dock 图标恢复窗口问题
|
||||
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
|
||||
- 修复架构升级后节点测速功能异常
|
||||
- 修复架构升级后流量统计功能异常
|
||||
- 修复架构升级后日志功能异常
|
||||
- 修复外部控制器跨域配置保存问题
|
||||
- 修复首页端口显示不一致问题
|
||||
- 修复首页流量统计刻度线显示问题
|
||||
- 修复日志页面按钮功能混淆问题
|
||||
- 修复日志等级设置保存问题
|
||||
- 修复日志等级异常过滤
|
||||
- 修复清理日志天数功能异常
|
||||
- 修复偶发性启动卡死问题
|
||||
- 修复首页虚拟网卡开关在管理模式下的状态问题
|
||||
|
||||
### 🔧 技术改进
|
||||
|
||||
- 统一使用新的内核通信方式
|
||||
- 新增外部控制器配置界面
|
||||
- 改进跨平台兼容性支持
|
||||
|
||||
## v2.3.2
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
@@ -69,19 +69,6 @@ Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
|
||||
|
||||
🌐 Official Website: 👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### Build Infrastructure Sponsor — [YXVM Dedicated Servers](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
Our builds and releases run on YXVM dedicated servers that deliver premium resources, strong performance, and high-speed networking. If downloads feel fast and usage feels snappy, it is thanks to robust hardware.
|
||||
|
||||
🧩 Highlights of YXVM Dedicated Servers:
|
||||
|
||||
- 🌎 Optimized global routes for dramatically faster downloads
|
||||
- 🔧 Bare-metal resources instead of shared VPS capacity for maximum performance
|
||||
- 🧠 Great for proxy workloads, hosting web/CDN services, CI/CD pipelines, or any high-load tasks
|
||||
- 💡 Ready to use instantly with multiple datacenter options, including CN2 and IEPL
|
||||
- 📦 The configuration used by this project is on sale—feel free to get the same setup
|
||||
- 🎯 Want the same build environment? [Order a YXVM server today](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## Features
|
||||
|
||||
- Built on high-performance Rust with the Tauri 2 framework
|
||||
|
||||
@@ -63,19 +63,6 @@ Consulta la [documentación del proyecto](https://clash-verge-rev.github.io/) pa
|
||||
- Desbloquea servicios de streaming y acceso a ChatGPT
|
||||
- Sitio oficial: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### Patrocinador de la infraestructura de compilación — [Servidores dedicados YXVM](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
Las compilaciones y lanzamientos del proyecto se ejecutan en servidores dedicados de YXVM, que proporcionan recursos premium, alto rendimiento y redes de alta velocidad. Si las descargas son rápidas y el uso es fluido, es gracias a este hardware robusto.
|
||||
|
||||
🧩 Ventajas de los servidores dedicados YXVM:
|
||||
|
||||
- 🌎 Rutas globales optimizadas para descargas significativamente más rápidas
|
||||
- 🔧 Recursos bare-metal, en lugar de VPS compartidos, para obtener el máximo rendimiento
|
||||
- 🧠 Ideales para proxys, alojamiento de sitios web/CDN, pipelines de CI/CD o cualquier carga elevada
|
||||
- 💡 Listos para usar al instante, con múltiples centros de datos disponibles (incluidos CN2 e IEPL)
|
||||
- 📦 La misma configuración utilizada por este proyecto está disponible para su compra
|
||||
- 🎯 ¿Quieres el mismo entorno de compilación? [Solicita un servidor YXVM hoy](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## Funciones
|
||||
|
||||
- Basado en Rust de alto rendimiento y en el framework Tauri 2
|
||||
|
||||
@@ -62,18 +62,6 @@
|
||||
- پشتیبانی از سرویسهای استریم و دسترسی به ChatGPT
|
||||
- وبسایت رسمی: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### حامی زیرساخت ساخت — [سرورهای اختصاصی YXVM](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
بیلدها و نسخههای ما روی سرورهای اختصاصی YXVM اجرا میشوند که منابع ممتاز، عملکرد قوی و شبکه پرسرعت را ارائه میدهند. اگر دانلودها سریع و استفاده از آن سریع به نظر میرسد، به لطف سختافزار قوی است.
|
||||
🧩 نکات برجسته سرورهای اختصاصی YXVM:
|
||||
|
||||
- 🌎 مسیرهای جهانی بهینه شده برای دانلودهای بسیار سریعتر
|
||||
- 🔧 منابع فیزیکی به جای ظرفیت VPS مشترک برای حداکثر کارایی
|
||||
- 🧠 عالی برای بارهای کاری پروکسی، میزبانی سرویسهای وب/CDN، خطوط لوله CI/CD یا هرگونه کار با بار بالا
|
||||
- 💡 آماده استفاده فوری با گزینههای متعدد مرکز داده، از جمله CN2 و IEPL
|
||||
- 📦 پیکربندی مورد استفاده در این پروژه در حال فروش است - میتوانید همان تنظیمات را تهیه کنید.
|
||||
- 🎯 آیا محیط ساخت مشابهی میخواهید؟ [همین امروز یک سرور YXVM سفارش دهید](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## ویژگیها
|
||||
|
||||
- ساخته شده بر اساس Rust با کارایی بالا و فریمورک Tauri 2
|
||||
|
||||
@@ -63,19 +63,6 @@ Windows (x64/x86)、Linux (x64/arm64)、macOS 10.15+ (Intel/Apple) をサポー
|
||||
- ストリーミングおよび ChatGPT の利用にも対応
|
||||
- 公式サイト: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### ビルド環境スポンサー — [YXVM 専用サーバー](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
本プロジェクトのビルドとリリースは、YXVM の専用サーバーによって支えられています。高速ダウンロードや快適な操作性は、強力なハードウェアがあってこそです。
|
||||
|
||||
🧩 YXVM 専用サーバーの特長:
|
||||
|
||||
- 🌎 最適化されたグローバル回線で圧倒的なダウンロード速度
|
||||
- 🔧 VPS とは異なるベアメタル資源で最高性能を発揮
|
||||
- 🧠 プロキシ運用、Web/CDN ホスティング、CI/CD など高負荷ワークロードに最適
|
||||
- 💡 複数データセンターから即時利用可能。CN2 や IEPL も選択可
|
||||
- 📦 本プロジェクトが使用している構成も販売中。同じ環境を入手できます
|
||||
- 🎯 同じビルド体験をしたい方は [今すぐ YXVM サーバーを注文](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## 機能
|
||||
|
||||
- 高性能な Rust と Tauri 2 フレームワークに基づくデスクトップアプリ
|
||||
|
||||
@@ -63,19 +63,6 @@ Windows (x64/x86), Linux (x64/arm64), macOS 10.15+ (Intel/Apple)을 지원합니
|
||||
- 스트리밍 및 ChatGPT 접근 지원
|
||||
- 공식 사이트: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### 빌드 인프라 스폰서 — [YXVM 전용 서버](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
본 프로젝트의 빌드 및 릴리스는 YXVM 전용 서버에서 구동됩니다. 빠른 다운로드와 경쾌한 사용감은 탄탄한 하드웨어 덕분입니다.
|
||||
|
||||
🧩 YXVM 전용 서버 하이라이트:
|
||||
|
||||
- 🌎 최적화된 글로벌 라우팅으로 대폭 빨라진 다운로드
|
||||
- 🔧 공유 VPS가 아닌 베어메탈 자원으로 최대 성능 제공
|
||||
- 🧠 프록시 워크로드, Web/CDN 호스팅, CI/CD, 고부하 작업에 적합
|
||||
- 💡 CN2 / IEPL 등 다양한 데이터센터 옵션, 즉시 사용 가능
|
||||
- 📦 본 프로젝트가 사용하는 구성도 판매 중 — 동일한 환경을 사용할 수 있습니다
|
||||
- 🎯 동일한 빌드 환경이 필요하다면 [지금 YXVM 서버 주문](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## 기능
|
||||
|
||||
- 고성능 Rust와 Tauri 2 프레임워크 기반 데스크톱 앱
|
||||
|
||||
@@ -59,19 +59,6 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
|
||||
- Разблокировка потоковые сервисы и ChatGPT
|
||||
- Официальный сайт: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### Среда сборки и публикации этого проекта полностью поддерживается выделенным сервером [YXVM](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
Благодарим вас за предоставление надежной бэкэнд-среды с эксклюзивными ресурсами, высокой производительностью и высокоскоростной сетью. Если вы считаете, что загрузка файлов происходит достаточно быстро, а использование — достаточно плавно, то это потому, что мы используем серверы высшего уровня!
|
||||
|
||||
🧩 Преимущества выделенного сервера YXVM:
|
||||
|
||||
- 🌎 Премиум-сеть с оптимизацией обратного пути для молниеносной скорости загрузки
|
||||
- 🔧 Выделенные физические серверные ресурсы, не имеющие аналогов среди VPS, обеспечивающие максимальную производительность
|
||||
- 🧠 Идеально подходит для прокси, хостинга веб-сайтов/CDN-сайтов, рабочих процессов CI/CD или любых приложений с высокой нагрузкой
|
||||
- 💡 Поддержка использования сразу после включения, выбор нескольких дата-центров, CN2 / IEPL на выбор
|
||||
- 📦 Эта конфигурация в настоящее время доступна для покупки — не стесняйтесь заказывать ту же модель!
|
||||
- 🎯 Хотите попробовать такую же сборку? [Закажите выделенный сервер YXVM прямо сейчас!](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## Фичи
|
||||
|
||||
- Основан на произвоительном Rust и фреймворке Tauri 2
|
||||
|
||||
@@ -5,6 +5,7 @@ import configPrettier from "eslint-config-prettier";
|
||||
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
||||
import pluginImportX from "eslint-plugin-import-x";
|
||||
import pluginPrettier from "eslint-plugin-prettier";
|
||||
import pluginReactCompiler from "eslint-plugin-react-compiler";
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import pluginReactRefresh from "eslint-plugin-react-refresh";
|
||||
import pluginUnusedImports from "eslint-plugin-unused-imports";
|
||||
@@ -19,6 +20,7 @@ export default defineConfig([
|
||||
js: eslintJS,
|
||||
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
|
||||
"react-hooks": pluginReactHooks,
|
||||
"react-compiler": pluginReactCompiler,
|
||||
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
||||
"import-x": pluginImportX,
|
||||
"react-refresh": pluginReactRefresh,
|
||||
@@ -52,6 +54,7 @@ export default defineConfig([
|
||||
// React
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"react-compiler/react-compiler": "error",
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
|
||||
65
package.json
65
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.4.4",
|
||||
"version": "2.4.6",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"prepare": "husky || true",
|
||||
@@ -28,7 +28,8 @@
|
||||
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"format:i18n": "node scripts/cleanup-unused-i18n.mjs --align --apply",
|
||||
"i18n:check": "node scripts/cleanup-unused-i18n.mjs",
|
||||
"i18n:format": "node scripts/cleanup-unused-i18n.mjs --align --apply",
|
||||
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
@@ -40,50 +41,51 @@
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^7.3.6",
|
||||
"@mui/icons-material": "^7.3.7",
|
||||
"@mui/lab": "7.0.0-beta.17",
|
||||
"@mui/material": "^7.3.6",
|
||||
"@mui/material": "^7.3.7",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.13",
|
||||
"@tanstack/react-virtual": "^3.13.18",
|
||||
"@tauri-apps/api": "2.9.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-http": "~2.5.4",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||
"@tauri-apps/plugin-http": "~2.5.6",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "2.3.3",
|
||||
"@tauri-apps/plugin-shell": "2.3.4",
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"ahooks": "^3.9.6",
|
||||
"axios": "^1.13.2",
|
||||
"axios": "^1.13.3",
|
||||
"dayjs": "1.11.19",
|
||||
"foxact": "^0.2.49",
|
||||
"i18next": "^25.7.3",
|
||||
"foxact": "^0.2.52",
|
||||
"i18next": "^25.8.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lodash-es": "^4.17.22",
|
||||
"lodash-es": "^4.17.23",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-i18next": "16.5.0",
|
||||
"react-error-boundary": "6.1.0",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-i18next": "16.5.3",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "^7.11.0",
|
||||
"react-virtuoso": "^4.17.0",
|
||||
"react-router": "^7.13.0",
|
||||
"react-virtuoso": "^4.18.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"swr": "^2.3.8",
|
||||
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
|
||||
"types-pac": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@eslint-react/eslint-plugin": "^2.3.13",
|
||||
"@actions/github": "^8.0.0",
|
||||
"@eslint-react/eslint-plugin": "^2.7.4",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@tauri-apps/cli": "2.9.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.10.4",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/react": "19.2.9",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
@@ -95,24 +97,25 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"glob": "^13.0.0",
|
||||
"globals": "^16.5.0",
|
||||
"globals": "^17.1.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.6.1",
|
||||
"lint-staged": "^16.2.7",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.7.4",
|
||||
"sass": "^1.97.0",
|
||||
"tar": "^7.5.2",
|
||||
"terser": "^5.44.1",
|
||||
"prettier": "^3.8.1",
|
||||
"sass": "^1.97.3",
|
||||
"tar": "^7.5.6",
|
||||
"terser": "^5.46.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"vite": "^7.3.0",
|
||||
"typescript-eslint": "^8.53.1",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -125,7 +128,7 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.26.1",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
4054
pnpm-lock.yaml
generated
4054
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
55
renovate.json
Normal file
55
renovate.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"extends": ["config:recommended", ":disableDependencyDashboard"],
|
||||
"baseBranches": ["dev"],
|
||||
"enabledManagers": ["cargo", "npm", "github-actions"],
|
||||
"labels": ["dependencies"],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**",
|
||||
"**/bower_components/**",
|
||||
"**/vendor/**",
|
||||
"**/__tests__/**",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/__fixtures__/**",
|
||||
"shared/**"
|
||||
],
|
||||
"rangeStrategy": "replace",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["*"],
|
||||
"semanticCommitType": "chore"
|
||||
},
|
||||
{
|
||||
"description": "Disable node/pnpm version updates",
|
||||
"matchPackageNames": ["node", "pnpm"],
|
||||
"matchDepTypes": ["engines", "packageManager"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Group all cargo dependencies into a single PR",
|
||||
"matchManagers": ["cargo"],
|
||||
"groupName": "cargo dependencies"
|
||||
},
|
||||
{
|
||||
"description": "Group all npm dependencies into a single PR",
|
||||
"matchManagers": ["npm"],
|
||||
"groupName": "npm dependencies"
|
||||
},
|
||||
{
|
||||
"description": "Group all GitHub Actions updates into a single PR",
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "github actions"
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"ignoreDeps": ["criterion"],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"description": "Force update lockfile to track latest commits of git dependencies",
|
||||
"schedule": ["before 5am on monday"]
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
extends: ["config:recommended", ":disableDependencyDashboard"],
|
||||
baseBranches: ["dev"],
|
||||
enabledManagers: ["cargo", "npm", "github-actions"],
|
||||
labels: ["dependencies"],
|
||||
ignorePaths: [
|
||||
"**/node_modules/**",
|
||||
"**/bower_components/**",
|
||||
"**/vendor/**",
|
||||
"**/__tests__/**",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/__fixtures__/**",
|
||||
"shared/**",
|
||||
],
|
||||
rangeStrategy: "replace",
|
||||
packageRules: [
|
||||
{
|
||||
matchUpdateTypes: ["patch"],
|
||||
automerge: true,
|
||||
},
|
||||
{
|
||||
semanticCommitType: "chore",
|
||||
matchPackageNames: ["*"],
|
||||
},
|
||||
{
|
||||
description: "Disable node/pnpm version updates",
|
||||
matchPackageNames: ["node", "pnpm"],
|
||||
matchDepTypes: ["engines", "packageManager"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
description: "Group all cargo dependencies into a single PR",
|
||||
matchManagers: ["cargo"],
|
||||
groupName: "cargo dependencies",
|
||||
},
|
||||
{
|
||||
description: "Group all npm dependencies into a single PR",
|
||||
matchManagers: ["npm"],
|
||||
groupName: "npm dependencies",
|
||||
},
|
||||
{
|
||||
description: "Group all GitHub Actions updates into a single PR",
|
||||
matchManagers: ["github-actions"],
|
||||
groupName: "github actions",
|
||||
},
|
||||
],
|
||||
postUpdateOptions: ["pnpmDedupe"],
|
||||
ignoreDeps: ["criterion"],
|
||||
}
|
||||
61
scripts-workflow/bump_changelog.sh
Executable file
61
scripts-workflow/bump_changelog.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# bump_changelog.sh
|
||||
# - prepend ./Changelog.md to ./docs/Changelog.history.md
|
||||
# - overwrite ./Changelog.md with ./template/Changelog.md
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
CHANGELOG="Changelog.md"
|
||||
HISTORY="docs/Changelog.history.md"
|
||||
TEMPLATE="template/Changelog.md"
|
||||
|
||||
timestamp() { date +"%Y%m%d%H%M%S"; }
|
||||
|
||||
echo "Repo root: $ROOT_DIR"
|
||||
|
||||
if [ ! -f "$CHANGELOG" ]; then
|
||||
echo "Error: $CHANGELOG not found" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -f "$TEMPLATE" ]; then
|
||||
echo "Error: $TEMPLATE not found" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
BACKUP_DIR=".changelog_backups"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
bak_ts=$(timestamp)
|
||||
cp "$CHANGELOG" "$BACKUP_DIR/Changelog.md.bak.$bak_ts"
|
||||
echo "Backed up $CHANGELOG -> $BACKUP_DIR/Changelog.md.bak.$bak_ts"
|
||||
|
||||
if [ -f "$HISTORY" ]; then
|
||||
cp "$HISTORY" "$BACKUP_DIR/Changelog.history.md.bak.$bak_ts"
|
||||
echo "Backed up $HISTORY -> $BACKUP_DIR/Changelog.history.md.bak.$bak_ts"
|
||||
fi
|
||||
|
||||
# Prepend current Changelog.md content to top of docs/Changelog.history.md
|
||||
tmpfile=$(mktemp)
|
||||
{
|
||||
cat "$CHANGELOG"
|
||||
echo
|
||||
echo ""
|
||||
if [ -f "$HISTORY" ]; then
|
||||
cat "$HISTORY"
|
||||
fi
|
||||
} > "$tmpfile"
|
||||
|
||||
mv "$tmpfile" "$HISTORY"
|
||||
echo "Prepended $CHANGELOG -> $HISTORY"
|
||||
|
||||
# Overwrite Changelog.md with template
|
||||
cp "$TEMPLATE" "$CHANGELOG"
|
||||
echo "Overwrote $CHANGELOG with $TEMPLATE"
|
||||
|
||||
echo "Done. Backups saved under $BACKUP_DIR"
|
||||
|
||||
exit 0
|
||||
@@ -4,18 +4,23 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import ts from "typescript";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
|
||||
const TAURI_LOCALES_DIR = path.resolve(__dirname, "../src-tauri/locales");
|
||||
const DEFAULT_SOURCE_DIRS = [
|
||||
path.resolve(__dirname, "../src"),
|
||||
const FRONTEND_LOCALES_DIR = path.resolve(__dirname, "../src/locales");
|
||||
const BACKEND_LOCALES_DIR = path.resolve(
|
||||
__dirname,
|
||||
"../crates/clash-verge-i18n/locales",
|
||||
);
|
||||
const DEFAULT_FRONTEND_SOURCE_DIRS = [path.resolve(__dirname, "../src")];
|
||||
const DEFAULT_BACKEND_SOURCE_DIRS = [
|
||||
path.resolve(__dirname, "../src-tauri"),
|
||||
path.resolve(__dirname, "../crates"),
|
||||
];
|
||||
const EXCLUDE_USAGE_DIRS = [LOCALES_DIR, TAURI_LOCALES_DIR];
|
||||
const EXCLUDE_USAGE_DIRS = [FRONTEND_LOCALES_DIR, BACKEND_LOCALES_DIR];
|
||||
const DEFAULT_BASELINE_LANG = "en";
|
||||
const IGNORE_DIR_NAMES = new Set([
|
||||
".git",
|
||||
@@ -36,7 +41,7 @@ const IGNORE_DIR_NAMES = new Set([
|
||||
"logs",
|
||||
"__pycache__",
|
||||
]);
|
||||
const SUPPORTED_EXTENSIONS = new Set([
|
||||
const FRONTEND_EXTENSIONS = new Set([
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
@@ -46,6 +51,7 @@ const SUPPORTED_EXTENSIONS = new Set([
|
||||
".vue",
|
||||
".json",
|
||||
]);
|
||||
const BACKEND_EXTENSIONS = new Set([".rs"]);
|
||||
|
||||
const TS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
||||
|
||||
@@ -86,20 +92,25 @@ const WHITELIST_KEYS = new Set([
|
||||
"theme.light",
|
||||
"theme.dark",
|
||||
"theme.system",
|
||||
"Already Using Latest Core Version",
|
||||
"_version",
|
||||
]);
|
||||
|
||||
const MAX_PREVIEW_ENTRIES = 40;
|
||||
const dynamicKeyCache = new Map();
|
||||
const fileUsageCache = new Map();
|
||||
|
||||
function resetUsageCaches() {
|
||||
dynamicKeyCache.clear();
|
||||
fileUsageCache.clear();
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`Usage: pnpm node scripts/cleanup-unused-i18n.mjs [options]
|
||||
|
||||
Options:
|
||||
--apply Write locale files with unused keys removed (default: report only)
|
||||
--align Align locale structure/order using the baseline locale
|
||||
--baseline <lang> Baseline locale file name (default: ${DEFAULT_BASELINE_LANG})
|
||||
--baseline <lang> Baseline locale file name for frontend/backend (default: ${DEFAULT_BASELINE_LANG})
|
||||
--keep-extra Preserve keys that exist only in non-baseline locales when aligning
|
||||
--no-backup Skip creating \`.bak\` backups when applying changes
|
||||
--report <path> Write a JSON report to the given path
|
||||
@@ -148,7 +159,7 @@ function parseArgs(argv) {
|
||||
if (!next) {
|
||||
throw new Error("--baseline requires a locale name (e.g. en)");
|
||||
}
|
||||
options.baseline = next.replace(/\.json$/, "");
|
||||
options.baseline = next.replace(/\.(json|ya?ml)$/i, "");
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
@@ -211,14 +222,16 @@ function getAllFiles(start, predicate) {
|
||||
return files;
|
||||
}
|
||||
|
||||
function collectSourceFiles(sourceDirs) {
|
||||
function collectSourceFiles(sourceDirs, options = {}) {
|
||||
const supportedExtensions =
|
||||
options.supportedExtensions ?? FRONTEND_EXTENSIONS;
|
||||
const seen = new Set();
|
||||
const files = [];
|
||||
|
||||
for (const dir of sourceDirs) {
|
||||
const resolved = getAllFiles(dir, (filePath) => {
|
||||
if (seen.has(filePath)) return false;
|
||||
if (!SUPPORTED_EXTENSIONS.has(path.extname(filePath))) return false;
|
||||
if (!supportedExtensions.has(path.extname(filePath))) return false;
|
||||
if (
|
||||
EXCLUDE_USAGE_DIRS.some((excluded) =>
|
||||
filePath.startsWith(`${excluded}${path.sep}`),
|
||||
@@ -673,6 +686,45 @@ function collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys) {
|
||||
}
|
||||
}
|
||||
|
||||
function readRustStringLiteral(source, startIndex) {
|
||||
const slice = source.slice(startIndex);
|
||||
if (slice.startsWith('"')) {
|
||||
const match = slice.match(/^"(?:\\.|[^"\\])*"/);
|
||||
if (!match) return null;
|
||||
return match[0].slice(1, -1);
|
||||
}
|
||||
if (slice.startsWith("r")) {
|
||||
const match = slice.match(/^r(#+)?"([\s\S]*?)"\1/);
|
||||
if (!match) return null;
|
||||
return match[2];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectUsedKeysFromRustFile(
|
||||
file,
|
||||
baselineNamespaces,
|
||||
usedKeys,
|
||||
_dynamicPrefixes,
|
||||
) {
|
||||
const pattern = /\b(?:[A-Za-z_][\w:]*::)?t!\s*\(/g;
|
||||
let match;
|
||||
while ((match = pattern.exec(file.content))) {
|
||||
let index = match.index + match[0].length;
|
||||
while (index < file.content.length && /\s/.test(file.content[index])) {
|
||||
index += 1;
|
||||
}
|
||||
const key = readRustStringLiteral(file.content, index);
|
||||
if (key) {
|
||||
addKeyIfValid(key, usedKeys, baselineNamespaces, {
|
||||
forceNamespace: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys);
|
||||
}
|
||||
|
||||
function collectUsedI18nKeys(sourceFiles, baselineNamespaces) {
|
||||
const usedKeys = new Set();
|
||||
const dynamicPrefixes = new Set();
|
||||
@@ -685,6 +737,13 @@ function collectUsedI18nKeys(sourceFiles, baselineNamespaces) {
|
||||
usedKeys,
|
||||
dynamicPrefixes,
|
||||
);
|
||||
} else if (file.extension === ".rs") {
|
||||
collectUsedKeysFromRustFile(
|
||||
file,
|
||||
baselineNamespaces,
|
||||
usedKeys,
|
||||
dynamicPrefixes,
|
||||
);
|
||||
} else {
|
||||
collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys);
|
||||
}
|
||||
@@ -864,12 +923,16 @@ function writeReport(reportPath, data) {
|
||||
fs.writeFileSync(reportPath, `${payload}\n`, "utf8");
|
||||
}
|
||||
|
||||
function loadLocales() {
|
||||
if (!fs.existsSync(LOCALES_DIR)) {
|
||||
throw new Error(`Locales directory not found: ${LOCALES_DIR}`);
|
||||
function isPlainObject(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function loadFrontendLocales() {
|
||||
if (!fs.existsSync(FRONTEND_LOCALES_DIR)) {
|
||||
throw new Error(`Locales directory not found: ${FRONTEND_LOCALES_DIR}`);
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(LOCALES_DIR, { withFileTypes: true });
|
||||
const entries = fs.readdirSync(FRONTEND_LOCALES_DIR, { withFileTypes: true });
|
||||
const locales = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
@@ -879,12 +942,12 @@ function loadLocales() {
|
||||
!entry.name.endsWith(".bak") &&
|
||||
!entry.name.endsWith(".old")
|
||||
) {
|
||||
const localePath = path.join(LOCALES_DIR, entry.name);
|
||||
const localePath = path.join(FRONTEND_LOCALES_DIR, entry.name);
|
||||
const name = path.basename(entry.name, ".json");
|
||||
const raw = fs.readFileSync(localePath, "utf8");
|
||||
locales.push({
|
||||
name,
|
||||
dir: LOCALES_DIR,
|
||||
dir: FRONTEND_LOCALES_DIR,
|
||||
format: "single-file",
|
||||
files: [
|
||||
{
|
||||
@@ -901,7 +964,7 @@ function loadLocales() {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name.startsWith(".")) continue;
|
||||
|
||||
const localeDir = path.join(LOCALES_DIR, entry.name);
|
||||
const localeDir = path.join(FRONTEND_LOCALES_DIR, entry.name);
|
||||
const namespaceEntries = fs
|
||||
.readdirSync(localeDir, { withFileTypes: true })
|
||||
.filter(
|
||||
@@ -942,6 +1005,51 @@ function loadLocales() {
|
||||
return locales;
|
||||
}
|
||||
|
||||
function loadBackendLocales() {
|
||||
if (!fs.existsSync(BACKEND_LOCALES_DIR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(BACKEND_LOCALES_DIR, { withFileTypes: true });
|
||||
const locales = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
if (entry.name.endsWith(".bak") || entry.name.endsWith(".old")) {
|
||||
continue;
|
||||
}
|
||||
if (!/\.(ya?ml)$/i.test(entry.name)) continue;
|
||||
|
||||
const localePath = path.join(BACKEND_LOCALES_DIR, entry.name);
|
||||
const name = entry.name.replace(/\.(ya?ml)$/i, "");
|
||||
const raw = fs.readFileSync(localePath, "utf8");
|
||||
let data = {};
|
||||
try {
|
||||
const parsed = yaml.load(raw);
|
||||
data = isPlainObject(parsed) ? parsed : {};
|
||||
} catch (error) {
|
||||
console.warn(`Warning: failed to parse ${localePath}: ${error.message}`);
|
||||
data = {};
|
||||
}
|
||||
|
||||
locales.push({
|
||||
name,
|
||||
dir: BACKEND_LOCALES_DIR,
|
||||
format: "yaml-file",
|
||||
files: [
|
||||
{
|
||||
namespace: "translation",
|
||||
path: localePath,
|
||||
},
|
||||
],
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
locales.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return locales;
|
||||
}
|
||||
|
||||
function ensureBackup(localePath) {
|
||||
const backupPath = `${localePath}.bak`;
|
||||
if (fs.existsSync(backupPath)) {
|
||||
@@ -1042,6 +1150,15 @@ function writeLocale(locale, data, options) {
|
||||
let success = false;
|
||||
|
||||
try {
|
||||
if (locale.format === "yaml-file") {
|
||||
const target = locale.files[0].path;
|
||||
backupIfNeeded(target, backups, options);
|
||||
const serialized = yaml.dump(data ?? {}, { lineWidth: -1, noRefs: true });
|
||||
fs.writeFileSync(target, `${serialized.trimEnd()}\n`, "utf8");
|
||||
success = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (locale.format === "single-file") {
|
||||
const target = locale.files[0].path;
|
||||
backupIfNeeded(target, backups, options);
|
||||
@@ -1097,6 +1214,8 @@ function processLocale(
|
||||
sourceFiles,
|
||||
missingFromSource,
|
||||
options,
|
||||
groupName,
|
||||
baselineName,
|
||||
) {
|
||||
const data = JSON.parse(JSON.stringify(locale.data));
|
||||
const flattened = flattenLocale(data);
|
||||
@@ -1112,7 +1231,7 @@ function processLocale(
|
||||
}
|
||||
|
||||
const sourceMissing =
|
||||
locale.name === options.baseline
|
||||
locale.name === baselineName
|
||||
? missingFromSource.filter((key) => !flattened.has(key))
|
||||
: [];
|
||||
|
||||
@@ -1165,8 +1284,9 @@ function processLocale(
|
||||
}
|
||||
|
||||
return {
|
||||
group: groupName,
|
||||
locale: locale.name,
|
||||
file: locale.format === "single-file" ? locale.files[0].path : locale.dir,
|
||||
file: locale.format === "multi-file" ? locale.dir : locale.files[0].path,
|
||||
totalKeys: flattened.size,
|
||||
expectedKeys: expectedTotal,
|
||||
unusedKeys: unused,
|
||||
@@ -1178,34 +1298,42 @@ function processLocale(
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const argv = process.argv.slice(2);
|
||||
|
||||
let options;
|
||||
try {
|
||||
options = parseArgs(argv);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
console.log();
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
function summarizeResults(results) {
|
||||
return results.reduce(
|
||||
(totals, result) => {
|
||||
totals.totalUnused += result.unusedKeys.length;
|
||||
totals.totalMissing += result.missingKeys.length;
|
||||
totals.totalExtra += result.extraKeys.length;
|
||||
totals.totalSourceMissing += result.missingSourceKeys.length;
|
||||
return totals;
|
||||
},
|
||||
{
|
||||
totalUnused: 0,
|
||||
totalMissing: 0,
|
||||
totalExtra: 0,
|
||||
totalSourceMissing: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function processLocaleGroup(group, options) {
|
||||
const sourceDirs = [
|
||||
...new Set([...DEFAULT_SOURCE_DIRS, ...options.extraSources]),
|
||||
...new Set([...group.sourceDirs, ...options.extraSources]),
|
||||
];
|
||||
|
||||
console.log("Scanning source directories:");
|
||||
console.log(`\n[${group.label}] Scanning source directories:`);
|
||||
for (const dir of sourceDirs) {
|
||||
console.log(` - ${dir}`);
|
||||
}
|
||||
|
||||
const sourceFiles = collectSourceFiles(sourceDirs);
|
||||
const locales = loadLocales();
|
||||
const sourceFiles = collectSourceFiles(sourceDirs, {
|
||||
supportedExtensions: group.supportedExtensions,
|
||||
});
|
||||
const locales = group.locales;
|
||||
|
||||
if (locales.length === 0) {
|
||||
console.log("No locale files found.");
|
||||
return;
|
||||
console.log(`[${group.label}] No locale files found.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const baselineLocale = locales.find(
|
||||
@@ -1215,7 +1343,7 @@ function main() {
|
||||
if (!baselineLocale) {
|
||||
const available = locales.map((item) => item.name).join(", ");
|
||||
throw new Error(
|
||||
`Baseline locale "${options.baseline}" not found. Available locales: ${available}`,
|
||||
`[${group.label}] Baseline locale "${options.baseline}" not found. Available locales: ${available}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1235,8 +1363,11 @@ function main() {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
console.log(`\nChecking ${locales.length} locale files...\n`);
|
||||
console.log(
|
||||
`\n[${group.label}] Checking ${locales.length} locale files...\n`,
|
||||
);
|
||||
|
||||
resetUsageCaches();
|
||||
const results = locales.map((locale) =>
|
||||
processLocale(
|
||||
locale,
|
||||
@@ -1246,35 +1377,85 @@ function main() {
|
||||
sourceFiles,
|
||||
missingFromSource,
|
||||
options,
|
||||
group.label,
|
||||
baselineLocale.name,
|
||||
),
|
||||
);
|
||||
|
||||
const totalUnused = results.reduce(
|
||||
(count, result) => count + result.unusedKeys.length,
|
||||
0,
|
||||
);
|
||||
const totalMissing = results.reduce(
|
||||
(count, result) => count + result.missingKeys.length,
|
||||
0,
|
||||
);
|
||||
const totalExtra = results.reduce(
|
||||
(count, result) => count + result.extraKeys.length,
|
||||
0,
|
||||
);
|
||||
const totalSourceMissing = results.reduce(
|
||||
(count, result) => count + result.missingSourceKeys.length,
|
||||
0,
|
||||
);
|
||||
const totals = summarizeResults(results);
|
||||
|
||||
console.log("\nSummary:");
|
||||
console.log(`\n[${group.label}] Summary:`);
|
||||
for (const result of results) {
|
||||
console.log(
|
||||
` • ${result.locale}: unused=${result.unusedKeys.length}, missing=${result.missingKeys.length}, extra=${result.extraKeys.length}, missingSource=${result.missingSourceKeys.length}, total=${result.totalKeys}, expected=${result.expectedKeys}`,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`\nTotals → unused: ${totalUnused}, missing: ${totalMissing}, extra: ${totalExtra}, missingSource: ${totalSourceMissing}`,
|
||||
`\n[${group.label}] Totals → unused: ${totals.totalUnused}, missing: ${totals.totalMissing}, extra: ${totals.totalExtra}, missingSource: ${totals.totalSourceMissing}`,
|
||||
);
|
||||
|
||||
return {
|
||||
group: group.label,
|
||||
baseline: baselineLocale.name,
|
||||
sourceDirs,
|
||||
totals,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const argv = process.argv.slice(2);
|
||||
|
||||
let options;
|
||||
try {
|
||||
options = parseArgs(argv);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
console.log();
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const localeGroups = [
|
||||
{
|
||||
label: "frontend",
|
||||
locales: loadFrontendLocales(),
|
||||
sourceDirs: DEFAULT_FRONTEND_SOURCE_DIRS,
|
||||
supportedExtensions: FRONTEND_EXTENSIONS,
|
||||
},
|
||||
{
|
||||
label: "backend",
|
||||
locales: loadBackendLocales(),
|
||||
sourceDirs: DEFAULT_BACKEND_SOURCE_DIRS,
|
||||
supportedExtensions: BACKEND_EXTENSIONS,
|
||||
},
|
||||
].filter((group) => group.locales.length > 0);
|
||||
|
||||
if (localeGroups.length === 0) {
|
||||
console.log("No locale files found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const groupReports = [];
|
||||
const allResults = [];
|
||||
|
||||
for (const group of localeGroups) {
|
||||
const report = processLocaleGroup(group, options);
|
||||
if (!report) continue;
|
||||
groupReports.push(report);
|
||||
allResults.push(...report.results);
|
||||
}
|
||||
|
||||
if (groupReports.length > 1) {
|
||||
const overallTotals = summarizeResults(allResults);
|
||||
console.log(
|
||||
`\nOverall totals → unused: ${overallTotals.totalUnused}, missing: ${overallTotals.totalMissing}, extra: ${overallTotals.totalExtra}, missingSource: ${overallTotals.totalSourceMissing}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (allResults.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (options.apply) {
|
||||
console.log(
|
||||
"Files were updated in-place; review diffs before committing changes.",
|
||||
@@ -1301,11 +1482,17 @@ function main() {
|
||||
apply: options.apply,
|
||||
backup: options.backup,
|
||||
align: options.align,
|
||||
baseline: baselineLocale.name,
|
||||
baseline: options.baseline,
|
||||
keepExtra: options.keepExtra,
|
||||
sourceDirs,
|
||||
},
|
||||
results,
|
||||
groups: groupReports.map((report) => ({
|
||||
group: report.group,
|
||||
baseline: report.baseline,
|
||||
sourceDirs: report.sourceDirs,
|
||||
totals: report.totals,
|
||||
locales: report.results.map((result) => result.locale),
|
||||
})),
|
||||
results: allResults,
|
||||
};
|
||||
writeReport(options.reportPath, payload);
|
||||
console.log(`Report written to ${options.reportPath}`);
|
||||
|
||||
@@ -39,9 +39,10 @@ function is_valid_ip() {
|
||||
|
||||
# 获取网络接口和硬件端口
|
||||
nic=$(route -n get default | grep "interface" | awk '{print $2}')
|
||||
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
|
||||
/Hardware Port:/{port=$0; gsub("Hardware Port: ", "", port)}
|
||||
/Device: /{if ($2 == dev) {print port; exit}}
|
||||
# 从网络服务列表中获取硬件端口
|
||||
hardware_port=$(networksetup -listnetworkserviceorder | awk -v dev="$nic" '
|
||||
/^\([0-9]+\) /{port=$0; sub(/^\([0-9]+\) /, "", port)}
|
||||
/\(Hardware Port:/{interface=$NF;sub(/\)/, "", interface); if (interface == dev) {print port; exit}}
|
||||
')
|
||||
|
||||
# 获取当前DNS设置
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
#!/bin/bash
|
||||
nic=$(route -n get default | grep "interface" | awk '{print $2}')
|
||||
|
||||
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
|
||||
/Hardware Port:/{
|
||||
port=$0; gsub("Hardware Port: ", "", port)
|
||||
}
|
||||
/Device: /{
|
||||
if ($2 == dev) {
|
||||
print port;
|
||||
exit
|
||||
}
|
||||
}
|
||||
hardware_port=$(networksetup -listnetworkserviceorder | awk -v dev="$nic" '
|
||||
/^\([0-9]+\) /{port=$0; sub(/^\([0-9]+\) /, "", port)}
|
||||
/\(Hardware Port:/{interface=$NF;sub(/\)/, "", interface); if (interface == dev) {print port; exit}}
|
||||
')
|
||||
|
||||
if [ -f .original_dns.txt ]; then
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.4.4"
|
||||
version = "2.4.6"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
verge-dev = ["clash_verge_logger/color"]
|
||||
tauri-dev = ["clash-verge-logging/tauri-dev"]
|
||||
tauri-dev = []
|
||||
tokio-trace = ["console-subscriber"]
|
||||
clippy = ["tauri/test"]
|
||||
tracing = []
|
||||
@@ -34,7 +34,7 @@ tauri-build = { version = "2.5.3", features = [] }
|
||||
clash-verge-draft = { workspace = true }
|
||||
clash-verge-logging = { workspace = true }
|
||||
clash-verge-signal = { workspace = true }
|
||||
clash-verge-types = { workspace = true }
|
||||
clash-verge-i18n = { workspace = true }
|
||||
tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
tauri = { workspace = true, features = [
|
||||
@@ -54,53 +54,58 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
smartstring = { workspace = true, features = ["serde"] }
|
||||
bitflags = { workspace = true }
|
||||
warp = { version = "0.4.2", features = ["server"] }
|
||||
open = "5.3.3"
|
||||
dunce = "1.0.5"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.42"
|
||||
chrono = "0.4.43"
|
||||
boa_engine = "0.21.0"
|
||||
once_cell = { version = "1.21.3", features = ["parking_lot"] }
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
percent-encoding = "2.3.2"
|
||||
reqwest = { version = "0.12.24", features = ["json", "cookies", "rustls-tls"] }
|
||||
reqwest = { version = "0.13.1", features = [
|
||||
"json",
|
||||
"cookies",
|
||||
"rustls",
|
||||
"form",
|
||||
] }
|
||||
regex = "1.12.2"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "0.4.3", features = [
|
||||
"guard",
|
||||
] }
|
||||
network-interface = { version = "2.0.3", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.3.3"
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-fs = "2.4.4"
|
||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.3.4"
|
||||
tauri-plugin-dialog = "2.6.0"
|
||||
tauri-plugin-fs = "2.4.5"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-deep-link = "2.4.5"
|
||||
tauri-plugin-deep-link = "2.4.6"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
zip = "6.0.0"
|
||||
reqwest_dav = "0.2.2"
|
||||
zip = "7.2.0"
|
||||
reqwest_dav = "0.3.1"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
getrandom = "0.3.4"
|
||||
futures = "0.3.31"
|
||||
sys-locale = "0.3.2"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-stream = "0.1.18"
|
||||
backoff = { version = "0.4.0", features = ["tokio"] }
|
||||
tauri-plugin-http = "2.5.4"
|
||||
tauri-plugin-http = "2.5.6"
|
||||
console-subscriber = { version = "0.5.0", optional = true }
|
||||
tauri-plugin-devtools = { version = "2.0.1" }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
|
||||
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
|
||||
async-trait = "0.1.89"
|
||||
clash_verge_service_ipc = { version = "2.0.26", features = [
|
||||
clash_verge_service_ipc = { version = "2.1.2", features = [
|
||||
"client",
|
||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||
arc-swap = "1.7.1"
|
||||
rust-i18n = "3.1.5"
|
||||
arc-swap = "1.8.0"
|
||||
rust_iso3166 = "0.1.14"
|
||||
dark-light = "2.0.0"
|
||||
# Use the git repo until the next release after v2.0.0.
|
||||
dark-light = { git = "https://github.com/rust-dark-light/dark-light" }
|
||||
governor = "0.10.4"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
deelevate = { workspace = true }
|
||||
@@ -114,10 +119,12 @@ winapi = { version = "0.3.9", features = [
|
||||
"errhandlingapi",
|
||||
"minwindef",
|
||||
"winerror",
|
||||
"stringapiset",
|
||||
"tlhelp32",
|
||||
"processthreadsapi",
|
||||
"winhttp",
|
||||
"winreg",
|
||||
"winnls",
|
||||
] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminPrompt: Installing the service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
rule: Rule
|
||||
direct: Direct
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
lightweightMode: Lightweight Mode
|
||||
copyEnv: Copy Environment Variables
|
||||
confDir: Configuration Directory
|
||||
coreDir: Core Directory
|
||||
logsDir: Log Directory
|
||||
openDir: Open Directory
|
||||
appLog: Application Log
|
||||
coreLog: Core Log
|
||||
restartClash: Restart Clash Core
|
||||
restartApp: Restart Application
|
||||
vergeVersion: Verge Version
|
||||
more: More
|
||||
exit: Exit
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
@@ -1,59 +0,0 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminPrompt: Installing the service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
rule: Regel
|
||||
direct: Direkt
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
lightweightMode: Lightweight Mode
|
||||
copyEnv: Copy Environment Variables
|
||||
confDir: Configuration Directory
|
||||
coreDir: Core Directory
|
||||
logsDir: Log Directory
|
||||
openDir: Open Directory
|
||||
appLog: Application Log
|
||||
coreLog: Core Log
|
||||
restartClash: Restart Clash Core
|
||||
restartApp: Restart Application
|
||||
vergeVersion: Verge Version
|
||||
more: More
|
||||
exit: Exit
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
@@ -1,59 +0,0 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminPrompt: Installing the service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
rule: Regla
|
||||
direct: Directo
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
lightweightMode: Lightweight Mode
|
||||
copyEnv: Copy Environment Variables
|
||||
confDir: Configuration Directory
|
||||
coreDir: Core Directory
|
||||
logsDir: Log Directory
|
||||
openDir: Open Directory
|
||||
appLog: Application Log
|
||||
coreLog: Core Log
|
||||
restartClash: Restart Clash Core
|
||||
restartApp: Restart Application
|
||||
vergeVersion: Verge Version
|
||||
more: More
|
||||
exit: Exit
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
@@ -1,59 +0,0 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminPrompt: Installing the service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
rule: Rule
|
||||
direct: Direct
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
lightweightMode: Lightweight Mode
|
||||
copyEnv: Copy Environment Variables
|
||||
confDir: Configuration Directory
|
||||
coreDir: Core Directory
|
||||
logsDir: Log Directory
|
||||
openDir: Open Directory
|
||||
appLog: Application Log
|
||||
coreLog: Core Log
|
||||
restartClash: Restart Clash Core
|
||||
restartApp: Restart Application
|
||||
vergeVersion: Verge Version
|
||||
more: More
|
||||
exit: Exit
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
@@ -1,59 +0,0 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminPrompt: Installing the service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
rule: Aturan
|
||||
direct: Langsung
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
lightweightMode: Lightweight Mode
|
||||
copyEnv: Copy Environment Variables
|
||||
confDir: Configuration Directory
|
||||
coreDir: Core Directory
|
||||
logsDir: Log Directory
|
||||
openDir: Open Directory
|
||||
appLog: Application Log
|
||||
coreLog: Core Log
|
||||
restartClash: Restart Clash Core
|
||||
restartApp: Restart Application
|
||||
vergeVersion: Verge Version
|
||||
more: More
|
||||
exit: Exit
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
@@ -1,59 +0,0 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminPrompt: Installing the service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
rule: ルール
|
||||
direct: ダイレクト
|
||||
global: グローバル
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
lightweightMode: Lightweight Mode
|
||||
copyEnv: Copy Environment Variables
|
||||
confDir: Configuration Directory
|
||||
coreDir: Core Directory
|
||||
logsDir: Log Directory
|
||||
openDir: Open Directory
|
||||
appLog: Application Log
|
||||
coreLog: Core Log
|
||||
restartClash: Restart Clash Core
|
||||
restartApp: Restart Application
|
||||
vergeVersion: Verge Version
|
||||
more: More
|
||||
exit: Exit
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
@@ -1,59 +0,0 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminPrompt: Installing the service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
rule: Правило
|
||||
direct: Прямой
|
||||
global: Глобальный
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
lightweightMode: Lightweight Mode
|
||||
copyEnv: Copy Environment Variables
|
||||
confDir: Configuration Directory
|
||||
coreDir: Core Directory
|
||||
logsDir: Log Directory
|
||||
openDir: Open Directory
|
||||
appLog: Application Log
|
||||
coreLog: Core Log
|
||||
restartClash: Restart Clash Core
|
||||
restartApp: Restart Application
|
||||
vergeVersion: Verge Version
|
||||
more: More
|
||||
exit: Exit
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
@@ -1,59 +0,0 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminPrompt: Installing the service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
rule: Kural
|
||||
direct: Doğrudan
|
||||
global: Küresel
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
lightweightMode: Lightweight Mode
|
||||
copyEnv: Copy Environment Variables
|
||||
confDir: Configuration Directory
|
||||
coreDir: Core Directory
|
||||
logsDir: Log Directory
|
||||
openDir: Open Directory
|
||||
appLog: Application Log
|
||||
coreLog: Core Log
|
||||
restartClash: Restart Clash Core
|
||||
restartApp: Restart Application
|
||||
vergeVersion: Verge Version
|
||||
more: More
|
||||
exit: Exit
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
@@ -1,59 +0,0 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
service:
|
||||
adminPrompt: Installing the service requires administrator privileges.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
rule: Rule
|
||||
direct: Direct
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
lightweightMode: Lightweight Mode
|
||||
copyEnv: Copy Environment Variables
|
||||
confDir: Configuration Directory
|
||||
coreDir: Core Directory
|
||||
logsDir: Log Directory
|
||||
openDir: Open Directory
|
||||
appLog: Application Log
|
||||
coreLog: Core Log
|
||||
restartClash: Restart Clash Core
|
||||
restartApp: Restart Application
|
||||
vergeVersion: Verge Version
|
||||
more: More
|
||||
exit: Exit
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
@@ -27,6 +27,12 @@ pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
|
||||
feat::restore_local_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
/// Import local backup into the app's backup directory
|
||||
#[tauri::command]
|
||||
pub async fn import_local_backup(source: String) -> CmdResult<String> {
|
||||
feat::import_local_backup(source).await.stringify_err()
|
||||
}
|
||||
|
||||
/// Export local backup to a user selected destination
|
||||
#[tauri::command]
|
||||
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
|
||||
|
||||
@@ -4,6 +4,7 @@ use clash_verge_logging::{Type, logging};
|
||||
use gethostname::gethostname;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use std::net::TcpListener;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tauri_plugin_clash_verge_sysinfo;
|
||||
|
||||
@@ -95,3 +96,8 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_port_in_use(port: u16) -> bool {
|
||||
TcpListener::bind(("127.0.0.1", port)).is_err()
|
||||
}
|
||||
|
||||
@@ -13,10 +13,9 @@ use crate::{
|
||||
feat,
|
||||
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
||||
process::AsyncHandler,
|
||||
ret_err,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use clash_verge_draft::SharedBox;
|
||||
use clash_verge_draft::SharedDraft;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
@@ -26,7 +25,7 @@ use std::time::Duration;
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<SharedBox<IProfiles>> {
|
||||
pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
|
||||
logging!(debug, Type::Cmd, "获取配置文件列表");
|
||||
let draft = Config::profiles().await;
|
||||
let data = draft.data_arc();
|
||||
@@ -455,7 +454,7 @@ pub async fn view_profile(index: String) -> CmdResult {
|
||||
|
||||
let path = dirs::app_profiles_dir().stringify_err()?.join(file.as_str());
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
return CmdResult::Err(format!("file not found \"{}\"", path.display()).into());
|
||||
}
|
||||
|
||||
help::open_file(path).stringify_err()
|
||||
|
||||
@@ -99,7 +99,7 @@ pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option<ser
|
||||
runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
|
||||
// 我们需要在 CoreManager 中验证并应用配置,这里不应该直接调用 runtime.apply()
|
||||
}
|
||||
logging_error!(Type::Core, CoreManager::global().apply_generate_confihg().await);
|
||||
logging_error!(Type::Core, CoreManager::global().apply_generate_config().await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr as _, config::IVerge, feat};
|
||||
use clash_verge_draft::SharedBox;
|
||||
use clash_verge_draft::SharedDraft;
|
||||
|
||||
/// 获取Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
|
||||
pub async fn get_verge_config() -> CmdResult<SharedDraft<IVerge>> {
|
||||
feat::fetch_verge_config().await.stringify_err()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{IClashTemp, IProfiles, IVerge};
|
||||
use crate::{
|
||||
config::{PrfItem, profiles_append_item_safe},
|
||||
config::{PrfItem, profiles_append_item_safe, runtime::IRuntime},
|
||||
constants::{files, timing},
|
||||
core::{
|
||||
CoreManager,
|
||||
@@ -16,7 +16,6 @@ use anyhow::{Result, anyhow};
|
||||
use backoff::{Error as BackoffError, ExponentialBackoff};
|
||||
use clash_verge_draft::Draft;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use clash_verge_types::runtime::IRuntime;
|
||||
use smartstring::alias::String;
|
||||
use std::path::PathBuf;
|
||||
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
||||
@@ -65,6 +64,9 @@ impl Config {
|
||||
pub async fn init_config() -> Result<()> {
|
||||
Self::ensure_default_profile_items().await?;
|
||||
|
||||
let verge = Self::verge().await.latest_arc();
|
||||
clash_verge_i18n::sync_locale(verge.language.as_deref());
|
||||
|
||||
// init Tun mode
|
||||
let handle = Handle::app_handle();
|
||||
let is_admin = is_current_app_handle_admin(handle);
|
||||
@@ -89,6 +91,12 @@ impl Config {
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
}
|
||||
|
||||
{
|
||||
let profiles = Self::profiles().await.data_arc();
|
||||
// Logging error internally
|
||||
let _ = profiles.cleanup_orphaned_files().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -165,11 +173,14 @@ impl Config {
|
||||
};
|
||||
|
||||
let runtime = Self::runtime().await;
|
||||
let runtime_arc = runtime.latest_arc();
|
||||
let config = runtime_arc
|
||||
let runtime_lastest = runtime.latest_arc();
|
||||
// Fall back to committed config if runtime config is missing
|
||||
let runtime_data = runtime.data_arc();
|
||||
let config = runtime_lastest
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("failed to get runtime config"))?;
|
||||
.or_else(|| runtime_data.config.as_ref())
|
||||
.ok_or_else(|| anyhow!("failed to generate runtime config, might need to restart application"))?;
|
||||
|
||||
help::save_yaml(&path, config, Some("# Generated by Clash Verge")).await?;
|
||||
Ok(path)
|
||||
|
||||
@@ -4,6 +4,7 @@ mod config;
|
||||
mod encrypt;
|
||||
mod prfitem;
|
||||
pub mod profiles;
|
||||
pub mod runtime;
|
||||
mod verge;
|
||||
|
||||
pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, verge::*};
|
||||
|
||||
@@ -8,7 +8,7 @@ use clash_verge_logging::{Type, logging};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
use std::collections::HashSet;
|
||||
use tokio::fs;
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
@@ -31,8 +31,8 @@ pub struct IProfilePreview<'a> {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CleanupResult {
|
||||
pub total_files: usize,
|
||||
pub deleted_files: Vec<String>,
|
||||
pub failed_deletions: Vec<String>,
|
||||
pub deleted_files: usize,
|
||||
pub failed_deletions: usize,
|
||||
}
|
||||
|
||||
macro_rules! patch {
|
||||
@@ -126,15 +126,6 @@ impl IProfiles {
|
||||
bail!("failed to get the profile item \"uid:{}\"", uid_str);
|
||||
}
|
||||
|
||||
pub fn get_item_arc(&self, uid: &str) -> Option<Arc<PrfItem>> {
|
||||
self.items.as_ref().and_then(|items| {
|
||||
items
|
||||
.iter()
|
||||
.find(|it| it.uid.as_deref() == Some(uid))
|
||||
.map(|it| Arc::new(it.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
/// append new item
|
||||
/// if the file_data is some
|
||||
/// then should save the data to file
|
||||
@@ -374,15 +365,11 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// 以 app 中的 profile 列表为准,删除不再需要的文件
|
||||
pub async fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
|
||||
pub async fn cleanup_orphaned_files(&self) -> Result<()> {
|
||||
let profiles_dir = dirs::app_profiles_dir()?;
|
||||
|
||||
if !profiles_dir.exists() {
|
||||
return Ok(CleanupResult {
|
||||
total_files: 0,
|
||||
deleted_files: vec![],
|
||||
failed_deletions: vec![],
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 获取所有 active profile 的文件名集合
|
||||
@@ -393,11 +380,11 @@ impl IProfiles {
|
||||
|
||||
// 扫描 profiles 目录下的所有文件
|
||||
let mut total_files = 0;
|
||||
let mut deleted_files = vec![];
|
||||
let mut failed_deletions = vec![];
|
||||
let mut deleted_files = 0;
|
||||
let mut failed_deletions = 0;
|
||||
|
||||
for entry in std::fs::read_dir(&profiles_dir)? {
|
||||
let entry = entry?;
|
||||
let mut dir_entries = tokio::fs::read_dir(&profiles_dir).await?;
|
||||
while let Some(entry) = dir_entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_file() {
|
||||
@@ -419,11 +406,11 @@ impl IProfiles {
|
||||
if !active_files.contains(file_name) {
|
||||
match path.to_path_buf().remove_if_exists().await {
|
||||
Ok(_) => {
|
||||
deleted_files.push(file_name.into());
|
||||
logging!(info, Type::Config, "已清理冗余文件: {file_name}");
|
||||
deleted_files += 1;
|
||||
logging!(debug, Type::Config, "已清理冗余文件: {file_name}");
|
||||
}
|
||||
Err(e) => {
|
||||
failed_deletions.push(format!("{file_name}: {e}").into());
|
||||
failed_deletions += 1;
|
||||
logging!(warn, Type::Config, "Warning: 清理文件失败: {file_name} - {e}");
|
||||
}
|
||||
}
|
||||
@@ -442,11 +429,11 @@ impl IProfiles {
|
||||
Type::Config,
|
||||
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
|
||||
result.total_files,
|
||||
result.deleted_files.len(),
|
||||
result.failed_deletions.len()
|
||||
result.deleted_files,
|
||||
result.failed_deletions
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 不删除全局扩展配置
|
||||
|
||||
@@ -2,6 +2,8 @@ use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::enhance::field::use_keys;
|
||||
|
||||
const PATCH_CONFIG_INNER: [&str; 4] = ["allow-lan", "ipv6", "log-level", "unified-delay"];
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
@@ -125,9 +127,8 @@ impl IRuntime {
|
||||
&& let Some(Value::Sequence(proxies)) = config.get_mut("proxies")
|
||||
{
|
||||
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
|
||||
if let Some(Value::Mapping(proxy)) = proxies
|
||||
.iter_mut()
|
||||
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
|
||||
if let Some(Value::Mapping(proxy)) =
|
||||
proxies.iter_mut().find(|proxy| proxy.get("name") == Some(dialer_proxy))
|
||||
&& i != 0
|
||||
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
|
||||
{
|
||||
@@ -137,16 +138,3 @@ impl IRuntime {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO 完整迁移 enhance 行为后移除
|
||||
#[inline]
|
||||
fn use_keys<'a>(config: &'a Mapping) -> impl Iterator<Item = String> + 'a {
|
||||
config
|
||||
.iter()
|
||||
.filter_map(|(key, _)| key.as_str())
|
||||
.map(|s: &str| {
|
||||
let mut s: String = s.into();
|
||||
s.make_ascii_lowercase();
|
||||
s
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
|
||||
utils::{dirs, help, i18n},
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
@@ -66,6 +66,13 @@ pub struct IVerge {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub menu_order: Option<Vec<String>>,
|
||||
|
||||
/// toast / notice position on screen
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub notice_position: Option<String>,
|
||||
|
||||
/// collapse navigation bar
|
||||
pub collapse_navbar: Option<bool>,
|
||||
|
||||
/// sysproxy tray icon
|
||||
pub sysproxy_tray_icon: Option<bool>,
|
||||
|
||||
@@ -87,6 +94,9 @@ pub struct IVerge {
|
||||
/// enable proxy guard
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
|
||||
/// enable bypass format check
|
||||
pub enable_bypass_check: Option<bool>,
|
||||
|
||||
/// enable dns settings - this controls whether dns_config.yaml is applied
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
|
||||
@@ -145,6 +155,9 @@ pub struct IVerge {
|
||||
/// 是否自动检测当前节点延迟
|
||||
pub enable_auto_delay_detection: Option<bool>,
|
||||
|
||||
/// 自动检测当前节点延迟的间隔(分钟)
|
||||
pub auto_delay_detection_interval_minutes: Option<u64>,
|
||||
|
||||
/// 是否使用内部的脚本支持,默认为真
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
|
||||
@@ -222,7 +235,10 @@ pub struct IVerge {
|
||||
|
||||
// pub enable_tray_icon: Option<bool>,
|
||||
/// show proxy groups directly on tray root menu
|
||||
pub tray_inline_proxy_groups: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_proxy_groups_display_mode: Option<String>,
|
||||
/// show outbound modes directly on tray root menu
|
||||
pub tray_inline_outbound_modes: Option<bool>,
|
||||
|
||||
/// 自动进入轻量模式
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
@@ -336,19 +352,6 @@ impl IVerge {
|
||||
self.clash_core.clone().unwrap_or_else(|| "verge-mihomo".into())
|
||||
}
|
||||
|
||||
fn get_system_language() -> String {
|
||||
let sys_lang = sys_locale::get_locale().unwrap_or_else(|| "en".into()).to_lowercase();
|
||||
|
||||
let lang_code = sys_lang.split(['_', '-']).next().unwrap_or("en");
|
||||
let supported_languages = i18n::get_supported_languages();
|
||||
|
||||
if supported_languages.contains(&lang_code.into()) {
|
||||
lang_code.into()
|
||||
} else {
|
||||
String::from("en")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new() -> Self {
|
||||
match dirs::verge_path() {
|
||||
Ok(path) => match help::read_yaml::<Self>(&path).await {
|
||||
@@ -378,7 +381,7 @@ impl IVerge {
|
||||
app_log_max_size: Some(128),
|
||||
app_log_max_count: Some(8),
|
||||
clash_core: Some("verge-mihomo".into()),
|
||||
language: Some(Self::get_system_language()),
|
||||
language: Some(clash_verge_i18n::system_language().into()),
|
||||
theme_mode: Some("system".into()),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
env_type: Some("bash".into()),
|
||||
@@ -391,6 +394,8 @@ impl IVerge {
|
||||
#[cfg(target_os = "macos")]
|
||||
tray_icon: Some("monochrome".into()),
|
||||
menu_icon: Some("monochrome".into()),
|
||||
notice_position: Some("top-right".into()),
|
||||
collapse_navbar: Some(false),
|
||||
common_tray_icon: Some(false),
|
||||
sysproxy_tray_icon: Some(false),
|
||||
tun_tray_icon: Some(false),
|
||||
@@ -416,6 +421,7 @@ impl IVerge {
|
||||
verge_port: Some(7899),
|
||||
verge_http_enabled: Some(false),
|
||||
enable_proxy_guard: Some(false),
|
||||
enable_bypass_check: Some(true),
|
||||
use_default_bypass: Some(true),
|
||||
proxy_guard_duration: Some(30),
|
||||
auto_close_connection: Some(true),
|
||||
@@ -430,7 +436,8 @@ impl IVerge {
|
||||
webdav_password: None,
|
||||
enable_tray_speed: Some(false),
|
||||
// enable_tray_icon: Some(true),
|
||||
tray_inline_proxy_groups: Some(true),
|
||||
tray_proxy_groups_display_mode: Some("default".into()),
|
||||
tray_inline_outbound_modes: Some(false),
|
||||
enable_global_hotkey: Some(true),
|
||||
enable_auto_light_weight_mode: Some(false),
|
||||
auto_light_weight_minutes: Some(10),
|
||||
@@ -475,6 +482,8 @@ impl IVerge {
|
||||
patch!(tray_icon);
|
||||
patch!(menu_icon);
|
||||
patch!(menu_order);
|
||||
patch!(notice_position);
|
||||
patch!(collapse_navbar);
|
||||
patch!(common_tray_icon);
|
||||
patch!(sysproxy_tray_icon);
|
||||
patch!(tun_tray_icon);
|
||||
@@ -499,6 +508,7 @@ impl IVerge {
|
||||
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);
|
||||
@@ -516,6 +526,7 @@ impl IVerge {
|
||||
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);
|
||||
@@ -529,7 +540,8 @@ impl IVerge {
|
||||
patch!(webdav_password);
|
||||
patch!(enable_tray_speed);
|
||||
// patch!(enable_tray_icon);
|
||||
patch!(tray_inline_proxy_groups);
|
||||
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);
|
||||
|
||||
@@ -25,7 +25,6 @@ pub mod timing {
|
||||
pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(300);
|
||||
pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20);
|
||||
pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2);
|
||||
pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const SERVICE_WAIT_MAX: Duration = Duration::from_millis(3000);
|
||||
@@ -33,10 +32,6 @@ pub mod timing {
|
||||
pub const SERVICE_WAIT_INTERVAL: Duration = Duration::from_millis(200);
|
||||
}
|
||||
|
||||
pub mod retry {
|
||||
pub const EVENT_EMIT_THRESHOLD: u64 = 10;
|
||||
}
|
||||
|
||||
pub mod files {
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
|
||||
@@ -52,16 +52,16 @@ impl Operation {
|
||||
}
|
||||
|
||||
pub struct WebDavClient {
|
||||
config: Arc<ArcSwapOption<WebDavConfig>>,
|
||||
clients: Arc<ArcSwap<HashMap<Operation, reqwest_dav::Client>>>,
|
||||
config: ArcSwapOption<WebDavConfig>,
|
||||
clients: ArcSwap<HashMap<Operation, reqwest_dav::Client>>,
|
||||
}
|
||||
|
||||
impl WebDavClient {
|
||||
pub fn global() -> &'static Self {
|
||||
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
||||
WEBDAV_CLIENT.get_or_init(|| Self {
|
||||
config: Arc::new(ArcSwapOption::new(None)),
|
||||
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
|
||||
config: ArcSwapOption::new(None),
|
||||
clients: ArcSwap::new(Arc::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,11 +146,12 @@ impl WebDavClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存客户端(替换 Arc<Mutex<HashMap<...>>> 的写法)
|
||||
{
|
||||
let mut map = (**self.clients.load()).clone();
|
||||
map.insert(op, client.clone());
|
||||
self.clients.store(map.into());
|
||||
self.clients.rcu(|clients_map| {
|
||||
let mut new_map = (**clients_map).clone();
|
||||
new_map.insert(op, client.clone());
|
||||
Arc::new(new_map)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
use crate::{APP_HANDLE, constants::timing, singleton};
|
||||
use crate::{APP_HANDLE, singleton, utils::window_manager::WindowManager};
|
||||
use parking_lot::RwLock;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread,
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tauri::{AppHandle, Manager as _, WebviewWindow};
|
||||
use tauri_plugin_mihomo::{Mihomo, MihomoExt as _};
|
||||
use tokio::sync::RwLockReadGuard;
|
||||
|
||||
use super::notification::{ErrorMessage, FrontendEvent, NotificationSystem};
|
||||
use super::notification::{FrontendEvent, NotificationSystem};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Handle {
|
||||
is_exiting: AtomicBool,
|
||||
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||
startup_completed: AtomicBool,
|
||||
pub(crate) notification_system: Arc<RwLock<Option<NotificationSystem>>>,
|
||||
}
|
||||
|
||||
@@ -26,8 +21,6 @@ impl Default for Handle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
is_exiting: AtomicBool::new(false),
|
||||
startup_errors: Arc::new(RwLock::new(Vec::new())),
|
||||
startup_completed: AtomicBool::new(false),
|
||||
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
|
||||
}
|
||||
}
|
||||
@@ -47,7 +40,7 @@ impl Handle {
|
||||
|
||||
let mut system_opt = self.notification_system.write();
|
||||
if let Some(system) = system_opt.as_mut()
|
||||
&& !system.is_running
|
||||
&& !system.is_running()
|
||||
{
|
||||
system.start();
|
||||
}
|
||||
@@ -111,21 +104,19 @@ impl Handle {
|
||||
// TODO 利用 &str 等缩短 Clone
|
||||
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||
let handle = Self::global();
|
||||
let status_str = status.into();
|
||||
let msg_str = msg.into();
|
||||
|
||||
if !handle.startup_completed.load(Ordering::Acquire) {
|
||||
handle.startup_errors.write().push(ErrorMessage {
|
||||
status: status_str,
|
||||
message: msg_str,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if handle.is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only send notice when main window exists
|
||||
if WindowManager::get_main_window().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let status_str = status.into();
|
||||
let msg_str = msg.into();
|
||||
|
||||
Self::send_event(FrontendEvent::NoticeMessage {
|
||||
status: status_str,
|
||||
message: msg_str,
|
||||
@@ -144,49 +135,6 @@ impl Handle {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_startup_completed(&self) {
|
||||
self.startup_completed.store(true, Ordering::Release);
|
||||
self.send_startup_errors();
|
||||
}
|
||||
|
||||
fn send_startup_errors(&self) {
|
||||
let errors = {
|
||||
let mut errors = self.startup_errors.write();
|
||||
std::mem::take(&mut *errors)
|
||||
};
|
||||
|
||||
if errors.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = thread::Builder::new()
|
||||
.name("startup-errors-sender".into())
|
||||
.spawn(move || {
|
||||
thread::sleep(timing::STARTUP_ERROR_DELAY);
|
||||
|
||||
let handle = Self::global();
|
||||
if handle.is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
let system_opt = handle.notification_system.read();
|
||||
if let Some(system) = system_opt.as_ref() {
|
||||
for error in errors {
|
||||
if handle.is_exiting() {
|
||||
break;
|
||||
}
|
||||
|
||||
system.send_event(FrontendEvent::NoticeMessage {
|
||||
status: error.status,
|
||||
message: error.message,
|
||||
});
|
||||
|
||||
thread::sleep(timing::ERROR_BATCH_DELAY);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_is_exiting(&self) {
|
||||
self.is_exiting.store(true, Ordering::Release);
|
||||
|
||||
|
||||
@@ -1 +1,221 @@
|
||||
// TODO: global logger to record verge log message
|
||||
use std::{
|
||||
str::FromStr as _,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use clash_verge_service_ipc::WriterConfig;
|
||||
use compact_str::CompactString;
|
||||
use flexi_logger::{
|
||||
Cleanup, Criterion, DeferredNow, FileSpec, LogSpecBuilder, LogSpecification, LoggerHandle,
|
||||
writers::{FileLogWriter, FileLogWriterBuilder, LogWriter as _},
|
||||
};
|
||||
use log::{Level, LevelFilter, Record};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
|
||||
use crate::{
|
||||
core::service,
|
||||
singleton,
|
||||
utils::dirs::{self, service_log_dir, sidecar_log_dir},
|
||||
};
|
||||
|
||||
pub struct Logger {
|
||||
handle: Arc<Mutex<Option<LoggerHandle>>>,
|
||||
sidecar_file_writer: Arc<RwLock<Option<FileLogWriter>>>,
|
||||
log_level: Arc<RwLock<LevelFilter>>,
|
||||
log_max_size: AtomicU64,
|
||||
log_max_count: AtomicUsize,
|
||||
}
|
||||
|
||||
impl Default for Logger {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
handle: Arc::new(Mutex::new(None)),
|
||||
sidecar_file_writer: Arc::new(RwLock::new(None)),
|
||||
log_level: Arc::new(RwLock::new(LevelFilter::Info)),
|
||||
log_max_size: AtomicU64::new(128),
|
||||
log_max_count: AtomicUsize::new(8),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
singleton!(Logger, LOGGER);
|
||||
|
||||
impl Logger {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let (log_level, log_max_size, log_max_count) = {
|
||||
let verge_guard = crate::config::Config::verge().await;
|
||||
let verge = verge_guard.latest_arc();
|
||||
(
|
||||
verge.get_log_level(),
|
||||
verge.app_log_max_size.unwrap_or(128),
|
||||
verge.app_log_max_count.unwrap_or(8),
|
||||
)
|
||||
};
|
||||
let log_level = std::env::var("RUST_LOG")
|
||||
.ok()
|
||||
.and_then(|v| log::LevelFilter::from_str(&v).ok())
|
||||
.unwrap_or(log_level);
|
||||
*self.log_level.write() = log_level;
|
||||
self.log_max_size.store(log_max_size, Ordering::SeqCst);
|
||||
self.log_max_count.store(log_max_count, Ordering::SeqCst);
|
||||
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
{
|
||||
let log_spec = Self::generate_log_spec(log_level);
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
let logger = flexi_logger::Logger::with(log_spec)
|
||||
.log_to_file(FileSpec::default().directory(log_dir).basename(""))
|
||||
.duplicate_to_stdout(log_level.into())
|
||||
.format(clash_verge_logger::console_format)
|
||||
.format_for_files(clash_verge_logger::file_format_with_level)
|
||||
.rotate(
|
||||
Criterion::Size(log_max_size * 1024),
|
||||
flexi_logger::Naming::TimestampsCustomFormat {
|
||||
current_infix: Some("latest"),
|
||||
format: "%Y-%m-%d_%H-%M-%S",
|
||||
},
|
||||
Cleanup::KeepLogFiles(log_max_count),
|
||||
);
|
||||
|
||||
let mut filter_modules = vec!["wry", "tokio_tungstenite", "tungstenite"];
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
filter_modules.push("tauri");
|
||||
#[cfg(feature = "tracing")]
|
||||
filter_modules.extend(["tauri_plugin_mihomo", "kode_bridge"]);
|
||||
let logger = logger.filter(Box::new(clash_verge_logging::NoModuleFilter(filter_modules)));
|
||||
|
||||
let handle = logger.start()?;
|
||||
*self.handle.lock() = Some(handle);
|
||||
}
|
||||
|
||||
let sidecar_file_writer = self.generate_sidecar_writer()?;
|
||||
*self.sidecar_file_writer.write() = Some(sidecar_file_writer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_log_spec(log_level: LevelFilter) -> LogSpecification {
|
||||
let mut spec = LogSpecBuilder::new();
|
||||
let log_level = std::env::var("RUST_LOG")
|
||||
.ok()
|
||||
.and_then(|v| log::LevelFilter::from_str(&v).ok())
|
||||
.unwrap_or(log_level);
|
||||
spec.default(log_level);
|
||||
#[cfg(feature = "tracing")]
|
||||
spec.module("tauri", log::LevelFilter::Debug)
|
||||
.module("wry", log::LevelFilter::Off)
|
||||
.module("tauri_plugin_mihomo", log::LevelFilter::Off);
|
||||
spec.build()
|
||||
}
|
||||
|
||||
fn generate_file_log_writer(&self) -> Result<FileLogWriterBuilder> {
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
|
||||
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
|
||||
let flwb = FileLogWriter::builder(FileSpec::default().directory(log_dir).basename("")).rotate(
|
||||
Criterion::Size(log_max_size * 1024),
|
||||
flexi_logger::Naming::TimestampsCustomFormat {
|
||||
current_infix: Some("latest"),
|
||||
format: "%Y-%m-%d_%H-%M-%S",
|
||||
},
|
||||
Cleanup::KeepLogFiles(log_max_count),
|
||||
);
|
||||
Ok(flwb)
|
||||
}
|
||||
|
||||
/// only update app log level
|
||||
pub fn update_log_level(&self, level: LevelFilter) -> Result<()> {
|
||||
*self.log_level.write() = level;
|
||||
let log_level = self.log_level.read().to_owned();
|
||||
if let Some(handle) = self.handle.lock().as_mut() {
|
||||
let log_spec = Self::generate_log_spec(log_level);
|
||||
handle.set_new_spec(log_spec);
|
||||
handle.adapt_duplication_to_stdout(log_level.into())?;
|
||||
} else {
|
||||
bail!("failed to get logger handle, make sure it init");
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update app and mihomo core log config
|
||||
pub async fn update_log_config(&self, log_max_size: u64, log_max_count: usize) -> Result<()> {
|
||||
self.log_max_size.store(log_max_size, Ordering::SeqCst);
|
||||
self.log_max_count.store(log_max_count, Ordering::SeqCst);
|
||||
if let Some(handle) = self.handle.lock().as_ref() {
|
||||
let log_file_writer = self.generate_file_log_writer()?;
|
||||
handle.reset_flw(&log_file_writer)?;
|
||||
} else {
|
||||
bail!("failed to get logger handle, make sure it init");
|
||||
};
|
||||
let sidecar_writer = self.generate_sidecar_writer()?;
|
||||
*self.sidecar_file_writer.write() = Some(sidecar_writer);
|
||||
|
||||
// update service writer config
|
||||
if service::is_service_ipc_path_exists() && service::is_service_available().await.is_ok() {
|
||||
let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.into();
|
||||
clash_verge_service_ipc::update_writer(&WriterConfig {
|
||||
directory: service_log_dir,
|
||||
max_log_size: log_max_size * 1024,
|
||||
max_log_files: log_max_count,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_sidecar_writer(&self) -> Result<FileLogWriter> {
|
||||
let sidecar_log_dir = sidecar_log_dir()?;
|
||||
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
|
||||
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
|
||||
Ok(FileLogWriter::builder(
|
||||
FileSpec::default()
|
||||
.directory(sidecar_log_dir)
|
||||
.basename("sidecar")
|
||||
.suppress_timestamp(),
|
||||
)
|
||||
.format(clash_verge_logger::file_format_without_level)
|
||||
.rotate(
|
||||
Criterion::Size(log_max_size * 1024),
|
||||
flexi_logger::Naming::TimestampsCustomFormat {
|
||||
current_infix: Some("latest"),
|
||||
format: "%Y-%m-%d_%H-%M-%S",
|
||||
},
|
||||
Cleanup::KeepLogFiles(log_max_count),
|
||||
)
|
||||
.try_build()?)
|
||||
}
|
||||
|
||||
pub fn writer_sidecar_log(&self, level: Level, message: &CompactString) {
|
||||
if let Some(writer) = self.sidecar_file_writer.read().as_ref() {
|
||||
let mut now = DeferredNow::default();
|
||||
let args = format_args!("{}", message);
|
||||
let record = Record::builder().args(args).level(level).target("sidecar").build();
|
||||
let _ = writer.write(&mut now, &record);
|
||||
} else {
|
||||
logging!(error, Type::System, "failed to get sidecar file log writer");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn service_writer_config(&self) -> Result<WriterConfig> {
|
||||
let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.into();
|
||||
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
|
||||
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
|
||||
let writer_config = WriterConfig {
|
||||
directory: service_log_dir,
|
||||
max_log_size: log_max_size * 1024,
|
||||
max_log_files: log_max_count,
|
||||
};
|
||||
|
||||
Ok(writer_config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use super::CoreManager;
|
||||
use crate::{
|
||||
config::{Config, ConfigType},
|
||||
config::{Config, ConfigType, runtime::IRuntime},
|
||||
constants::timing,
|
||||
core::{handle, validate::CoreConfigValidator},
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use clash_verge_types::runtime::IRuntime;
|
||||
use smartstring::alias::String;
|
||||
use std::{collections::HashSet, path::PathBuf, time::Instant};
|
||||
use tauri_plugin_mihomo::Error as MihomoError;
|
||||
@@ -60,10 +59,10 @@ impl CoreManager {
|
||||
|
||||
async fn perform_config_update(&self) -> Result<(bool, String)> {
|
||||
Config::generate().await?;
|
||||
self.apply_generate_confihg().await
|
||||
self.apply_generate_config().await
|
||||
}
|
||||
|
||||
pub async fn apply_generate_confihg(&self) -> Result<(bool, String)> {
|
||||
pub async fn apply_generate_config(&self) -> Result<(bool, String)> {
|
||||
match CoreConfigValidator::global().validate_config().await {
|
||||
Ok((true, _)) => {
|
||||
let run_path = Config::generate_file(ConfigType::Run).await?;
|
||||
|
||||
@@ -2,14 +2,13 @@ use super::{CoreManager, RunningMode};
|
||||
use crate::{
|
||||
AsyncHandler,
|
||||
config::{Config, IClashTemp},
|
||||
core::{handle, manager::CLASH_LOGGER, service},
|
||||
core::{handle, logger::Logger, manager::CLASH_LOGGER, service},
|
||||
logging,
|
||||
utils::{dirs, init::sidecar_writer},
|
||||
utils::dirs,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{SharedWriter, Type, write_sidecar_log};
|
||||
use clash_verge_logging::Type;
|
||||
use compact_str::CompactString;
|
||||
use flexi_logger::DeferredNow;
|
||||
use log::Level;
|
||||
use scopeguard::defer;
|
||||
use tauri_plugin_shell::ShellExt as _;
|
||||
@@ -31,6 +30,8 @@ impl CoreManager {
|
||||
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
|
||||
#[cfg(unix)]
|
||||
let previous_mask = unsafe { tauri_plugin_clash_verge_sysinfo::libc::umask(0o007) };
|
||||
let (mut rx, child) = app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core.as_str())?
|
||||
@@ -47,6 +48,10 @@ impl CoreManager {
|
||||
&IClashTemp::guard_external_controller_ipc(),
|
||||
])
|
||||
.spawn()?;
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
tauri_plugin_clash_verge_sysinfo::libc::umask(previous_mask)
|
||||
};
|
||||
|
||||
let pid = child.pid();
|
||||
logging!(trace, Type::Core, "Sidecar started with PID: {}", pid);
|
||||
@@ -54,20 +59,16 @@ impl CoreManager {
|
||||
self.set_running_child_sidecar(child);
|
||||
self.set_running_mode(RunningMode::Sidecar);
|
||||
|
||||
let shared_writer: SharedWriter = std::sync::Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?));
|
||||
|
||||
AsyncHandler::spawn(|| async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
tauri_plugin_shell::process::CommandEvent::Stdout(line)
|
||||
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
|
||||
let mut now = DeferredNow::default();
|
||||
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
|
||||
write_sidecar_log(shared_writer.lock().await, &mut now, Level::Error, &message);
|
||||
let message = CompactString::from(&*String::from_utf8_lossy(&line));
|
||||
Logger::global().writer_sidecar_log(Level::Error, &message);
|
||||
CLASH_LOGGER.append_log(message).await;
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
|
||||
let mut now = DeferredNow::default();
|
||||
let message = if let Some(code) = term.code {
|
||||
CompactString::from(format!("Process terminated with code: {}", code))
|
||||
} else if let Some(signal) = term.signal {
|
||||
@@ -75,7 +76,7 @@ impl CoreManager {
|
||||
} else {
|
||||
CompactString::from("Process terminated")
|
||||
};
|
||||
write_sidecar_log(shared_writer.lock().await, &mut now, Level::Info, &message);
|
||||
Logger::global().writer_sidecar_log(Level::Info, &message);
|
||||
CLASH_LOGGER.clear_logs().await;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
use super::handle::Handle;
|
||||
use crate::constants::{retry, timing};
|
||||
use crate::constants::timing;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::RwLock;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
time::Instant,
|
||||
};
|
||||
use std::{sync::mpsc, thread};
|
||||
use tauri::{Emitter as _, WebviewWindow};
|
||||
|
||||
// TODO 重构或优化,避免 Clone 过多
|
||||
@@ -25,27 +17,11 @@ pub enum FrontendEvent {
|
||||
ProfileUpdateCompleted { uid: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct EventStats {
|
||||
total_sent: AtomicU64,
|
||||
total_errors: AtomicU64,
|
||||
last_error_time: RwLock<Option<Instant>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ErrorMessage {
|
||||
pub status: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotificationSystem {
|
||||
sender: Option<mpsc::Sender<FrontendEvent>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
worker_handle: Option<thread::JoinHandle<()>>,
|
||||
pub(super) is_running: bool,
|
||||
stats: EventStats,
|
||||
emergency_mode: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for NotificationSystem {
|
||||
@@ -55,25 +31,26 @@ impl Default for NotificationSystem {
|
||||
}
|
||||
|
||||
impl NotificationSystem {
|
||||
pub fn new() -> Self {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
sender: None,
|
||||
worker_handle: None,
|
||||
is_running: false,
|
||||
stats: EventStats::default(),
|
||||
emergency_mode: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_running(&self) -> bool {
|
||||
self.sender.is_some() && self.worker_handle.is_some()
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
if self.is_running {
|
||||
if self.is_running() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.sender = Some(tx);
|
||||
self.is_running = true;
|
||||
|
||||
//? Do we have to create a new thread for this?
|
||||
let result = thread::Builder::new()
|
||||
.name("frontend-notifier".into())
|
||||
.spawn(move || Self::worker_loop(rx));
|
||||
@@ -107,10 +84,6 @@ impl NotificationSystem {
|
||||
None => return,
|
||||
};
|
||||
|
||||
if system.should_skip_event(&event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(window) = super::handle::Handle::get_window() {
|
||||
system.emit_to_window(&window, event);
|
||||
drop(binding);
|
||||
@@ -118,30 +91,15 @@ impl NotificationSystem {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip_event(&self, event: &FrontendEvent) -> bool {
|
||||
let is_emergency = self.emergency_mode.load(Ordering::Acquire);
|
||||
matches!(
|
||||
(is_emergency, event),
|
||||
(true, FrontendEvent::NoticeMessage { status, .. }) if status == "info"
|
||||
)
|
||||
}
|
||||
|
||||
fn emit_to_window(&self, window: &WebviewWindow, event: FrontendEvent) {
|
||||
let (event_name, payload) = self.serialize_event(event);
|
||||
|
||||
let Ok(payload) = payload else {
|
||||
self.stats.total_errors.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
};
|
||||
|
||||
match window.emit(event_name, payload) {
|
||||
Ok(_) => {
|
||||
self.stats.total_sent.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Frontend, "Event emit failed: {}", e);
|
||||
self.handle_emit_error();
|
||||
}
|
||||
if let Err(e) = window.emit(event_name, payload) {
|
||||
logging!(warn, Type::Frontend, "Event emit failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,19 +119,8 @@ impl NotificationSystem {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_emit_error(&self) {
|
||||
self.stats.total_errors.fetch_add(1, Ordering::Relaxed);
|
||||
*self.stats.last_error_time.write() = Some(Instant::now());
|
||||
|
||||
let errors = self.stats.total_errors.load(Ordering::Relaxed);
|
||||
if errors > retry::EVENT_EMIT_THRESHOLD && !self.emergency_mode.load(Ordering::Acquire) {
|
||||
logging!(warn, Type::Frontend, "Entering emergency mode after {} errors", errors);
|
||||
self.emergency_mode.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_event(&self, event: FrontendEvent) -> bool {
|
||||
if self.should_skip_event(&event) {
|
||||
if !self.is_running() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -185,8 +132,6 @@ impl NotificationSystem {
|
||||
}
|
||||
|
||||
pub fn shutdown(&mut self) {
|
||||
self.is_running = false;
|
||||
|
||||
if let Some(sender) = self.sender.take() {
|
||||
drop(sender);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
config::{Config, IClashTemp},
|
||||
core::tray::Tray,
|
||||
utils::{dirs, init::service_writer_config},
|
||||
core::{logger::Logger, tray::Tray},
|
||||
utils::dirs,
|
||||
};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
@@ -30,9 +30,8 @@ pub enum ServiceStatus {
|
||||
#[derive(Clone)]
|
||||
pub struct ServiceManager(ServiceStatus);
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn uninstall_service() -> Result<()> {
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
@@ -63,9 +62,8 @@ async fn uninstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn install_service() -> Result<()> {
|
||||
fn install_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "install service");
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
@@ -93,27 +91,8 @@ async fn install_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "reinstall service");
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service().await {
|
||||
logging!(warn, Type::Service, "failed to uninstall service: {}", err);
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service().await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
bail!(format!("failed to install service: {err}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn uninstall_service() -> Result<()> {
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall");
|
||||
@@ -169,8 +148,7 @@ async fn uninstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn install_service() -> Result<()> {
|
||||
fn install_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "install service");
|
||||
|
||||
let install_path = tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-install");
|
||||
@@ -222,24 +200,6 @@ async fn install_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "reinstall service");
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service().await {
|
||||
logging!(warn, Type::Service, "failed to uninstall service: {}", err);
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service().await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
bail!(format!("failed to install service: {err}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_running_as_root() -> bool {
|
||||
use crate::core::handle;
|
||||
@@ -249,7 +209,7 @@ fn linux_running_as_root() -> bool {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn uninstall_service() -> Result<()> {
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
@@ -261,9 +221,9 @@ async fn uninstall_service() -> Result<()> {
|
||||
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||
|
||||
crate::utils::i18n::sync_locale().await;
|
||||
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
|
||||
|
||||
let prompt = rust_i18n::t!("service.adminUninstallPrompt").to_string();
|
||||
let prompt = clash_verge_i18n::t!("service.adminUninstallPrompt");
|
||||
let command =
|
||||
format!(r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#);
|
||||
|
||||
@@ -282,7 +242,7 @@ async fn uninstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn install_service() -> Result<()> {
|
||||
fn install_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "install service");
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
@@ -294,11 +254,13 @@ async fn install_service() -> Result<()> {
|
||||
|
||||
let install_shell: String = install_path.to_string_lossy().into_owned();
|
||||
|
||||
crate::utils::i18n::sync_locale().await;
|
||||
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
|
||||
|
||||
let prompt = rust_i18n::t!("service.adminInstallPrompt").to_string();
|
||||
let command =
|
||||
format!(r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#);
|
||||
let gid = tauri_plugin_clash_verge_sysinfo::current_gid();
|
||||
let prompt = clash_verge_i18n::t!("service.adminInstallPrompt");
|
||||
let command = format!(
|
||||
r#"do shell script "sudo CLASH_VERGE_SERVICE_GID={gid} '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||
);
|
||||
|
||||
let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?;
|
||||
|
||||
@@ -309,17 +271,16 @@ async fn install_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn reinstall_service() -> Result<()> {
|
||||
fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "reinstall service");
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service().await {
|
||||
if let Err(err) = uninstall_service() {
|
||||
logging!(warn, Type::Service, "failed to uninstall service: {}", err);
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service().await {
|
||||
match install_service() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
bail!(format!("failed to install service: {err}"))
|
||||
@@ -328,62 +289,14 @@ async fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
/// 强制重装服务(UI修复按钮)
|
||||
async fn force_reinstall_service() -> Result<()> {
|
||||
fn force_reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "用户请求强制重装服务");
|
||||
reinstall_service().await.map_err(|err| {
|
||||
reinstall_service().map_err(|err| {
|
||||
logging!(error, Type::Service, "强制重装服务失败: {}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
/// 检查服务版本 - 使用IPC通信
|
||||
async fn check_service_version() -> Result<String> {
|
||||
let version_arc: Result<String> = {
|
||||
logging!(info, Type::Service, "开始检查服务版本 (IPC)");
|
||||
let result = clash_verge_service_ipc::get_version().await;
|
||||
logging!(debug, Type::Service, "检查服务版本 (IPC) 结果: {:?}", result);
|
||||
|
||||
// 检查错误信息是否是JSON序列化错误或预期值错误,以适配老版本服务
|
||||
// 这可能是因为老版本服务的API不兼容,导致无法正确解析响应
|
||||
// 如果是这种情况,直接返回空字符串,表示无法获取版本
|
||||
if let Err(e) = result.as_ref()
|
||||
&& (e.to_string().contains("JSON serialization error") || e.to_string().contains("expected value"))
|
||||
{
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
"服务版本检查失败,可能是老版本服务 API 不兼容: {}",
|
||||
e
|
||||
);
|
||||
return Ok("".to_string());
|
||||
}
|
||||
|
||||
// 因为上面的错误处理 Error 可能会被忽略,所以这里需要再次检查
|
||||
let response = result.context("无法连接到Clash Verge Service")?;
|
||||
if response.code > 0 {
|
||||
let err_msg = response.message;
|
||||
logging!(error, Type::Service, "获取服务版本失败: {}", err_msg);
|
||||
return Err(anyhow::anyhow!(err_msg));
|
||||
}
|
||||
|
||||
let version = response.data.unwrap_or_else(|| "unknown".into());
|
||||
Ok(version)
|
||||
};
|
||||
|
||||
match version_arc.as_ref() {
|
||||
Ok(v) => Ok(v.clone()),
|
||||
Err(e) => Err(anyhow::Error::msg(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查服务是否需要重装
|
||||
pub async fn check_service_needs_reinstall() -> Result<bool> {
|
||||
match check_service_version().await {
|
||||
Ok(version) => Ok(version != clash_verge_service_ipc::VERSION),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// 尝试使用服务启动core
|
||||
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
|
||||
logging!(info, Type::Service, "尝试使用现有服务启动核心");
|
||||
@@ -402,7 +315,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
core_ipc_path: IClashTemp::guard_external_controller_ipc(),
|
||||
config_dir: dirs::path_to_str(&dirs::app_home_dir()?)?.into(),
|
||||
},
|
||||
log_config: service_writer_config().await?,
|
||||
log_config: Logger::global().service_writer_config()?,
|
||||
};
|
||||
|
||||
let response = clash_verge_service_ipc::start_clash(&payload)
|
||||
@@ -470,7 +383,12 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
/// 检查服务是否正在运行
|
||||
pub async fn is_service_available() -> Result<()> {
|
||||
if let Err(e) = Path::metadata(clash_verge_service_ipc::IPC_PATH.as_ref()) {
|
||||
logging!(warn, Type::Service, "Some issue with service IPC Path: {}", e);
|
||||
let verge = Config::verge().await;
|
||||
let verge_last = verge.latest_arc();
|
||||
let is_enable = verge_last.enable_tun_mode.unwrap_or(false);
|
||||
if is_enable {
|
||||
logging!(warn, Type::Service, "Some issue with service IPC Path: {}", e);
|
||||
}
|
||||
return Err(e.into());
|
||||
}
|
||||
clash_verge_service_ipc::connect().await?;
|
||||
@@ -524,20 +442,10 @@ impl ServiceManager {
|
||||
|
||||
/// 综合服务状态检查(一次性完成所有检查)
|
||||
pub async fn check_service_comprehensive(&self) -> ServiceStatus {
|
||||
match check_service_needs_reinstall().await {
|
||||
Ok(need) => {
|
||||
logging!(debug, Type::Service, "服务当前可用,检查是否需要重装");
|
||||
if need {
|
||||
logging!(debug, Type::Service, "服务需要重装且需要重装");
|
||||
ServiceStatus::NeedsReinstall
|
||||
} else {
|
||||
ServiceStatus::Ready
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Service, "服务不可用,检查安装状态");
|
||||
ServiceStatus::Unavailable(err.to_string())
|
||||
}
|
||||
if clash_verge_service_ipc::is_reinstall_service_needed().await {
|
||||
ServiceStatus::NeedsReinstall
|
||||
} else {
|
||||
ServiceStatus::Ready
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,22 +458,22 @@ impl ServiceManager {
|
||||
}
|
||||
ServiceStatus::NeedsReinstall | ServiceStatus::ReinstallRequired => {
|
||||
logging!(info, Type::Service, "服务需要重装,执行重装流程");
|
||||
reinstall_service().await?;
|
||||
reinstall_service()?;
|
||||
wait_and_check_service_available(self).await?;
|
||||
}
|
||||
ServiceStatus::ForceReinstallRequired => {
|
||||
logging!(info, Type::Service, "服务需要强制重装,执行强制重装流程");
|
||||
force_reinstall_service().await?;
|
||||
force_reinstall_service()?;
|
||||
wait_and_check_service_available(self).await?;
|
||||
}
|
||||
ServiceStatus::InstallRequired => {
|
||||
logging!(info, Type::Service, "需要安装服务,执行安装流程");
|
||||
install_service().await?;
|
||||
install_service()?;
|
||||
wait_and_check_service_available(self).await?;
|
||||
}
|
||||
ServiceStatus::UninstallRequired => {
|
||||
logging!(info, Type::Service, "服务需要卸载,执行卸载流程");
|
||||
uninstall_service().await?;
|
||||
uninstall_service()?;
|
||||
self.0 = ServiceStatus::Unavailable("Service Uninstalled".into());
|
||||
}
|
||||
ServiceStatus::Unavailable(reason) => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::utils::autostart as startup_shortcut;
|
||||
use crate::utils::schtasks as startup_task;
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle::Handle,
|
||||
singleton,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use clash_verge_logging::logging_error;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::RwLock;
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
@@ -18,7 +20,10 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use tauri_plugin_autostart::ManagerExt as _;
|
||||
#[cfg(target_os = "windows")]
|
||||
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
||||
|
||||
pub struct Sysopt {
|
||||
update_sysproxy: AtomicBool,
|
||||
@@ -230,35 +235,21 @@ impl Sysopt {
|
||||
let is_enable = enable_auto_launch.unwrap_or(false);
|
||||
logging!(info, Type::System, "Setting auto-launch state to: {:?}", is_enable);
|
||||
|
||||
// 首先尝试使用快捷方式方法
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if is_enable {
|
||||
if let Err(e) = startup_shortcut::create_shortcut().await {
|
||||
logging!(error, Type::Setup, "创建启动快捷方式失败: {e}");
|
||||
// 如果快捷方式创建失败,回退到原来的方法
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Err(e) = startup_shortcut::remove_shortcut().await {
|
||||
logging!(error, Type::Setup, "删除启动快捷方式失败: {e}");
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
let is_admin = is_current_app_handle_admin(Handle::app_handle());
|
||||
startup_task::set_auto_launch(is_enable, is_admin).await
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// 非Windows平台使用原来的方法
|
||||
self.try_original_autostart_method(is_enable);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 尝试使用原来的自启动方法
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn try_original_autostart_method(&self, is_enable: bool) {
|
||||
let app_handle = Handle::app_handle();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
@@ -272,32 +263,28 @@ impl Sysopt {
|
||||
|
||||
/// 获取当前自启动的实际状态
|
||||
pub fn get_launch_status(&self) -> Result<bool> {
|
||||
// 首先尝试检查快捷方式是否存在
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
match startup_shortcut::is_shortcut_enabled() {
|
||||
Ok(enabled) => {
|
||||
logging!(info, Type::System, "快捷方式自启动状态: {enabled}");
|
||||
return Ok(enabled);
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::System, "检查快捷方式失败,尝试原来的方法: {e}");
|
||||
}
|
||||
let enabled = startup_task::is_auto_launch_enabled();
|
||||
if let Ok(status) = enabled {
|
||||
logging!(info, Type::System, "Auto launch status (scheduled task): {status}");
|
||||
}
|
||||
enabled
|
||||
}
|
||||
|
||||
// 回退到原来的方法
|
||||
let app_handle = Handle::app_handle();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
|
||||
match autostart_manager.is_enabled() {
|
||||
Ok(status) => {
|
||||
logging!(info, Type::System, "Auto launch status: {status}");
|
||||
Ok(status)
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::System, "Failed to get auto launch status: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let app_handle = Handle::app_handle();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
match autostart_manager.is_enabled() {
|
||||
Ok(status) => {
|
||||
logging!(info, Type::System, "Auto launch status: {status}");
|
||||
Ok(status)
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::System, "Failed to get auto launch status: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
use rust_i18n::t;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
fn to_arc_str(value: Cow<'static, str>) -> Arc<str> {
|
||||
match value {
|
||||
Cow::Borrowed(s) => Arc::from(s),
|
||||
Cow::Owned(s) => Arc::from(s.into_boxed_str()),
|
||||
}
|
||||
}
|
||||
use clash_verge_i18n::t;
|
||||
use std::borrow::Cow;
|
||||
|
||||
macro_rules! define_menu {
|
||||
($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => {
|
||||
#[derive(Debug)]
|
||||
pub struct MenuTexts {
|
||||
$(pub $field: Arc<str>,)+
|
||||
$(pub $field: Cow<'static, str>,)+
|
||||
}
|
||||
|
||||
pub struct MenuIds;
|
||||
@@ -20,7 +13,7 @@ macro_rules! define_menu {
|
||||
impl MenuTexts {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
$($field: to_arc_str(t!($text)),)+
|
||||
$($field: t!($text),)+
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,3 +49,24 @@ define_menu! {
|
||||
more => MORE, "tray_more", "tray.more",
|
||||
exit => EXIT, "tray_exit", "tray.exit",
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum TrayAction {
|
||||
SystemProxy,
|
||||
TunMode,
|
||||
MainWindow,
|
||||
TrayMenue,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<&str> for TrayAction {
|
||||
fn from(s: &str) -> Self {
|
||||
match s {
|
||||
"system_proxy" => Self::SystemProxy,
|
||||
"tun_mode" => Self::TunMode,
|
||||
"main_window" => Self::MainWindow,
|
||||
"tray_menue" => Self::TrayMenue,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
||||
use tauri_plugin_mihomo::models::Proxies;
|
||||
use tokio::fs;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::config::{IProfilePreview, IVerge};
|
||||
use crate::core::service;
|
||||
use crate::core::tray::menu_def::TrayAction;
|
||||
use crate::module::lightweight;
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::singleton;
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
use crate::{
|
||||
Type, cmd,
|
||||
config::Config,
|
||||
feat, logging,
|
||||
module::lightweight::is_in_lightweight_mode,
|
||||
utils::{dirs::find_target_icons, i18n},
|
||||
Type, cmd, config::Config, feat, logging, module::lightweight::is_in_lightweight_mode,
|
||||
utils::dirs::find_target_icons,
|
||||
};
|
||||
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
||||
use tauri_plugin_mihomo::models::Proxies;
|
||||
use tokio::fs;
|
||||
|
||||
use super::handle;
|
||||
use anyhow::Result;
|
||||
use parking_lot::Mutex;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::num::NonZeroU32;
|
||||
use std::time::Duration;
|
||||
use tauri::{
|
||||
AppHandle, Wry,
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
@@ -41,48 +33,27 @@ use menu_def::{MenuIds, MenuTexts};
|
||||
|
||||
type ProxyMenuItem = (Option<Submenu<Wry>>, Vec<Box<dyn IsMenuItem<Wry>>>);
|
||||
|
||||
const TRAY_CLICK_DEBOUNCE_MS: u64 = 1_275;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TrayState {}
|
||||
|
||||
// 托盘点击防抖机制
|
||||
static TRAY_CLICK_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
|
||||
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
||||
|
||||
fn get_tray_click_debounce() -> &'static Mutex<Instant> {
|
||||
TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
|
||||
}
|
||||
|
||||
fn should_handle_tray_click() -> bool {
|
||||
let debounce_lock = get_tray_click_debounce();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(*debounce_lock.lock()) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
||||
*debounce_lock.lock() = now;
|
||||
true
|
||||
} else {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Tray,
|
||||
"托盘点击被防抖机制忽略,距离上次点击 {}ms",
|
||||
now.duration_since(*debounce_lock.lock()).as_millis()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub struct Tray {
|
||||
last_menu_update: Mutex<Option<Instant>>,
|
||||
menu_updating: AtomicBool,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub struct Tray {
|
||||
last_menu_update: Mutex<Option<Instant>>,
|
||||
menu_updating: AtomicBool,
|
||||
limiter: DefaultDirectRateLimiter,
|
||||
}
|
||||
|
||||
impl TrayState {
|
||||
async fn get_tray_icon(verge: &IVerge) -> (bool, Vec<u8>) {
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
match (*system_mode, *tun_mode) {
|
||||
(true, true) => Self::get_tun_tray_icon(verge).await,
|
||||
(true, false) => Self::get_sysproxy_tray_icon(verge).await,
|
||||
(false, true) => Self::get_tun_tray_icon(verge).await,
|
||||
(false, false) => Self::get_common_tray_icon(verge).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_common_tray_icon(verge: &IVerge) -> (bool, Vec<u8>) {
|
||||
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
|
||||
if is_common_tray_icon
|
||||
@@ -162,10 +133,14 @@ impl TrayState {
|
||||
}
|
||||
|
||||
impl Default for Tray {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_menu_update: Mutex::new(None),
|
||||
menu_updating: AtomicBool::new(false),
|
||||
limiter: RateLimiter::direct(
|
||||
Quota::with_period(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS))
|
||||
.unwrap()
|
||||
.allow_burst(NonZeroU32::new(1).unwrap()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,12 +185,12 @@ impl Tray {
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
|
||||
let tray_event = tray_event.unwrap_or_else(|| "main_window".into());
|
||||
let tray_event = TrayAction::from(tray_event.as_deref().unwrap_or("main_window"));
|
||||
let tray = app_handle
|
||||
.tray_by_id("main")
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
|
||||
match tray_event.as_str() {
|
||||
"tray_menu" => tray.set_show_menu_on_left_click(true)?,
|
||||
match tray_event {
|
||||
TrayAction::TrayMenue => tray.set_show_menu_on_left_click(true)?,
|
||||
_ => tray.set_show_menu_on_left_click(false)?,
|
||||
}
|
||||
Ok(())
|
||||
@@ -227,45 +202,8 @@ impl Tray {
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘菜单更新");
|
||||
return Ok(());
|
||||
}
|
||||
// 调整最小更新间隔,确保状态及时刷新
|
||||
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
// 检查是否正在更新
|
||||
if self.menu_updating.load(Ordering::Acquire) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 检查更新频率,但允许重要事件跳过频率限制
|
||||
let should_force_update = match std::thread::current().name() {
|
||||
Some("main") => true,
|
||||
_ => {
|
||||
let last_update = self.last_menu_update.lock();
|
||||
if let Some(last_time) = *last_update {
|
||||
last_time.elapsed() >= MIN_UPDATE_INTERVAL
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !should_force_update {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
// 设置更新状态
|
||||
self.menu_updating.store(true, Ordering::Release);
|
||||
|
||||
let result = self.update_menu_internal(app_handle).await;
|
||||
|
||||
{
|
||||
let mut last_update = self.last_menu_update.lock();
|
||||
*last_update = Some(Instant::now());
|
||||
}
|
||||
self.menu_updating.store(false, Ordering::Release);
|
||||
|
||||
result
|
||||
self.update_menu_internal(app_handle).await
|
||||
}
|
||||
|
||||
async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
@@ -331,15 +269,7 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
(true, true) => TrayState::get_tun_tray_icon(verge).await,
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon(verge).await,
|
||||
(false, true) => TrayState::get_tun_tray_icon(verge).await,
|
||||
(false, false) => TrayState::get_common_tray_icon(verge).await,
|
||||
};
|
||||
let (_is_custom_icon, icon_bytes) = TrayState::get_tray_icon(verge).await;
|
||||
|
||||
let colorful = verge.tray_icon.clone().unwrap_or_else(|| "monochrome".into());
|
||||
let is_colorful = colorful == "colorful";
|
||||
@@ -366,15 +296,7 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
(true, true) => TrayState::get_tun_tray_icon(verge).await,
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon(verge).await,
|
||||
(false, true) => TrayState::get_tun_tray_icon(verge).await,
|
||||
(false, false) => TrayState::get_common_tray_icon(verge).await,
|
||||
};
|
||||
let (_is_custom_icon, icon_bytes) = TrayState::get_tray_icon(verge).await;
|
||||
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
Ok(())
|
||||
@@ -389,8 +311,6 @@ impl Tray {
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
i18n::sync_locale().await;
|
||||
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
@@ -417,9 +337,9 @@ impl Tray {
|
||||
}
|
||||
|
||||
// Get localized strings before using them
|
||||
let sys_proxy_text = rust_i18n::t!("tray.tooltip.systemProxy");
|
||||
let tun_text = rust_i18n::t!("tray.tooltip.tun");
|
||||
let profile_text = rust_i18n::t!("tray.tooltip.profile");
|
||||
let sys_proxy_text = clash_verge_i18n::t!("tray.tooltip.systemProxy");
|
||||
let tun_text = clash_verge_i18n::t!("tray.tooltip.tun");
|
||||
let profile_text = clash_verge_i18n::t!("tray.tooltip.profile");
|
||||
|
||||
let v = env!("CARGO_PKG_VERSION");
|
||||
let reassembled_version = v.split_once('+').map_or_else(
|
||||
@@ -469,25 +389,20 @@ impl Tray {
|
||||
|
||||
let verge = Config::verge().await.data_arc();
|
||||
|
||||
// 获取图标
|
||||
let icon_bytes = TrayState::get_common_tray_icon(&verge).await.1;
|
||||
let icon_bytes = TrayState::get_tray_icon(&verge).await.1;
|
||||
let icon = tauri::image::Image::from_bytes(&icon_bytes)?;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let builder = TrayIconBuilder::with_id("main").icon(icon).icon_as_template(false);
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
let show_menu_on_left_click = {
|
||||
// TODO 优化这里 复用 verge
|
||||
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
|
||||
tray_event.is_some_and(|v| v == "tray_menu")
|
||||
};
|
||||
let show_menu_on_left_click = verge.tray_event.as_ref().is_some_and(|v| v == "tray_menu");
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let mut builder = TrayIconBuilder::with_id("main").icon(icon).icon_as_template(false);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let is_monochrome = verge.tray_icon.clone().is_none_or(|v| v == "monochrome");
|
||||
let is_monochrome = verge.tray_icon.as_ref().is_none_or(|v| v == "monochrome");
|
||||
builder = builder.icon_as_template(is_monochrome);
|
||||
}
|
||||
|
||||
@@ -499,8 +414,10 @@ impl Tray {
|
||||
}
|
||||
|
||||
let tray = builder.build(app_handle)?;
|
||||
let tray_event = verge.tray_event.clone().unwrap_or_else(|| "main_window".into());
|
||||
let tray_action = TrayAction::from(tray_event.as_str());
|
||||
|
||||
tray.on_tray_icon_event(|_app_handle, event| {
|
||||
tray.on_tray_icon_event(move |_app_handle, event| {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
@@ -508,33 +425,46 @@ impl Tray {
|
||||
} = event
|
||||
{
|
||||
// 添加防抖检查,防止快速连击
|
||||
if !should_handle_tray_click() {
|
||||
logging!(info, Type::Tray, "click tray icon too fast, ignore");
|
||||
#[allow(clippy::use_self)]
|
||||
if !Tray::global().should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
AsyncHandler::spawn(|| async move {
|
||||
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
|
||||
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
|
||||
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy().await,
|
||||
"tun_mode" => feat::toggle_tun_mode(None).await,
|
||||
"main_window" => {
|
||||
logging!(debug, Type::Tray, "tray event: {tray_action:?}");
|
||||
match tray_action {
|
||||
TrayAction::SystemProxy => {
|
||||
AsyncHandler::spawn(|| async move {
|
||||
let _ = feat::toggle_system_proxy().await;
|
||||
});
|
||||
}
|
||||
TrayAction::TunMode => {
|
||||
AsyncHandler::spawn(|| async move {
|
||||
let _ = feat::toggle_tun_mode(None).await;
|
||||
});
|
||||
}
|
||||
TrayAction::MainWindow => {
|
||||
AsyncHandler::spawn(|| async move {
|
||||
if !lightweight::exit_lightweight_mode().await {
|
||||
WindowManager::show_main_window().await;
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
logging!(warn, Type::Tray, "invalid tray event: {}", tray_event);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
logging!(warn, Type::Tray, "invalid tray event: {}", tray_event);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
tray.on_menu_event(on_menu_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_handle_tray_click(&self) -> bool {
|
||||
let res = self.limiter.check().is_ok();
|
||||
if !res {
|
||||
logging!(debug, Type::Tray, "tray click rate limited");
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
|
||||
@@ -680,7 +610,7 @@ fn create_proxy_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
show_proxy_groups_inline: bool,
|
||||
proxy_submenus: Vec<Submenu<Wry>>,
|
||||
proxies_text: &Arc<str>,
|
||||
proxies_text: &str,
|
||||
) -> Result<ProxyMenuItem> {
|
||||
// 创建代理主菜单
|
||||
let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline {
|
||||
@@ -724,8 +654,6 @@ async fn create_tray_menu(
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
|
||||
i18n::sync_locale().await;
|
||||
|
||||
// TODO: should update tray menu again when it was timeout error
|
||||
let proxy_nodes_data = tokio::time::timeout(
|
||||
Duration::from_millis(1000),
|
||||
@@ -770,7 +698,11 @@ async fn create_tray_menu(
|
||||
});
|
||||
|
||||
let verge_settings = Config::verge().await.latest_arc();
|
||||
let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(true);
|
||||
let tray_proxy_groups_display_mode = verge_settings
|
||||
.tray_proxy_groups_display_mode
|
||||
.as_deref()
|
||||
.unwrap_or("default");
|
||||
let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false);
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -794,13 +726,6 @@ async fn create_tray_menu(
|
||||
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
|
||||
)?;
|
||||
|
||||
let current_mode_text = match current_proxy_mode {
|
||||
"global" => rust_i18n::t!("tray.global"),
|
||||
"direct" => rust_i18n::t!("tray.direct"),
|
||||
_ => rust_i18n::t!("tray.rule"),
|
||||
};
|
||||
let outbound_modes_label = format!("{} ({})", texts.outbound_modes, current_mode_text);
|
||||
|
||||
let rule_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
MenuIds::RULE_MODE,
|
||||
@@ -828,17 +753,27 @@ async fn create_tray_menu(
|
||||
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
|
||||
)?;
|
||||
|
||||
let outbound_modes = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
MenuIds::OUTBOUND_MODES,
|
||||
outbound_modes_label.as_str(),
|
||||
true,
|
||||
&[
|
||||
rule_mode as &dyn IsMenuItem<Wry>,
|
||||
global_mode as &dyn IsMenuItem<Wry>,
|
||||
direct_mode as &dyn IsMenuItem<Wry>,
|
||||
],
|
||||
)?;
|
||||
let outbound_modes = if show_outbound_modes_inline {
|
||||
None
|
||||
} else {
|
||||
let current_mode_text = match current_proxy_mode {
|
||||
"global" => clash_verge_i18n::t!("tray.global"),
|
||||
"direct" => clash_verge_i18n::t!("tray.direct"),
|
||||
_ => clash_verge_i18n::t!("tray.rule"),
|
||||
};
|
||||
let outbound_modes_label = format!("{} ({})", texts.outbound_modes, current_mode_text);
|
||||
Some(Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
MenuIds::OUTBOUND_MODES,
|
||||
outbound_modes_label.as_str(),
|
||||
true,
|
||||
&[
|
||||
rule_mode as &dyn IsMenuItem<Wry>,
|
||||
global_mode as &dyn IsMenuItem<Wry>,
|
||||
direct_mode as &dyn IsMenuItem<Wry>,
|
||||
],
|
||||
)?)
|
||||
};
|
||||
|
||||
let profiles = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
@@ -851,8 +786,11 @@ async fn create_tray_menu(
|
||||
let proxy_sub_menus =
|
||||
create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data);
|
||||
|
||||
let (proxies_menu, inline_proxy_items) =
|
||||
create_proxy_menu_item(app_handle, show_proxy_groups_inline, proxy_sub_menus, &texts.proxies)?;
|
||||
let (proxies_menu, inline_proxy_items) = match tray_proxy_groups_display_mode {
|
||||
"default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?,
|
||||
"inline" => create_proxy_menu_item(app_handle, true, proxy_sub_menus, &texts.proxies)?,
|
||||
_ => (None, Vec::new()),
|
||||
};
|
||||
|
||||
let system_proxy = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -946,15 +884,29 @@ async fn create_tray_menu(
|
||||
let separator = &PredefinedMenuItem::separator(app_handle)?;
|
||||
|
||||
// 动态构建菜单项
|
||||
let mut menu_items: Vec<&dyn IsMenuItem<Wry>> = vec![open_window, outbound_modes, separator, profiles];
|
||||
let mut menu_items: Vec<&dyn IsMenuItem<Wry>> = vec![open_window, separator];
|
||||
|
||||
if show_outbound_modes_inline {
|
||||
menu_items.extend_from_slice(&[
|
||||
rule_mode as &dyn IsMenuItem<Wry>,
|
||||
global_mode as &dyn IsMenuItem<Wry>,
|
||||
direct_mode as &dyn IsMenuItem<Wry>,
|
||||
]);
|
||||
} else if let Some(ref outbound_modes) = outbound_modes {
|
||||
menu_items.push(outbound_modes);
|
||||
}
|
||||
|
||||
menu_items.extend_from_slice(&[separator, profiles]);
|
||||
|
||||
// 如果有代理节点,添加代理节点菜单
|
||||
if show_proxy_groups_inline {
|
||||
if !inline_proxy_items.is_empty() {
|
||||
match tray_proxy_groups_display_mode {
|
||||
"default" => {
|
||||
menu_items.extend(proxies_menu.iter().map(|item| item as &dyn IsMenuItem<_>));
|
||||
}
|
||||
"inline" if !inline_proxy_items.is_empty() => {
|
||||
menu_items.extend(inline_proxy_items.iter().map(|item| item.as_ref()));
|
||||
}
|
||||
} else if let Some(ref proxies_menu) = proxies_menu {
|
||||
menu_items.push(proxies_menu);
|
||||
_ => {}
|
||||
}
|
||||
|
||||
menu_items.extend_from_slice(&[
|
||||
@@ -984,10 +936,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
MenuIds::DASHBOARD => {
|
||||
logging!(info, Type::Tray, "托盘菜单点击: 打开窗口");
|
||||
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
if !lightweight::exit_lightweight_mode().await {
|
||||
WindowManager::show_main_window().await;
|
||||
};
|
||||
@@ -1005,7 +953,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
MenuIds::COPY_ENV => feat::copy_clash_env().await,
|
||||
MenuIds::CONF_DIR => {
|
||||
println!("Open directory submenu clicked");
|
||||
let _ = cmd::open_app_dir().await;
|
||||
}
|
||||
MenuIds::CORE_DIR => {
|
||||
@@ -1023,9 +970,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
MenuIds::RESTART_CLASH => feat::restart_clash_core().await,
|
||||
MenuIds::RESTART_APP => feat::restart_app().await,
|
||||
MenuIds::LIGHTWEIGHT_MODE => {
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
if !is_in_lightweight_mode() {
|
||||
lightweight::entry_lightweight_mode().await;
|
||||
} else {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -61,14 +61,11 @@ pub fn use_sort(config: Mapping) -> Mapping {
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn use_keys(config: &Mapping) -> Vec<String> {
|
||||
config
|
||||
.iter()
|
||||
.filter_map(|(key, _)| key.as_str())
|
||||
.map(|s: &str| {
|
||||
let mut s: String = s.into();
|
||||
s.make_ascii_lowercase();
|
||||
s
|
||||
})
|
||||
.collect()
|
||||
#[inline]
|
||||
pub fn use_keys<'a>(config: &'a Mapping) -> impl Iterator<Item = String> + 'a {
|
||||
config.iter().filter_map(|(key, _)| key.as_str()).map(|s: &str| {
|
||||
let mut s: String = s.into();
|
||||
s.make_ascii_lowercase();
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ use clash_verge_logging::{Type, logging};
|
||||
use super::use_lowercase;
|
||||
use serde_yaml_ng::{self, Mapping, Value};
|
||||
|
||||
fn deep_merge(a: &mut Value, b: &Value) {
|
||||
fn deep_merge(a: &mut Value, b: Value) {
|
||||
match (a, b) {
|
||||
(&mut Value::Mapping(ref mut a), Value::Mapping(b)) => {
|
||||
for (k, v) in b {
|
||||
deep_merge(a.entry(k.clone()).or_insert(Value::Null), v);
|
||||
}
|
||||
}
|
||||
(a, b) => *a = b.clone(),
|
||||
(a, b) => *a = b,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub fn use_merge(merge: &Mapping, config: Mapping) -> Mapping {
|
||||
let mut config = Value::from(config);
|
||||
let merge = use_lowercase(merge);
|
||||
|
||||
deep_merge(&mut config, &Value::from(merge));
|
||||
deep_merge(&mut config, Value::from(merge));
|
||||
|
||||
config.as_mapping().cloned().unwrap_or_else(|| {
|
||||
logging!(
|
||||
|
||||
@@ -106,7 +106,7 @@ async fn get_config_values() -> ConfigValues {
|
||||
ref verge_http_enabled,
|
||||
ref enable_dns_settings,
|
||||
..
|
||||
} = **verge_arc;
|
||||
} = *verge_arc;
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = (
|
||||
Some(verge_arc.get_valid_clash_core()),
|
||||
@@ -310,7 +310,7 @@ fn process_global_items(
|
||||
profile_name: &String,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
let mut result_map = HashMap::new();
|
||||
let mut exists_keys = use_keys(&config);
|
||||
let mut exists_keys = use_keys(&config).collect::<Vec<_>>();
|
||||
|
||||
if let ChainType::Merge(merge) = global_merge.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user