Compare commits

..

8 Commits

Author SHA1 Message Date
Tunglies
c57a962109 refactor: replace useSWR with custom hooks for update and network interfaces (#6195) 2026-01-27 12:52:20 +00:00
Tunglies
36926df26c refactor: remove SWR_REALTIME configuration and simplify SWR usage in AppDataProvider 2026-01-27 20:07:48 +08:00
renovate[bot]
9d81a13c58 chore(deps): update dependency @actions/github to v8 (#6184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 01:51:06 +00:00
Tunglies
511fab9a9d Revert "perf: improve config processing (#6091)"
This reverts commit bf189bb144.
2026-01-26 23:36:57 +08:00
Tunglies
88529af8c8 fix(Linux): use PKEXEC_UID #6159 (#6160)
* fix(Linux): add GID environment variable for Linux service installation #6159

* chore: bump clash_verge_service_ipc to 2.1.2

* chore: remove CLASH_VERGE_SERVICE_GID for linux

---------

Co-authored-by: Sline <realakayuki@gmail.com>
2026-01-26 12:41:45 +00:00
renovate[bot]
425096e8af chore(deps): lock file maintenance cargo dependencies (#6167)
* chore(deps): lock file maintenance cargo dependencies

* chore: run cargo upgrade and cargo update

* chore: fix clippy

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-26 07:45:36 +00:00
renovate[bot]
8a4e2327c1 chore(deps): lock file maintenance npm dependencies (#6168)
* chore(deps): lock file maintenance npm dependencies

* chore: pnpm update

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-26 07:33:53 +00:00
Tunglies
74b1687be9 feat: implement git-hook using cargo make and add Makefile.toml (#5498)
* feat: implement pre-push checks using cargo make and add Makefile.toml for task management

* feat: enhance Makefile.toml with condition checks for tasks and improve clippy args

* fix: update file patterns for format-check task in Makefile.toml

* feat: update file patterns for eslint and typecheck tasks in Makefile.toml

* feat: refactor Makefile.toml to consolidate Rust tasks and update pre-commit checks

* feat: update Makefile.toml to add i18n-check and lint-staged tasks; modify pre-commit script

* feat: update Makefile.toml to add i18n-check and lint-staged tasks; modify pre-commit script

* refactor: simplify Makefile.toml by removing unused conditions and consolidating dependencies

* feat: update Makefile.toml to define Rust and frontend tasks for pre-commit and pre-push checks

* chore: remove unnecessary tasks

* chore: add windows override

* chore: remove format and format-check

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-26 07:21:02 +00:00
24 changed files with 1256 additions and 1192 deletions

View File

@@ -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

View File

@@ -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

174
Cargo.lock generated
View File

@@ -156,7 +156,7 @@ dependencies = [
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -463,9 +463,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.15.3"
version = "1.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86"
checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -473,9 +473,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.36.0"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8"
checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
dependencies = [
"cc",
"cmake",
@@ -982,9 +982,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.53"
version = "1.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1178,7 +1178,7 @@ dependencies = [
"warp",
"winapi",
"winreg 0.55.0",
"zip 7.1.0",
"zip 7.2.0",
]
[[package]]
@@ -1233,8 +1233,8 @@ dependencies = [
[[package]]
name = "clash_verge_service_ipc"
version = "2.1.1"
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#d9a3b701008a0b55ab21aaa8e5a60ce140dc90b9"
version = "2.1.2"
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#dc7238ef3a8d8b6b87e5e140a008f2e2d66ef262"
dependencies = [
"anyhow",
"compact_str",
@@ -1276,7 +1276,7 @@ dependencies = [
"block",
"cocoa-foundation",
"core-foundation 0.10.1",
"core-graphics",
"core-graphics 0.24.0",
"foreign-types",
"libc",
"objc",
@@ -1302,7 +1302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1509,6 +1509,19 @@ dependencies = [
"libc",
]
[[package]]
name = "core-graphics"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
dependencies = [
"bitflags 2.10.0",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.2.0"
@@ -2012,7 +2025,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2026,12 +2039,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "dispatch"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "dispatch2"
version = "0.3.0"
@@ -2276,7 +2283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3432,12 +3439,12 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"socket2 0.6.2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
"windows-registry 0.6.1",
]
[[package]]
@@ -3765,7 +3772,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi 0.5.2",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4497,7 +4504,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4513,9 +4520,9 @@ dependencies = [
[[package]]
name = "num-conv"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-derive"
@@ -4855,9 +4862,9 @@ dependencies = [
[[package]]
name = "openssl-probe"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "option-ext"
@@ -4901,7 +4908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5449,9 +5456,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppmd-rust"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4"
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
[[package]]
name = "ppv-lite86"
@@ -5542,9 +5549,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.105"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
@@ -5710,7 +5717,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.5.10",
"socket2 0.6.2",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -5748,16 +5755,16 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.5.10",
"socket2 0.6.2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
name = "quote"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
@@ -6370,7 +6377,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6428,7 +6435,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7042,9 +7049,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
@@ -7244,16 +7251,16 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.37.2"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f"
checksum = "fe840c5b1afe259a5657392a4dbb74473a14c8db999c3ec2f4ae812e028a94da"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows 0.61.3",
"windows 0.62.2",
]
[[package]]
@@ -7315,21 +7322,20 @@ checksum = "c0e973b34477b7823833469eb0f5a3a60370fef7a453e02d751b59180d0a5a05"
[[package]]
name = "tao"
version = "0.34.5"
source = "git+https://github.com/tauri-apps/tao#e196538f989894fcb92401fced54d7d3a65fc91a"
source = "git+https://github.com/tauri-apps/tao#648161769c35ddd2bffd939f4d4d6a027325326c"
dependencies = [
"bitflags 2.10.0",
"block2",
"core-foundation 0.10.1",
"core-graphics",
"core-graphics 0.25.0",
"crossbeam-channel",
"dispatch",
"dispatch2",
"dlopen2",
"dpi",
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
"jni",
"lazy_static",
"libc",
"log",
"ndk",
@@ -7365,7 +7371,7 @@ dependencies = [
[[package]]
name = "tao-macros"
version = "0.1.3"
source = "git+https://github.com/tauri-apps/tao#e196538f989894fcb92401fced54d7d3a65fc91a"
source = "git+https://github.com/tauri-apps/tao#648161769c35ddd2bffd939f4d4d6a027325326c"
dependencies = [
"proc-macro2",
"quote",
@@ -7587,7 +7593,7 @@ dependencies = [
"thiserror 2.0.18",
"tracing",
"url",
"windows-registry",
"windows-registry 0.5.3",
"windows-result 0.3.4",
]
@@ -7943,7 +7949,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.3",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -8102,9 +8108,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.45"
version = "0.3.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5"
dependencies = [
"deranged",
"itoa",
@@ -8120,15 +8126,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.7"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.25"
version = "0.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4"
dependencies = [
"num-conv",
"time-core",
@@ -8191,7 +8197,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.6.1",
"socket2 0.6.2",
"tokio-macros",
"tracing",
"windows-sys 0.61.2",
@@ -8424,7 +8430,7 @@ dependencies = [
"hyper-util",
"percent-encoding",
"pin-project",
"socket2 0.6.1",
"socket2 0.6.2",
"sync_wrapper 1.0.2",
"tokio",
"tokio-stream",
@@ -8716,6 +8722,12 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typed-path"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e43ffa54726cdc9ea78392023ffe9fe9cf9ac779e1c6fcb0d23f9862e3879d20"
[[package]]
name = "typeid"
version = "1.0.3"
@@ -8889,9 +8901,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.19.0"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
dependencies = [
"getrandom 0.3.4",
"js-sys",
@@ -9327,7 +9339,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -9507,6 +9519,17 @@ dependencies = [
"windows-strings 0.4.2",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -10083,9 +10106,9 @@ dependencies = [
[[package]]
name = "zbus"
version = "5.13.1"
version = "5.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f79257df967b6779afa536788657777a0001f5b42524fcaf5038d4344df40b"
checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1"
dependencies = [
"async-broadcast",
"async-executor",
@@ -10118,9 +10141,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
version = "5.13.1"
version = "5.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aad23e2d2f91cae771c7af7a630a49e755f1eb74f8a46e9f6d5f7a146edf5a37"
checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1"
dependencies = [
"proc-macro-crate 3.4.0",
"proc-macro2",
@@ -10144,18 +10167,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.33"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.33"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
dependencies = [
"proc-macro2",
"quote",
@@ -10251,9 +10274,9 @@ dependencies = [
[[package]]
name = "zip"
version = "7.1.0"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9013f1222db8a6d680f13a7ccdc60a781199cd09c2fa4eff58e728bb181757fc"
checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0"
dependencies = [
"aes",
"bzip2",
@@ -10271,6 +10294,7 @@ dependencies = [
"ppmd-rust",
"sha1",
"time",
"typed-path",
"zeroize",
"zopfli",
"zstd",
@@ -10284,9 +10308,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
[[package]]
name = "zmij"
version = "1.0.15"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2"
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
[[package]]
name = "zopfli"

View File

@@ -5,6 +5,7 @@
- 修复首次启动时代理信息刷新缓慢
- 修复无网络时无限请求 IP 归属查询
- 修复 WebDAV 页面重试逻辑
- 修复 Linux 通过 GUI 安装服务模式权限不符合预期
<details>
<summary><strong> ✨ 新增功能 </strong></summary>

86
Makefile.toml Normal file
View 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"]

View File

@@ -8,7 +8,7 @@ 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.180"

View File

@@ -55,12 +55,12 @@
"@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.52",
"i18next": "^25.7.4",
"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",
@@ -70,7 +70,7 @@
"react-hook-form": "^7.71.1",
"react-i18next": "16.5.3",
"react-markdown": "10.1.0",
"react-router": "^7.12.0",
"react-router": "^7.13.0",
"react-virtuoso": "^4.18.1",
"rehype-raw": "^7.0.0",
"swr": "^2.3.8",
@@ -78,14 +78,14 @@
"types-pac": "^1.0.3"
},
"devDependencies": {
"@actions/github": "^7.0.0",
"@eslint-react/eslint-plugin": "^2.7.2",
"@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.9",
"@types/react": "19.2.8",
"@types/react": "19.2.9",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react-swc": "^4.2.2",
@@ -103,18 +103,18 @@
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-unused-imports": "^4.3.0",
"glob": "^13.0.0",
"globals": "^17.0.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.8.0",
"sass": "^1.97.2",
"tar": "^7.5.3",
"prettier": "^3.8.1",
"sass": "^1.97.3",
"tar": "^7.5.6",
"terser": "^5.46.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.0",
"typescript-eslint": "^8.53.1",
"vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0"
},

1370
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -81,7 +81,7 @@ tauri-plugin-fs = "2.4.5"
tauri-plugin-process = "2.3.1"
tauri-plugin-deep-link = "2.4.6"
tauri-plugin-window-state = "2.4.1"
zip = "7.1.0"
zip = "7.2.0"
reqwest_dav = "0.3.1"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
@@ -98,7 +98,7 @@ 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.1.1", 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.8.0"

View File

@@ -117,7 +117,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
match profiles_reorder_safe(&active_id, &over_id).await {
Ok(_) => {
logging!(debug, Type::Cmd, "重新排序配置文件");
logging!(info, Type::Cmd, "重新排序配置文件");
Config::profiles().await.apply();
Ok(())
}

View File

@@ -60,13 +60,13 @@ pub struct PrfItem {
pub file_data: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct PrfSelected {
pub name: Option<String>,
pub now: Option<String>,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
pub struct PrfExtra {
pub upload: u64,
pub download: u64,
@@ -124,22 +124,25 @@ pub struct PrfOption {
impl PrfOption {
pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option<Self> {
match (one, other) {
(Some(a), Some(b)) => Some(Self {
user_agent: b.user_agent.as_ref().or(a.user_agent.as_ref()).cloned(),
with_proxy: b.with_proxy.or(a.with_proxy),
self_proxy: b.self_proxy.or(a.self_proxy),
danger_accept_invalid_certs: b.danger_accept_invalid_certs.or(a.danger_accept_invalid_certs),
allow_auto_update: b.allow_auto_update.or(a.allow_auto_update),
update_interval: b.update_interval.or(a.update_interval),
merge: b.merge.as_ref().or(a.merge.as_ref()).cloned(),
script: b.script.as_ref().or(a.script.as_ref()).cloned(),
rules: b.rules.as_ref().or(a.rules.as_ref()).cloned(),
proxies: b.proxies.as_ref().or(a.proxies.as_ref()).cloned(),
groups: b.groups.as_ref().or(a.groups.as_ref()).cloned(),
timeout_seconds: b.timeout_seconds.or(a.timeout_seconds),
}),
(Some(a), None) => Some(a.clone()),
(None, Some(b)) => Some(b.clone()),
(Some(a_ref), Some(b_ref)) => {
let mut result = a_ref.clone();
result.user_agent = b_ref.user_agent.clone().or(result.user_agent);
result.with_proxy = b_ref.with_proxy.or(result.with_proxy);
result.self_proxy = b_ref.self_proxy.or(result.self_proxy);
result.danger_accept_invalid_certs =
b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs);
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update);
result.update_interval = b_ref.update_interval.or(result.update_interval);
result.merge = b_ref.merge.clone().or(result.merge);
result.script = b_ref.script.clone().or(result.script);
result.rules = b_ref.rules.clone().or(result.rules);
result.proxies = b_ref.proxies.clone().or(result.proxies);
result.groups = b_ref.groups.clone().or(result.groups);
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
Some(result)
}
(Some(a_ref), None) => Some(a_ref.clone()),
(None, Some(b_ref)) => Some(b_ref.clone()),
(None, None) => None,
}
}

View File

@@ -5,16 +5,12 @@ use crate::utils::{
};
use anyhow::{Context as _, Result, bail};
use clash_verge_logging::{Type, logging};
use once_cell::sync::OnceCell;
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use std::collections::HashSet;
use tokio::fs;
static PROFILE_FILE_RE: OnceCell<Regex> = OnceCell::new();
/// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IProfiles {
@@ -41,15 +37,23 @@ pub struct CleanupResult {
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if let Some(ref val) = $rv.$key {
if Some(val) != $lv.$key.as_ref() {
$lv.$key = Some(val.to_owned());
}
if ($rv.$key).is_some() {
$lv.$key = $rv.$key.to_owned();
}
};
}
impl IProfiles {
// Helper to find and remove an item by uid from the items vec, returning its file name (if any).
fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<String>) -> Option<String> {
for (i, _) in items.iter().enumerate() {
if items[i].uid == target_uid {
return items.remove(i).file;
}
}
None
}
pub async fn new() -> Self {
let path = match dirs::profiles_path() {
Ok(p) => p,
@@ -59,22 +63,21 @@ impl IProfiles {
}
};
let mut profiles = match help::read_yaml::<Self>(&path).await {
Ok(profiles) => profiles,
match help::read_yaml::<Self>(&path).await {
Ok(mut profiles) => {
let items = profiles.items.get_or_insert_with(Vec::new);
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d").into());
}
}
profiles
}
Err(err) => {
logging!(error, Type::Config, "{err}");
return Self::default();
}
};
let items = profiles.items.get_or_insert_with(Vec::new);
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d").into());
Self::default()
}
}
profiles
}
pub async fn save_file(&self) -> Result<()> {
@@ -110,28 +113,38 @@ impl IProfiles {
pub fn get_item(&self, uid: impl AsRef<str>) -> Result<&PrfItem> {
let uid_str = uid.as_ref();
self.items
.as_ref()
.ok_or_else(|| anyhow::anyhow!("no profile items found"))?
.iter()
.find(|each| each.uid.as_ref().is_some_and(|uid_val| uid_val.as_str() == uid_str))
.ok_or_else(|| anyhow::anyhow!("failed to get the profile item \"uid:{}\"", uid_str))
if let Some(items) = self.items.as_ref() {
for each in items.iter() {
if let Some(uid_val) = &each.uid
&& uid_val.as_str() == uid_str
{
return Ok(each);
}
}
}
bail!("failed to get the profile item \"uid:{}\"", uid_str);
}
/// append new item
/// if the file_data is some
/// then should save the data to file
pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> {
anyhow::ensure!(item.uid.is_some(), "the uid should not be null");
let uid = &item.uid;
if uid.is_none() {
bail!("the uid should not be null");
}
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
anyhow::ensure!(item.file.is_some(), "the file should not be null");
if item.file.is_none() {
bail!("the file should not be null");
}
let file = item
.file
.as_ref()
.clone()
.ok_or_else(|| anyhow::anyhow!("file field is required when file_data is provided"))?;
let path = dirs::app_profiles_dir()?.join(file.as_str());
@@ -140,116 +153,111 @@ impl IProfiles {
.with_context(|| format!("failed to write to file \"{file}\""))?;
}
if self.current.is_none()
&& let Some(t) = item.itype.as_deref()
&& (t == "remote" || t == "local")
{
self.current = item.uid.to_owned();
if self.current.is_none() && (item.itype == Some("remote".into()) || item.itype == Some("local".into())) {
self.current = uid.to_owned();
}
self.items.get_or_insert_default().push(std::mem::take(item));
if self.items.is_none() {
self.items = Some(vec![]);
}
if let Some(items) = self.items.as_mut() {
items.push(item.to_owned());
}
Ok(())
}
/// reorder items
pub async fn reorder(&mut self, active_id: &str, over_id: &str) -> Result<()> {
if active_id == over_id {
return Ok(());
pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
let mut old_index = None;
let mut new_index = None;
for (i, _) in items.iter().enumerate() {
if items[i].uid.as_ref() == Some(active_id) {
old_index = Some(i);
}
if items[i].uid.as_ref() == Some(over_id) {
new_index = Some(i);
}
}
let Some(items) = self.items.as_mut() else {
return Ok(());
let (old_idx, new_idx) = match (old_index, new_index) {
(Some(old), Some(new)) => (old, new),
_ => return Ok(()),
};
let mut old_idx = None;
let mut new_idx = None;
for (i, item) in items.iter().enumerate() {
if let Some(uid) = item.uid.as_ref() {
if uid == active_id {
old_idx = Some(i);
}
if uid == over_id {
new_idx = Some(i);
}
}
if old_idx.is_some() && new_idx.is_some() {
break;
}
}
if let (Some(old), Some(new)) = (old_idx, new_idx) {
if old < new {
items[old..=new].rotate_left(1);
} else {
items[new..=old].rotate_right(1);
}
return self.save_file().await;
}
Ok(())
let item = items.remove(old_idx);
items.insert(new_idx, item);
self.items = Some(items);
self.save_file().await
}
/// update the item value
pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> {
let items = self
.items
.as_mut()
.ok_or_else(|| anyhow::anyhow!("no profile items found"))?;
let mut items = self.items.take().unwrap_or_default();
let target = items.iter_mut().find(|each| each.uid.as_ref() == Some(uid));
for each in items.iter_mut() {
if each.uid.as_ref() == Some(uid) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
patch!(each, item, file);
patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, extra);
patch!(each, item, updated);
patch!(each, item, option);
if let Some(each) = target {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
patch!(each, item, file);
patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, extra);
patch!(each, item, updated);
patch!(each, item, option);
return self.save_file().await;
self.items = Some(items);
return self.save_file().await;
}
}
self.items = Some(items);
bail!("failed to find the profile item \"uid:{uid}\"")
}
/// be used to update the remote item
/// only patch `updated` `extra` `file_data`
pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> {
let target = self
.items
.get_or_insert_default()
.iter_mut()
.find(|each| each.uid.as_ref() == Some(uid))
.ok_or_else(|| anyhow::anyhow!("Item not found"))?;
if self.items.is_none() {
self.items = Some(vec![]);
}
target.extra = item.extra;
target.updated = item.updated;
target.home = std::mem::take(&mut item.home);
target.option = PrfOption::merge(target.option.as_ref(), item.option.as_ref());
// find the item
let _ = self.get_item(uid)?;
let Some(file_data) = item.file_data.take() else {
return self.save_file().await;
};
if let Some(items) = self.items.as_mut() {
let some_uid = Some(uid.clone());
let file = target
.file
.take()
.or_else(|| item.file.take())
.unwrap_or_else(|| format!("{}.yaml", uid).into());
for each in items.iter_mut() {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
each.home = item.home.to_owned();
each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref());
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
let file = each.file.take();
let file =
file.unwrap_or_else(|| item.file.take().unwrap_or_else(|| format!("{}.yaml", &uid).into()));
let path = dirs::app_profiles_dir()?.join(file.as_str());
// the file must exists
each.file = Some(file.clone());
fs::write(&path, file_data.as_bytes())
.await
.with_context(|| format!("failed to write to file \"{file}\""))?;
let path = dirs::app_profiles_dir()?.join(file.as_str());
target.file = Some(file);
fs::write(&path, file_data.as_bytes())
.await
.with_context(|| format!("failed to write to file \"{file}\""))?;
}
break;
}
}
}
self.save_file().await
}
@@ -257,82 +265,68 @@ impl IProfiles {
/// delete item
/// if delete the current then return true
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> {
let uids_to_remove: HashSet<String> = {
let item = self.get_item(uid)?;
let mut set = HashSet::new();
set.insert(uid.clone());
if let Some(opt) = &item.option {
if let Some(u) = &opt.merge {
set.insert(u.clone());
}
if let Some(u) = &opt.script {
set.insert(u.clone());
}
if let Some(u) = &opt.rules {
set.insert(u.clone());
}
if let Some(u) = &opt.proxies {
set.insert(u.clone());
}
if let Some(u) = &opt.groups {
set.insert(u.clone());
}
}
set
};
let current = self.current.as_ref().unwrap_or(uid);
let current = current.clone();
let item = self.get_item(uid)?;
let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone());
let script_uid = item.option.as_ref().and_then(|e| e.script.clone());
let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone());
let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone());
let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone());
let mut items = self.items.take().unwrap_or_default();
let mut deleted_files = Vec::new();
items.retain_mut(|item| {
if let Some(item_uid) = item.uid.as_ref()
&& uids_to_remove.contains(item_uid)
{
if let Some(file) = item.file.take() {
deleted_files.push(file);
// remove the main item (if exists) and delete its file
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.clone())) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
// remove related extension items (merge, script, rules, proxies, groups)
if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
// delete the original uid
if current == *uid {
self.current = None;
for item in items.iter() {
if item.itype == Some("remote".into()) || item.itype == Some("local".into()) {
self.current = item.uid.clone();
break;
}
return false;
}
true
});
let is_deleting_current = self.current.as_ref() == Some(uid);
if is_deleting_current {
self.current = items
.iter()
.find(|i| i.itype.as_deref() == Some("remote") || i.itype.as_deref() == Some("local"))
.and_then(|i| i.uid.clone());
}
self.items = Some(items);
if let Ok(profile_dir) = dirs::app_profiles_dir() {
for file in deleted_files {
let _ = profile_dir.join(file.as_str()).remove_if_exists().await;
}
}
self.save_file().await?;
Ok(is_deleting_current)
Ok(current == *uid)
}
/// 获取current指向的订阅内容
pub async fn current_mapping(&self) -> Result<Mapping> {
let (Some(current), Some(items)) = (self.current.as_ref(), self.items.as_ref()) else {
return Ok(Mapping::new());
};
let Some(target) = items.iter().find(|e| e.uid.as_ref() == Some(current)) else {
bail!("failed to find the current profile \"uid:{current}\"");
};
let file = target
.file
.as_ref()
.ok_or_else(|| anyhow::anyhow!("failed to get the file field"))?;
let file_path = dirs::app_profiles_dir()?.join(file.as_str());
help::read_mapping(&file_path).await
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let file_path = match item.file.as_ref() {
Some(file) => dirs::app_profiles_dir()?.join(file.as_str()),
None => bail!("failed to get the file field"),
};
return help::read_mapping(&file_path).await;
}
bail!("failed to find the current profile \"uid:{current}\"");
}
_ => Ok(Mapping::new()),
}
}
/// 判断profile是否是current指向的
@@ -342,32 +336,32 @@ impl IProfiles {
/// 获取所有的profiles(uid名称, 是否为 current)
pub fn profiles_preview(&self) -> Option<Vec<IProfilePreview<'_>>> {
let items = self.items.as_ref()?;
let current_uid = self.current.as_ref();
let previews = items
.iter()
.filter_map(|e| {
let uid = e.uid.as_ref()?;
let name = e.name.as_ref()?;
Some(IProfilePreview {
uid,
name,
is_current: current_uid == Some(uid),
self.items.as_ref().map(|items| {
items
.iter()
.filter_map(|e| {
if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) {
let is_current = self.is_current_profile_index(uid);
let preview = IProfilePreview { uid, name, is_current };
Some(preview)
} else {
None
}
})
})
.collect();
Some(previews)
.collect()
})
}
/// 通过 uid 获取名称
pub fn get_name_by_uid(&self, uid: &str) -> Option<&String> {
self.items
.as_ref()?
.iter()
.find(|item| item.uid.as_deref() == Some(uid))
.and_then(|item| item.name.as_ref())
pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> {
if let Some(items) = &self.items {
for item in items {
if item.uid.as_ref() == Some(uid) {
return item.name.as_ref();
}
}
}
None
}
/// 以 app 中的 profile 列表为准,删除不再需要的文件
@@ -454,39 +448,59 @@ impl IProfiles {
/// 获取所有 active profile 关联的文件名
fn get_all_active_files(&self) -> HashSet<&str> {
let mut active_files = HashSet::new();
let items = match &self.items {
Some(i) => i,
None => return active_files,
};
let mut active_files: HashSet<&str> = HashSet::new();
let item_map: HashMap<Option<&str>, &PrfItem> = items.iter().map(|i| (i.uid.as_deref(), i)).collect();
if let Some(items) = &self.items {
for item in items {
// 收集所有类型 profile 的文件
if let Some(file) = &item.file {
active_files.insert(file);
}
for item in items {
if let Some(f) = &item.file {
active_files.insert(f.as_str());
}
let Some(opt) = &item.option else {
continue;
};
let related = [
opt.merge.as_deref(),
opt.script.as_deref(),
opt.rules.as_deref(),
opt.proxies.as_deref(),
opt.groups.as_deref(),
];
for r_uid in related.into_iter().flatten() {
if let Some(r_item) = item_map.get(&Some(r_uid))
&& let Some(f) = &r_item.file
// 对于主 profile 类型remote/local还需要收集其关联的扩展文件
if let Some(itype) = &item.itype
&& (itype == "remote" || itype == "local")
&& let Some(option) = &item.option
{
active_files.insert(f.as_str());
// 收集关联的扩展文件
if let Some(merge_uid) = &option.merge
&& let Ok(merge_item) = self.get_item(merge_uid)
&& let Some(file) = &merge_item.file
{
active_files.insert(file);
}
if let Some(script_uid) = &option.script
&& let Ok(script_item) = self.get_item(script_uid)
&& let Some(file) = &script_item.file
{
active_files.insert(file);
}
if let Some(rules_uid) = &option.rules
&& let Ok(rules_item) = self.get_item(rules_uid)
&& let Some(file) = &rules_item.file
{
active_files.insert(file);
}
if let Some(proxies_uid) = &option.proxies
&& let Ok(proxies_item) = self.get_item(proxies_uid)
&& let Some(file) = &proxies_item.file
{
active_files.insert(file);
}
if let Some(groups_uid) = &option.groups
&& let Ok(groups_item) = self.get_item(groups_uid)
&& let Some(file) = &groups_item.file
{
active_files.insert(file);
}
}
}
}
active_files
}
@@ -501,10 +515,18 @@ impl IProfiles {
// p12345678.yaml (proxies)
// g12345678.yaml (groups)
#[allow(clippy::unwrap_used)]
let re = PROFILE_FILE_RE
.get_or_init(|| Regex::new(r"^(?:[RLmprg][a-zA-Z0-9_-]+\.yaml|s[a-zA-Z0-9_-]+\.js)$").unwrap());
re.is_match(filename)
let patterns = [
r"^[RL][a-zA-Z0-9]+\.yaml$", // Remote/Local profiles
r"^m[a-zA-Z0-9]+\.yaml$", // Merge files
r"^s[a-zA-Z0-9]+\.js$", // Script files
r"^[rpg][a-zA-Z0-9]+\.yaml$", // Rules/Proxies/Groups files
];
patterns.iter().any(|pattern| {
regex::Regex::new(pattern)
.map(|re| re.is_match(filename))
.unwrap_or(false)
})
}
}

View File

@@ -64,7 +64,7 @@ impl CoreManager {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line)
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
let message = CompactString::from(&*String::from_utf8_lossy(&line));
Logger::global().writer_sidecar_log(Level::Error, &message);
CLASH_LOGGER.append_log(message).await;
}

View File

@@ -25,7 +25,6 @@ pub struct TimerTask {
pub last_run: i64, // Timestamp of last execution
}
// TODO 一个 Timer 负责轻量, 一个 Timer 负责订阅更新。当前会生产 N(订阅数量) + 1 个定时任务
pub struct Timer {
/// cron manager
pub delay_timer: Arc<RwLock<DelayTimer>>,

View File

@@ -10,14 +10,13 @@ import { useLockFn } from "ahooks";
import { useCallback, useEffect, useMemo, useReducer } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import useSWR from "swr";
import { useServiceInstaller } from "@/hooks/use-service-installer";
import { useSystemState } from "@/hooks/use-system-state";
import { useUpdate } from "@/hooks/use-update";
import { useVerge } from "@/hooks/use-verge";
import { getSystemInfo } from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
import { checkUpdateSafe as checkUpdate } from "@/services/update";
import { version as appVersion } from "@root/package.json";
import { EnhancedCard } from "./enhanced-card";
@@ -52,6 +51,18 @@ export const SystemInfoCard = () => {
const { isAdminMode, isSidecarMode } = useSystemState();
const { installServiceAndRestartCore } = useServiceInstaller();
// 自动检查更新逻辑
const { checkUpdate: triggerCheckUpdate } = useUpdate(true, {
onSuccess: () => {
const now = Date.now();
localStorage.setItem("last_check_update", now.toString());
dispatchSystemState({
type: "set-last-check-update",
payload: new Date(now).toLocaleString(),
});
},
});
// 系统信息状态
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
osInfo: "",
@@ -109,7 +120,7 @@ export const SystemInfoCard = () => {
timeoutId = window.setTimeout(() => {
if (verge?.auto_check_update) {
checkUpdate().catch(console.error);
triggerCheckUpdate().catch(console.error);
}
}, 5000);
}
@@ -118,26 +129,7 @@ export const SystemInfoCard = () => {
window.clearTimeout(timeoutId);
}
};
}, [verge?.auto_check_update, dispatchSystemState]);
// 自动检查更新逻辑
useSWR(
verge?.auto_check_update ? "checkUpdate" : null,
async () => {
const now = Date.now();
localStorage.setItem("last_check_update", now.toString());
dispatchSystemState({
type: "set-last-check-update",
payload: new Date(now).toLocaleString(),
});
return await checkUpdate();
},
{
revalidateOnFocus: false,
refreshInterval: 24 * 60 * 60 * 1000, // 每天检查一次
dedupingInterval: 60 * 60 * 1000, // 1小时内不重复检查
},
);
}, [verge?.auto_check_update, dispatchSystemState, triggerCheckUpdate]);
// 导航到设置页面
const goToSettings = useCallback(() => {
@@ -164,7 +156,7 @@ export const SystemInfoCard = () => {
// 检查更新
const onCheckUpdate = useLockFn(async () => {
try {
const info = await checkUpdate();
const info = await triggerCheckUpdate();
if (!info?.available) {
showNotice.success(
"settings.components.verge.advanced.notifications.latestVersion",

View File

@@ -1,10 +1,8 @@
import { Button } from "@mui/material";
import { useRef } from "react";
import useSWR from "swr";
import { DialogRef } from "@/components/base";
import { useVerge } from "@/hooks/use-verge";
import { checkUpdateSafe } from "@/services/update";
import { useUpdate } from "@/hooks/use-update";
import { UpdateViewer } from "../setting/mods/update-viewer";
@@ -14,20 +12,9 @@ interface Props {
export const UpdateButton = (props: Props) => {
const { className } = props;
const { verge } = useVerge();
const { auto_check_update } = verge || {};
const viewerRef = useRef<DialogRef>(null);
const { data: updateInfo } = useSWR(
auto_check_update || auto_check_update === null ? "checkUpdate" : null,
checkUpdateSafe,
{
errorRetryCount: 2,
revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour
},
);
const { updateInfo } = useUpdate();
if (!updateInfo?.available) return null;

View File

@@ -1,9 +1,8 @@
import { useEffect, useMemo } from "react";
import useSWR from "swr";
import { useRuntimeConfig } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { getRuntimeConfig } from "@/services/cmds";
import delayManager from "@/services/delay";
import { debugLog } from "@/utils/debug";
@@ -107,14 +106,7 @@ export const useRenderList = (
const latencyTimeout = verge?.default_latency_timeout;
// 获取运行时配置用于链式代理模式
const { data: runtimeConfig } = useSWR(
isChainMode ? "getRuntimeConfig" : null,
getRuntimeConfig,
{
revalidateOnFocus: false,
revalidateIfStale: true,
},
);
const { data: runtimeConfig } = useRuntimeConfig(!!isChainMode);
// 计算列数
const col = useMemo(

View File

@@ -4,10 +4,9 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import type { Ref } from "react";
import { useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { BaseDialog, DialogRef } from "@/components/base";
import { getNetworkInterfacesInfo } from "@/services/cmds";
import { useNetworkInterfaces } from "@/hooks/use-network";
import { showNotice } from "@/services/notice-service";
export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
@@ -22,13 +21,7 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
close: () => setOpen(false),
}));
const { data: networkInterfaces } = useSWR(
"clash-verge-rev-internal://network-interfaces",
getNetworkInterfacesInfo,
{
fallbackData: [], // default data before fetch
},
);
const { networkInterfaces } = useNetworkInterfaces();
return (
<BaseDialog

View File

@@ -8,13 +8,12 @@ import { useImperativeHandle, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import useSWR from "swr";
import { BaseDialog, DialogRef } from "@/components/base";
import { useUpdate } from "@/hooks/use-update";
import { portableFlag } from "@/pages/_layout";
import { showNotice } from "@/services/notice-service";
import { useSetUpdateState, useUpdateState } from "@/services/states";
import { checkUpdateSafe as checkUpdate } from "@/services/update";
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation();
@@ -23,11 +22,7 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
const updateState = useUpdateState();
const setUpdateState = useSetUpdateState();
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
errorRetryCount: 2,
revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour
});
const { updateInfo } = useUpdate();
const [downloaded, setDownloaded] = useState(0);
const [total, setTotal] = useState(0);

View File

@@ -51,11 +51,12 @@ const validatePorts = (patch: ClashInfoPatch) => {
});
};
export const useRuntimeConfig = (shouldFetch: boolean = true) => {
return useSWR(shouldFetch ? "getRuntimeConfig" : null, getRuntimeConfig);
};
export const useClash = () => {
const { data: clash, mutate: mutateClash } = useSWR(
"getRuntimeConfig",
getRuntimeConfig,
);
const { data: clash, mutate: mutateClash } = useRuntimeConfig();
const { data: versionData, mutate: mutateVersion } = useSWR(
"getVersion",

22
src/hooks/use-network.ts Normal file
View File

@@ -0,0 +1,22 @@
import useSWR from "swr";
import { getNetworkInterfacesInfo } from "@/services/cmds";
export const useNetworkInterfaces = () => {
const { data, error, isLoading, mutate } = useSWR(
"getNetworkInterfacesInfo",
getNetworkInterfacesInfo,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
fallbackData: [],
},
);
return {
networkInterfaces: data || [],
loading: isLoading,
error,
mutate,
};
};

46
src/hooks/use-update.ts Normal file
View File

@@ -0,0 +1,46 @@
import useSWR, { SWRConfiguration } from "swr";
import { checkUpdateSafe } from "@/services/update";
import { useVerge } from "./use-verge";
export interface UpdateInfo {
version: string;
body: string;
date: string;
available: boolean;
downloadAndInstall: (onEvent?: any) => Promise<void>;
}
export const useUpdate = (
enabled: boolean = true,
options?: SWRConfiguration,
) => {
const { verge } = useVerge();
const { auto_check_update } = verge || {};
// Determine if we should check for updates
// If enabled is explicitly false, don't check
// Otherwise, respect the auto_check_update setting (or default to true if null/undefined for manual triggers)
const shouldCheck = enabled && auto_check_update !== false;
const {
data: updateInfo,
mutate: checkUpdate,
isValidating,
} = useSWR(shouldCheck ? "checkUpdate" : null, checkUpdateSafe, {
errorRetryCount: 2,
revalidateIfStale: false,
revalidateOnFocus: false,
focusThrottleInterval: 36e5, // 1 hour
refreshInterval: 24 * 60 * 60 * 1000, // 24 hours
dedupingInterval: 60 * 60 * 1000, // 1 hour
...options,
});
return {
updateInfo,
checkUpdate,
loading: isValidating,
};
};

View File

@@ -15,7 +15,7 @@ import {
getRunningMode,
getSystemProxy,
} from "@/services/cmds";
import { SWR_DEFAULTS, SWR_MIHOMO, SWR_REALTIME } from "@/services/config";
import { SWR_DEFAULTS, SWR_MIHOMO } from "@/services/config";
import { AppDataContext, AppDataContextType } from "./app-data-context";
@@ -30,14 +30,7 @@ export const AppDataProvider = ({
const { data: proxiesData, mutate: refreshProxy } = useSWR(
"getProxies",
calcuProxies,
{
...SWR_REALTIME,
onError: (_) => {
// FIXME when we intially start the app, and core is starting,
// there will be error thrown by getProxies API.
// We should handle this case properly later.
},
},
SWR_MIHOMO,
);
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(

View File

@@ -16,12 +16,6 @@ export const SWR_DEFAULTS = {
dedupingInterval: 5000,
} as const;
export const SWR_REALTIME = {
...SWR_DEFAULTS,
refreshInterval: 8000,
dedupingInterval: 3000,
} as const;
export const SWR_SLOW_POLL = {
...SWR_DEFAULTS,
refreshInterval: 60000,
@@ -29,4 +23,6 @@ export const SWR_SLOW_POLL = {
export const SWR_MIHOMO = {
...SWR_NOT_SMART,
errorRetryInterval: 500,
errorRetryCount: 15,
};