mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 07:14:40 +08:00
Compare commits
61 Commits
feat!/depr
...
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 |
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
|
||||
|
||||
53
.github/workflows/autobuild.yml
vendored
53
.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
|
||||
@@ -196,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
|
||||
@@ -253,10 +253,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
# 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
|
||||
- os: ubuntu-24.04
|
||||
- os: ubuntu-22.04
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
arch: armhf
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -292,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
|
||||
@@ -311,33 +313,30 @@ 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/
|
||||
|
||||
sudo rm -f /etc/apt/sources.list.d/ubuntu.sources
|
||||
sudo tee /etc/apt/sources.list << EOF
|
||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble main restricted universe multiverse
|
||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble-security main restricted universe multiverse
|
||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble-updates main restricted universe multiverse
|
||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble-backports main restricted universe multiverse
|
||||
cat > /tmp/sources.list << EOF
|
||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
|
||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
|
||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
|
||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
|
||||
|
||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
|
||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse
|
||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse
|
||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse
|
||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
|
||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
|
||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
|
||||
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
|
||||
EOF
|
||||
|
||||
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
|
||||
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 \
|
||||
libxslt1-dev:${{ matrix.arch }} \
|
||||
sudo apt install -y \
|
||||
libxslt1.1:${{ matrix.arch }} \
|
||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||
libssl-dev:${{ matrix.arch }} \
|
||||
@@ -440,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
|
||||
@@ -542,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
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
1250
Cargo.lock
generated
1250
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ members = [
|
||||
"crates/clash-verge-logging",
|
||||
"crates/clash-verge-signal",
|
||||
"crates/tauri-plugin-clash-verge-sysinfo",
|
||||
"crates/clash-verge-types",
|
||||
"crates/clash-verge-i18n",
|
||||
]
|
||||
resolver = "2"
|
||||
@@ -44,7 +43,6 @@ 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" }
|
||||
|
||||
@@ -59,14 +57,14 @@ tokio = { version = "1.49.0", features = [
|
||||
"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.148" }
|
||||
serde_json = { version = "1.0.149" }
|
||||
serde_yaml_ng = { version = "0.10.0" }
|
||||
bitflags = { version = "2.10.0" }
|
||||
|
||||
|
||||
37
Changelog.md
37
Changelog.md
@@ -1,44 +1,23 @@
|
||||
## v2.4.5
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.18**
|
||||
## v(2.4.6)
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复 macOS 有线网络 DNS 劫持失败
|
||||
- 修复 Monaco 编辑器内右键菜单显示异常
|
||||
- 修复设置代理端口时检查端口占用
|
||||
- 修复 Monaco 编辑器初始化卡 Loading
|
||||
- 修复恢复备份时 `config.yaml` / `profiles.yaml` 文件内字段未正确恢复
|
||||
- 修复 Windows 下系统主题同步问题
|
||||
- 修复 URL Schemes 无法正常导入
|
||||
- 修复首次启动时代理信息刷新缓慢
|
||||
- 修复无网络时无限请求 IP 归属查询
|
||||
- 修复 WebDAV 页面重试逻辑
|
||||
- 修复 Linux 通过 GUI 安装服务模式权限不符合预期
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 允许代理页面允许高级过滤搜索
|
||||
- 备份设置页面新增导入备份按钮
|
||||
- 允许修改通知弹窗位置
|
||||
- 支持收起导航栏(导航栏右键菜单 / 界面设置)
|
||||
- 允许将出站模式显示在托盘一级菜单
|
||||
- 允许禁用在托盘中显示代理组
|
||||
- 支持在「编辑节点」中直接导入 AnyTLS URI 配置
|
||||
- 支持关闭「验证代理绕过格式」
|
||||
- 新增系统代理绕过的可视化编辑器
|
||||
- 支持订阅设置自动延时监测间隔
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 应用内更新日志支持解析并渲染 HTML 标签
|
||||
- 性能优化前后端在渲染流量图时的资源
|
||||
- 在 Linux NVIDIA 显卡环境下尝试禁用 WebKit DMABUF 渲染以规避潜在问题
|
||||
- Windows 下自启动改为计划任务实现
|
||||
- 改进托盘和窗口操作频率限制实现
|
||||
- 使用「编辑节点」添加节点时,自动将节点添加到第一个 `select` 类型的代理组的第一位
|
||||
- 隐藏侧边导航栏和悬浮跳转导航的滚动条
|
||||
- 完善对 AnyTLS / Mieru / Sudoku 的 GUI 支持
|
||||
- macOS 和 Linux 对服务 IPC 权限进一步限制
|
||||
- 移除 Windows 自启动计划任务中冗余的 3 秒延时
|
||||
- 后端性能优化
|
||||
- 前端性能优化
|
||||
|
||||
</details>
|
||||
|
||||
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 框架
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
title: لوحة التحكم
|
||||
body: تم تحديث حالة عرض لوحة التحكم.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
title: تبديل الوضع
|
||||
body: تم التبديل إلى {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
title: وكيل النظام
|
||||
body: تم تحديث حالة وكيل النظام.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
title: وضع TUN
|
||||
body: تم تحديث حالة وضع TUN.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
title: الوضع الخفيف
|
||||
body: تم الدخول إلى الوضع الخفيف.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: الملفات التعريفية
|
||||
body: تمت إعادة تفعيل الملف التعريفي.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
title: على وشك الخروج
|
||||
body: Clash Verge على وشك الخروج.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
title: تم إخفاء التطبيق
|
||||
body: Clash Verge يعمل في الخلفية.
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
adminInstallPrompt: يتطلب تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||
adminUninstallPrompt: يتطلب إلغاء تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||
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
|
||||
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: System Proxy
|
||||
systemProxy: وكيل النظام
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
profile: ملف تعريفي
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
title: Übersicht
|
||||
body: Die Sichtbarkeit der Übersicht wurde aktualisiert.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
title: Moduswechsel
|
||||
body: Auf {mode} umgeschaltet.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
title: Systemproxy
|
||||
body: Der Status des Systemproxys wurde aktualisiert.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
title: TUN-Modus
|
||||
body: Der Status des TUN-Modus wurde aktualisiert.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
title: Leichtmodus
|
||||
body: Leichtmodus aktiviert.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: Profile
|
||||
body: Profil reaktiviert.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
title: Beenden steht bevor
|
||||
body: Clash Verge wird gleich beendet.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
title: Anwendung ausgeblendet
|
||||
body: Clash Verge läuft im Hintergrund.
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
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: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
dashboard: Übersicht
|
||||
ruleMode: Regelmodus
|
||||
globalMode: Globaler Modus
|
||||
directMode: Direktmodus
|
||||
outboundModes: Ausgangsmodi
|
||||
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
|
||||
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: System Proxy
|
||||
systemProxy: Systemproxy
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
profile: Profil
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
title: Panel
|
||||
body: La visibilidad del panel se ha actualizado.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
title: Cambio de modo
|
||||
body: Cambiado a {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
title: Proxy del sistema
|
||||
body: El estado del proxy del sistema se ha actualizado.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
title: Modo TUN
|
||||
body: El estado del modo TUN se ha actualizado.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
title: Modo ligero
|
||||
body: Se ha entrado en el modo ligero.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: Perfiles
|
||||
body: Perfil reactivado.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
title: A punto de salir
|
||||
body: Clash Verge está a punto de salir.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
title: Aplicación oculta
|
||||
body: Clash Verge se está ejecutando en segundo plano.
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
adminInstallPrompt: Instalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||
adminUninstallPrompt: Desinstalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
dashboard: Panel
|
||||
ruleMode: Modo de reglas
|
||||
globalMode: Modo global
|
||||
directMode: Modo directo
|
||||
outboundModes: Modos de salida
|
||||
rule: Regla
|
||||
direct: Directo
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
profiles: Perfiles
|
||||
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
|
||||
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: System Proxy
|
||||
systemProxy: Proxy del sistema
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
profile: Perfil
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
title: داشبورد
|
||||
body: وضعیت نمایش داشبورد بهروزرسانی شد.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
title: تغییر حالت
|
||||
body: به {mode} تغییر کرد.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
title: پروکسی سیستم
|
||||
body: وضعیت پروکسی سیستم بهروزرسانی شد.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
title: حالت TUN
|
||||
body: وضعیت حالت TUN بهروزرسانی شد.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
title: حالت سبک
|
||||
body: به حالت سبک وارد شد.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: پروفایلها
|
||||
body: پروفایل دوباره فعال شد.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
title: در آستانه خروج
|
||||
body: Clash Verge در آستانه خروج است.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
title: برنامه پنهان شد
|
||||
body: Clash Verge در پسزمینه در حال اجراست.
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
adminInstallPrompt: نصب سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||
adminUninstallPrompt: حذف سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||
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
|
||||
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: System Proxy
|
||||
systemProxy: پروکسی سیستم
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
profile: پروفایل
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
title: Dasbor
|
||||
body: Visibilitas dasbor telah diperbarui.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
title: Peralihan Mode
|
||||
body: Beralih ke {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
title: Proksi Sistem
|
||||
body: Status proksi sistem telah diperbarui.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
title: Mode TUN
|
||||
body: Status mode TUN telah diperbarui.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
title: Mode Ringan
|
||||
body: Masuk ke mode ringan.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: Profil
|
||||
body: Profil diaktifkan kembali.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
title: Akan Keluar
|
||||
body: Clash Verge akan keluar.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
title: Aplikasi Disembunyikan
|
||||
body: Clash Verge berjalan di latar belakang.
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
adminInstallPrompt: Menginstal layanan Clash Verge memerlukan hak administrator.
|
||||
adminUninstallPrompt: Menghapus instalasi layanan Clash Verge memerlukan hak administrator.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
dashboard: Dasbor
|
||||
ruleMode: Mode Aturan
|
||||
globalMode: Mode Global
|
||||
directMode: Mode Langsung
|
||||
outboundModes: Mode Keluar
|
||||
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
|
||||
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: System Proxy
|
||||
systemProxy: Proksi Sistem
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
profile: Profil
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
title: ダッシュボード
|
||||
body: ダッシュボードの表示状態が更新されました。
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
title: モード切り替え
|
||||
body: "{mode} に切り替えました。"
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
title: システムプロキシ
|
||||
body: システムプロキシの状態が更新されました。
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
title: TUN モード
|
||||
body: TUN モードの状態が更新されました。
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
title: 軽量モード
|
||||
body: 軽量モードに入りました。
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: プロファイル
|
||||
body: プロファイルが再有効化されました。
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
title: 終了間近
|
||||
body: Clash Verge はまもなく終了します。
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
title: アプリが非表示
|
||||
body: Clash Verge はバックグラウンドで実行中です。
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
adminInstallPrompt: Clash Verge サービスのインストールには管理者権限が必要です。
|
||||
adminUninstallPrompt: Clash Verge サービスのアンインストールには管理者権限が必要です。
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
dashboard: ダッシュボード
|
||||
ruleMode: ルールモード
|
||||
globalMode: グローバルモード
|
||||
directMode: ダイレクトモード
|
||||
outboundModes: アウトバウンドモード
|
||||
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
|
||||
profiles: プロファイル
|
||||
proxies: プロキシ
|
||||
systemProxy: システムプロキシ
|
||||
tunMode: TUN モード
|
||||
closeAllConnections: すべての接続を閉じる
|
||||
lightweightMode: 軽量モード
|
||||
copyEnv: 環境変数をコピー
|
||||
confDir: 設定ディレクトリ
|
||||
coreDir: コアディレクトリ
|
||||
logsDir: ログディレクトリ
|
||||
openDir: ディレクトリを開く
|
||||
appLog: アプリケーションログ
|
||||
coreLog: コアログ
|
||||
restartClash: Clash コアを再起動
|
||||
restartApp: アプリケーションを再起動
|
||||
vergeVersion: Verge バージョン
|
||||
more: その他
|
||||
exit: 終了
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
systemProxy: システムプロキシ
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
profile: プロファイル
|
||||
|
||||
@@ -16,8 +16,8 @@ notifications:
|
||||
title: 경량 모드
|
||||
body: 경량 모드에 진입했습니다.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: 프로필
|
||||
body: 프로필이 다시 활성화되었습니다.
|
||||
appQuit:
|
||||
title: 곧 종료
|
||||
body: Clash Verge가 곧 종료됩니다.
|
||||
@@ -25,14 +25,14 @@ notifications:
|
||||
title: 앱이 숨겨짐
|
||||
body: Clash Verge가 백그라운드에서 실행 중입니다.
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
adminInstallPrompt: Clash Verge 서비스 설치에는 관리자 권한이 필요합니다.
|
||||
adminUninstallPrompt: Clash Verge 서비스 제거에는 관리자 권한이 필요합니다.
|
||||
tray:
|
||||
dashboard: 대시보드
|
||||
ruleMode: 규칙 모드
|
||||
globalMode: 전역 모드
|
||||
directMode: 직접 모드
|
||||
outboundModes: Outbound Modes
|
||||
outboundModes: 아웃바운드 모드
|
||||
rule: 규칙
|
||||
direct: 직접
|
||||
global: 글로벌
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
title: Панель
|
||||
body: Видимость панели обновлена.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
title: Смена режима
|
||||
body: Переключено на {mode}.
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
title: Системный прокси
|
||||
body: Статус системного прокси обновлен.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
title: Режим TUN
|
||||
body: Статус режима TUN обновлен.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
title: Легкий режим
|
||||
body: Включен легкий режим.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: Профили
|
||||
body: Профиль повторно активирован.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
title: Скорый выход
|
||||
body: Clash Verge скоро завершит работу.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
title: Приложение скрыто
|
||||
body: Clash Verge работает в фоновом режиме.
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
adminInstallPrompt: Для установки службы Clash Verge требуются права администратора.
|
||||
adminUninstallPrompt: Для удаления службы Clash Verge требуются права администратора.
|
||||
tray:
|
||||
dashboard: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
dashboard: Панель
|
||||
ruleMode: Режим правил
|
||||
globalMode: Глобальный режим
|
||||
directMode: Прямой режим
|
||||
outboundModes: Исходящие режимы
|
||||
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
|
||||
profiles: Профили
|
||||
proxies: Прокси
|
||||
systemProxy: Системный прокси
|
||||
tunMode: Режим TUN
|
||||
closeAllConnections: Закрыть все соединения
|
||||
lightweightMode: Легкий режим
|
||||
copyEnv: Копировать переменные среды
|
||||
confDir: Каталог конфигурации
|
||||
coreDir: Каталог ядра
|
||||
logsDir: Каталог журналов
|
||||
openDir: Открыть каталог
|
||||
appLog: Журнал приложения
|
||||
coreLog: Журнал ядра
|
||||
restartClash: Перезапустить ядро Clash
|
||||
restartApp: Перезапустить приложение
|
||||
vergeVersion: Версия Verge
|
||||
more: Еще
|
||||
exit: Выход
|
||||
tooltip:
|
||||
systemProxy: System Proxy
|
||||
systemProxy: Системный прокси
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
profile: Профиль
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
title: Gösterge Paneli
|
||||
body: Gösterge panelinin görünürlüğü güncellendi.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
title: Mod Değişimi
|
||||
body: "{mode} moduna geçildi."
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
title: Sistem Vekil'i
|
||||
body: Sistem vekil'i durumu güncellendi.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
title: TUN Modu
|
||||
body: TUN modu durumu güncellendi.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
title: Hafif Mod
|
||||
body: Hafif moda geçildi.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: Profiller
|
||||
body: Profil yeniden etkinleştirildi.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
title: Çıkış Yapılmak Üzere
|
||||
body: Clash Verge kapanmak üzere.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
title: Uygulama Gizlendi
|
||||
body: Clash Verge arka planda çalışıyor.
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
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: Dashboard
|
||||
ruleMode: Rule Mode
|
||||
globalMode: Global Mode
|
||||
directMode: Direct Mode
|
||||
outboundModes: Outbound Modes
|
||||
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: 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
|
||||
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: System Proxy
|
||||
systemProxy: Sistem Vekil'i
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
profile: Profil
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Dashboard
|
||||
body: Dashboard visibility has been updated.
|
||||
title: Идарә панеле
|
||||
body: Идарә панеленең күренеше яңартылды.
|
||||
clashModeChanged:
|
||||
title: Mode Switch
|
||||
body: Switched to {mode}.
|
||||
title: Режим алыштыру
|
||||
body: "{mode} режимына күчтел."
|
||||
systemProxyToggled:
|
||||
title: System Proxy
|
||||
body: System proxy status has been updated.
|
||||
title: Системалы прокси
|
||||
body: Системалы прокси хәле яңартылды.
|
||||
tunModeToggled:
|
||||
title: TUN Mode
|
||||
body: TUN mode status has been updated.
|
||||
title: TUN режимы
|
||||
body: TUN режимы хәле яңартылды.
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
title: Җиңел режим
|
||||
body: Җиңел режимга күчелде.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
title: Профильләр
|
||||
body: Профиль яңадан активлаштырылды.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
title: Чыгар алдыннан
|
||||
body: Clash Verge чыгарга җыена.
|
||||
appHidden:
|
||||
title: Application Hidden
|
||||
body: Clash Verge is running in the background.
|
||||
title: Кушымта яшерелде
|
||||
body: Clash Verge фон режимында эшли.
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
adminInstallPrompt: Clash Verge хезмәтен урнаштыру өчен администратор хокуклары кирәк.
|
||||
adminUninstallPrompt: Clash Verge хезмәтен бетерү өчен администратор хокуклары кирәк.
|
||||
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
|
||||
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: System Proxy
|
||||
systemProxy: Системалы прокси
|
||||
tun: TUN
|
||||
profile: Profile
|
||||
profile: Профиль
|
||||
|
||||
@@ -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.179"
|
||||
libc = "0.2.180"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
deelevate = { workspace = true }
|
||||
|
||||
@@ -120,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();
|
||||
|
||||
@@ -1,3 +1,57 @@
|
||||
## 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**
|
||||
|
||||
@@ -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
|
||||
|
||||
58
package.json
58
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.6",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"prepare": "husky || true",
|
||||
@@ -41,36 +41,36 @@
|
||||
"@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.16",
|
||||
"@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.2",
|
||||
"react-hook-form": "^7.70.0",
|
||||
"react-i18next": "16.5.1",
|
||||
"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-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": "^6.0.1",
|
||||
"@eslint-react/eslint-plugin": "^2.5.1",
|
||||
"@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",
|
||||
@@ -97,25 +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": "^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.7.4",
|
||||
"sass": "^1.97.1",
|
||||
"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.51.0",
|
||||
"vite": "^7.3.0",
|
||||
"typescript-eslint": "^8.53.1",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -128,7 +128,7 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
2681
pnpm-lock.yaml
generated
2681
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -49,8 +49,7 @@
|
||||
"ignoreDeps": ["criterion"],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"description": "Force update Cargo.lock to track latest commits of git dependencies",
|
||||
"automerge": true,
|
||||
"description": "Force update lockfile to track latest commits of git dependencies",
|
||||
"schedule": ["before 5am on monday"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.4.5"
|
||||
version = "2.4.6"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -34,7 +34,6 @@ 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 }
|
||||
@@ -60,7 +59,7 @@ 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"] }
|
||||
delay_timer = "0.11.6"
|
||||
@@ -72,18 +71,18 @@ reqwest = { version = "0.13.1", features = [
|
||||
"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.5", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.3.3"
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-fs = "2.4.4"
|
||||
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 = "7.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"
|
||||
@@ -93,18 +92,19 @@ scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
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.28", 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"
|
||||
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]
|
||||
|
||||
@@ -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;
|
||||
@@ -174,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::*};
|
||||
|
||||
@@ -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 {
|
||||
@@ -365,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 的文件名集合
|
||||
@@ -384,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() {
|
||||
@@ -410,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());
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -433,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)]
|
||||
@@ -136,13 +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
|
||||
})
|
||||
}
|
||||
@@ -155,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>,
|
||||
|
||||
@@ -523,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);
|
||||
|
||||
@@ -14,7 +14,6 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tauri_plugin_http::reqwest as tauri_reqwest;
|
||||
use tokio::{fs, time::timeout};
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
@@ -111,12 +110,12 @@ impl WebDavClient {
|
||||
// 创建新的客户端
|
||||
let client = reqwest_dav::ClientBuilder::new()
|
||||
.set_agent(
|
||||
tauri_reqwest::Client::builder()
|
||||
reqwest::Client::builder()
|
||||
.use_rustls_tls()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(op.timeout()))
|
||||
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
|
||||
.redirect(tauri_reqwest::redirect::Policy::custom(|attempt| {
|
||||
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
||||
// 允许所有请求类型的重定向,包括PUT
|
||||
if attempt.previous().len() >= 5 {
|
||||
attempt.error("重定向次数过多")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -256,9 +256,11 @@ fn install_service() -> Result<()> {
|
||||
|
||||
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
|
||||
|
||||
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 '{install_shell}'" with administrator privileges with prompt "{prompt}""#);
|
||||
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()?;
|
||||
|
||||
@@ -381,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?;
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -19,7 +19,6 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Manager as _};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
@@ -61,11 +60,11 @@ mod app_init {
|
||||
.socket_path(crate::config::IClashTemp::guard_external_controller_ipc())
|
||||
.pool_config(
|
||||
tauri_plugin_mihomo::IpcPoolConfigBuilder::new()
|
||||
.min_connections(1)
|
||||
.min_connections(3)
|
||||
.max_connections(32)
|
||||
.idle_timeout(std::time::Duration::from_secs(60))
|
||||
.health_check_interval(std::time::Duration::from_secs(60))
|
||||
.reject_policy(RejectPolicy::Timeout(Duration::from_secs(3)))
|
||||
.reject_policy(RejectPolicy::Wait)
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.6",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"targets": ["deb", "rpm"],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["openssl"],
|
||||
"depends": ["openssl", "libayatana-appindicator3-1"],
|
||||
"desktopTemplate": "./packages/linux/clash-verge.desktop",
|
||||
"provides": ["clash-verge"],
|
||||
"conflicts": ["clash-verge"],
|
||||
@@ -14,7 +14,7 @@
|
||||
"preRemoveScript": "./packages/linux/pre-remove.sh"
|
||||
},
|
||||
"rpm": {
|
||||
"depends": ["openssl"],
|
||||
"depends": ["openssl", "libayatana-appindicator-gtk3"],
|
||||
"desktopTemplate": "./packages/linux/clash-verge.desktop",
|
||||
"provides": ["clash-verge"],
|
||||
"conflicts": ["clash-verge"],
|
||||
|
||||
@@ -2,15 +2,18 @@ import { ReactNode } from "react";
|
||||
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
||||
|
||||
function ErrorFallback({ error }: FallbackProps) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
|
||||
return (
|
||||
<div role="alert" style={{ padding: 16 }}>
|
||||
<h4>Something went wrong:(</h4>
|
||||
|
||||
<pre>{error.message}</pre>
|
||||
<pre>{errorMessage}</pre>
|
||||
|
||||
<details title="Error Stack">
|
||||
<summary>Error Stack</summary>
|
||||
<pre>{error.stack}</pre>
|
||||
<pre>{errorStack}</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
export { BaseDialog, type DialogRef } from "./base-dialog";
|
||||
export { BasePage } from "./base-page";
|
||||
export { BaseEmpty } from "./base-empty";
|
||||
export { BaseLoading } from "./base-loading";
|
||||
export { BaseErrorBoundary } from "./base-error-boundary";
|
||||
export { BaseSplitChipEditor } from "./base-split-chip-editor";
|
||||
export { Switch } from "./base-switch";
|
||||
export { BaseFieldset } from "./base-fieldset";
|
||||
export { BaseLoading } from "./base-loading";
|
||||
export { BaseLoadingOverlay } from "./base-loading-overlay";
|
||||
export { BasePage } from "./base-page";
|
||||
export { BaseSearchBox, type SearchState } from "./base-search-box";
|
||||
export {
|
||||
BaseSplitChipEditor,
|
||||
type BaseSplitChipEditorMode,
|
||||
} from "./base-split-chip-editor";
|
||||
export { BaseStyledSelect } from "./base-styled-select";
|
||||
export { BaseStyledTextField } from "./base-styled-text-field";
|
||||
export { Switch } from "./base-switch";
|
||||
export { TooltipIcon } from "./base-tooltip-icon";
|
||||
|
||||
@@ -21,20 +21,14 @@ import {
|
||||
ListItem,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import type { Column } from "@tanstack/react-table";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ColumnOption {
|
||||
field: string;
|
||||
label: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
columns: ColumnOption[];
|
||||
columns: Column<IConnectionsItem, unknown>[];
|
||||
onClose: () => void;
|
||||
onToggle: (field: string, visible: boolean) => void;
|
||||
onOrderChange: (order: string[]) => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
@@ -43,7 +37,6 @@ export const ConnectionColumnManager = ({
|
||||
open,
|
||||
columns,
|
||||
onClose,
|
||||
onToggle,
|
||||
onOrderChange,
|
||||
onReset,
|
||||
}: Props) => {
|
||||
@@ -54,9 +47,9 @@ export const ConnectionColumnManager = ({
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items = useMemo(() => columns.map((column) => column.field), [columns]);
|
||||
const items = useMemo(() => columns.map((column) => column.id), [columns]);
|
||||
const visibleCount = useMemo(
|
||||
() => columns.filter((column) => column.visible).length,
|
||||
() => columns.filter((column) => column.getIsVisible()).length,
|
||||
[columns],
|
||||
);
|
||||
|
||||
@@ -65,7 +58,7 @@ export const ConnectionColumnManager = ({
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
const order = columns.map((column) => column.field);
|
||||
const order = columns.map((column) => column.id);
|
||||
const oldIndex = order.indexOf(active.id as string);
|
||||
const newIndex = order.indexOf(over.id as string);
|
||||
if (oldIndex === -1 || newIndex === -1) return;
|
||||
@@ -94,13 +87,16 @@ export const ConnectionColumnManager = ({
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<SortableColumnItem
|
||||
key={column.field}
|
||||
key={column.id}
|
||||
column={column}
|
||||
onToggle={onToggle}
|
||||
label={getColumnLabel(column)}
|
||||
dragHandleLabel={t(
|
||||
"connections.components.columnManager.dragHandle",
|
||||
)}
|
||||
disableToggle={column.visible && visibleCount <= 1}
|
||||
disableToggle={
|
||||
!column.getCanHide() ||
|
||||
(column.getIsVisible() && visibleCount <= 1)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
@@ -120,15 +116,15 @@ export const ConnectionColumnManager = ({
|
||||
};
|
||||
|
||||
interface SortableColumnItemProps {
|
||||
column: ColumnOption;
|
||||
onToggle: (field: string, visible: boolean) => void;
|
||||
column: Column<IConnectionsItem, unknown>;
|
||||
label: string;
|
||||
dragHandleLabel: string;
|
||||
disableToggle?: boolean;
|
||||
}
|
||||
|
||||
const SortableColumnItem = ({
|
||||
column,
|
||||
onToggle,
|
||||
label,
|
||||
dragHandleLabel,
|
||||
disableToggle = false,
|
||||
}: SortableColumnItemProps) => {
|
||||
@@ -139,7 +135,7 @@ const SortableColumnItem = ({
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: column.field });
|
||||
} = useSortable({ id: column.id });
|
||||
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
@@ -167,12 +163,12 @@ const SortableColumnItem = ({
|
||||
>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
checked={column.visible}
|
||||
checked={column.getIsVisible()}
|
||||
disabled={disableToggle}
|
||||
onChange={(event) => onToggle(column.field, event.target.checked)}
|
||||
onChange={(event) => column.toggleVisibility(event.target.checked)}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={column.label}
|
||||
primary={label}
|
||||
slotProps={{ primary: { variant: "body2" } }}
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
@@ -189,3 +185,11 @@ const SortableColumnItem = ({
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
const getColumnLabel = (column: Column<IConnectionsItem, unknown>) => {
|
||||
const meta = column.columnDef.meta as { label?: string } | undefined;
|
||||
if (meta?.label) return meta.label;
|
||||
|
||||
const header = column.columnDef.header;
|
||||
return typeof header === "string" ? header : column.id;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ViewColumnRounded } from "@mui/icons-material";
|
||||
import { Box, IconButton, Tooltip } from "@mui/material";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnOrderState,
|
||||
ColumnSizingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
@@ -43,50 +44,57 @@ const reconcileColumnOrder = (
|
||||
return [...filtered, ...missing];
|
||||
};
|
||||
|
||||
const createConnectionRow = (each: IConnectionsItem) => {
|
||||
type ColumnField =
|
||||
| "host"
|
||||
| "download"
|
||||
| "upload"
|
||||
| "dlSpeed"
|
||||
| "ulSpeed"
|
||||
| "chains"
|
||||
| "rule"
|
||||
| "process"
|
||||
| "time"
|
||||
| "source"
|
||||
| "remoteDestination"
|
||||
| "type";
|
||||
|
||||
const getConnectionCellValue = (field: ColumnField, each: IConnectionsItem) => {
|
||||
const { metadata, rulePayload } = each;
|
||||
const chains = [...each.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
||||
const destination = metadata.destinationIP
|
||||
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||
|
||||
return {
|
||||
id: each.id,
|
||||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
ulSpeed: each.curUpload,
|
||||
chains,
|
||||
rule,
|
||||
process: truncateStr(metadata.process || metadata.processPath),
|
||||
time: each.start,
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
remoteDestination: destination,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
connectionData: each,
|
||||
};
|
||||
switch (field) {
|
||||
case "host":
|
||||
return metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||
case "download":
|
||||
return each.download;
|
||||
case "upload":
|
||||
return each.upload;
|
||||
case "dlSpeed":
|
||||
return each.curDownload;
|
||||
case "ulSpeed":
|
||||
return each.curUpload;
|
||||
case "chains":
|
||||
return [...each.chains].reverse().join(" / ");
|
||||
case "rule":
|
||||
return rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
||||
case "process":
|
||||
return truncateStr(metadata.process || metadata.processPath);
|
||||
case "time":
|
||||
return each.start;
|
||||
case "source":
|
||||
return `${metadata.sourceIP}:${metadata.sourcePort}`;
|
||||
case "remoteDestination":
|
||||
return metadata.destinationIP
|
||||
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||
case "type":
|
||||
return `${metadata.type}(${metadata.network})`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
type ConnectionRow = ReturnType<typeof createConnectionRow>;
|
||||
|
||||
const areRowsEqual = (a: ConnectionRow, b: ConnectionRow) =>
|
||||
a.host === b.host &&
|
||||
a.download === b.download &&
|
||||
a.upload === b.upload &&
|
||||
a.dlSpeed === b.dlSpeed &&
|
||||
a.ulSpeed === b.ulSpeed &&
|
||||
a.chains === b.chains &&
|
||||
a.rule === b.rule &&
|
||||
a.process === b.process &&
|
||||
a.time === b.time &&
|
||||
a.source === b.source &&
|
||||
a.remoteDestination === b.remoteDestination &&
|
||||
a.type === b.type;
|
||||
|
||||
interface Props {
|
||||
connections: IConnectionsItem[];
|
||||
onShowDetail: (data: IConnectionsItem) => void;
|
||||
@@ -104,33 +112,30 @@ export const ConnectionTable = (props: Props) => {
|
||||
onCloseColumnManager,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const [columnWidths, setColumnWidths] = useLocalStorage<
|
||||
Record<string, number>
|
||||
>(
|
||||
const [columnWidths, setColumnWidths] = useLocalStorage<ColumnSizingState>(
|
||||
"connection-table-widths",
|
||||
// server-side value, this is the default value used by server-side rendering (if any)
|
||||
// Do not omit (otherwise a Suspense boundary will be triggered)
|
||||
{},
|
||||
);
|
||||
|
||||
const [columnVisibilityModel, setColumnVisibilityModel] = useLocalStorage<
|
||||
Partial<Record<string, boolean>>
|
||||
>(
|
||||
"connection-table-visibility",
|
||||
{},
|
||||
{
|
||||
serializer: JSON.stringify,
|
||||
deserializer: (value) => {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed && typeof parsed === "object") return parsed;
|
||||
} catch (err) {
|
||||
console.warn("Failed to parse connection-table-visibility", err);
|
||||
}
|
||||
return {};
|
||||
const [columnVisibilityModel, setColumnVisibilityModel] =
|
||||
useLocalStorage<VisibilityState>(
|
||||
"connection-table-visibility",
|
||||
{},
|
||||
{
|
||||
serializer: JSON.stringify,
|
||||
deserializer: (value) => {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed && typeof parsed === "object") return parsed;
|
||||
} catch (err) {
|
||||
console.warn("Failed to parse connection-table-visibility", err);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
const [columnOrder, setColumnOrder] = useLocalStorage<string[]>(
|
||||
"connection-table-order",
|
||||
@@ -149,15 +154,13 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
type ColumnField = Exclude<keyof ConnectionRow, "connectionData">;
|
||||
|
||||
interface BaseColumn {
|
||||
field: ColumnField;
|
||||
headerName: string;
|
||||
width?: number;
|
||||
minWidth?: number;
|
||||
align?: "left" | "right";
|
||||
cell?: (row: ConnectionRow) => ReactNode;
|
||||
cell?: (row: IConnectionsItem) => ReactNode;
|
||||
}
|
||||
|
||||
const baseColumns = useMemo<BaseColumn[]>(() => {
|
||||
@@ -190,7 +193,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
width: 76,
|
||||
minWidth: 60,
|
||||
align: "right",
|
||||
cell: (row) => `${parseTraffic(row.dlSpeed).join(" ")}/s`,
|
||||
cell: (row) => `${parseTraffic(row.curDownload).join(" ")}/s`,
|
||||
},
|
||||
{
|
||||
field: "ulSpeed",
|
||||
@@ -198,7 +201,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
width: 76,
|
||||
minWidth: 60,
|
||||
align: "right",
|
||||
cell: (row) => `${parseTraffic(row.ulSpeed).join(" ")}/s`,
|
||||
cell: (row) => `${parseTraffic(row.curUpload).join(" ")}/s`,
|
||||
},
|
||||
{
|
||||
field: "chains",
|
||||
@@ -262,177 +265,76 @@ export const ConnectionTable = (props: Props) => {
|
||||
});
|
||||
}, [baseColumns, setColumnOrder]);
|
||||
|
||||
const columns = useMemo<BaseColumn[]>(() => {
|
||||
const order = Array.isArray(columnOrder) ? columnOrder : [];
|
||||
const orderMap = new Map(order.map((field, index) => [field, index]));
|
||||
|
||||
return [...baseColumns].sort((a, b) => {
|
||||
const aIndex = orderMap.has(a.field)
|
||||
? (orderMap.get(a.field) as number)
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
const bIndex = orderMap.has(b.field)
|
||||
? (orderMap.get(b.field) as number)
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
|
||||
if (aIndex === bIndex) {
|
||||
return order.indexOf(a.field) - order.indexOf(b.field);
|
||||
}
|
||||
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
}, [baseColumns, columnOrder]);
|
||||
|
||||
const visibleColumnsCount = useMemo(() => {
|
||||
return columns.reduce((count, column) => {
|
||||
return (columnVisibilityModel?.[column.field] ?? true) !== false
|
||||
? count + 1
|
||||
: count;
|
||||
}, 0);
|
||||
}, [columns, columnVisibilityModel]);
|
||||
|
||||
const handleToggleColumn = useCallback(
|
||||
(field: string, visible: boolean) => {
|
||||
if (!visible && visibleColumnsCount <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleColumnVisibilityChange = useCallback(
|
||||
(update: Updater<VisibilityState>) => {
|
||||
setColumnVisibilityModel((prev) => {
|
||||
const next = { ...(prev ?? {}) };
|
||||
if (visible) {
|
||||
delete next[field];
|
||||
} else {
|
||||
next[field] = false;
|
||||
const current = prev ?? {};
|
||||
const nextState =
|
||||
typeof update === "function" ? update(current) : update;
|
||||
|
||||
const visibleCount = baseColumns.reduce((count, column) => {
|
||||
const isVisible = (nextState[column.field] ?? true) !== false;
|
||||
return count + (isVisible ? 1 : 0);
|
||||
}, 0);
|
||||
|
||||
if (visibleCount === 0) {
|
||||
return current;
|
||||
}
|
||||
return next;
|
||||
|
||||
const sanitized: VisibilityState = {};
|
||||
baseColumns.forEach((column) => {
|
||||
if (nextState[column.field] === false) {
|
||||
sanitized[column.field] = false;
|
||||
}
|
||||
});
|
||||
return sanitized;
|
||||
});
|
||||
},
|
||||
[setColumnVisibilityModel, visibleColumnsCount],
|
||||
[baseColumns, setColumnVisibilityModel],
|
||||
);
|
||||
|
||||
const handleManagerOrderChange = useCallback(
|
||||
(order: string[]) => {
|
||||
setColumnOrder(() => {
|
||||
const handleColumnOrderChange = useCallback(
|
||||
(update: Updater<ColumnOrderState>) => {
|
||||
setColumnOrder((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
const nextState =
|
||||
typeof update === "function" ? update(current) : update;
|
||||
const baseFields = baseColumns.map((col) => col.field);
|
||||
return reconcileColumnOrder(order, baseFields);
|
||||
return reconcileColumnOrder(nextState, baseFields);
|
||||
});
|
||||
},
|
||||
[baseColumns, setColumnOrder],
|
||||
);
|
||||
|
||||
const handleResetColumns = useCallback(() => {
|
||||
setColumnVisibilityModel({});
|
||||
setColumnOrder(baseColumns.map((col) => col.field));
|
||||
}, [baseColumns, setColumnOrder, setColumnVisibilityModel]);
|
||||
|
||||
const handleColumnVisibilityChange = useCallback(
|
||||
(update: Updater<VisibilityState>) => {
|
||||
setColumnVisibilityModel((prev) => {
|
||||
const current = prev ?? {};
|
||||
const baseState: VisibilityState = {};
|
||||
columns.forEach((column) => {
|
||||
baseState[column.field] = (current[column.field] ?? true) !== false;
|
||||
});
|
||||
|
||||
const mergedState =
|
||||
typeof update === "function"
|
||||
? update(baseState)
|
||||
: { ...baseState, ...update };
|
||||
|
||||
const hiddenFields = columns
|
||||
.filter((column) => mergedState[column.field] === false)
|
||||
.map((column) => column.field);
|
||||
|
||||
if (columns.length - hiddenFields.length === 0) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const sanitized: Partial<Record<string, boolean>> = {};
|
||||
hiddenFields.forEach((field) => {
|
||||
sanitized[field] = false;
|
||||
});
|
||||
return sanitized;
|
||||
});
|
||||
},
|
||||
[columns, setColumnVisibilityModel],
|
||||
);
|
||||
|
||||
const columnVisibilityState = useMemo<VisibilityState>(() => {
|
||||
const result: VisibilityState = {};
|
||||
if (!columnVisibilityModel) {
|
||||
columns.forEach((column) => {
|
||||
result[column.field] = true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
columns.forEach((column) => {
|
||||
result[column.field] =
|
||||
(columnVisibilityModel?.[column.field] ?? true) !== false;
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [columnVisibilityModel, columns]);
|
||||
|
||||
const columnOptions = useMemo(() => {
|
||||
return columns.map((column) => ({
|
||||
field: column.field,
|
||||
label: column.headerName ?? column.field,
|
||||
visible: (columnVisibilityModel?.[column.field] ?? true) !== false,
|
||||
}));
|
||||
}, [columns, columnVisibilityModel]);
|
||||
|
||||
const prevRowsRef = useRef<Map<string, ConnectionRow>>(new Map());
|
||||
|
||||
const connRows = useMemo<ConnectionRow[]>(() => {
|
||||
const prevMap = prevRowsRef.current;
|
||||
const nextMap = new Map<string, ConnectionRow>();
|
||||
|
||||
const nextRows = connections.map((each) => {
|
||||
const nextRow = createConnectionRow(each);
|
||||
const prevRow = prevMap.get(each.id);
|
||||
|
||||
if (prevRow && areRowsEqual(prevRow, nextRow)) {
|
||||
nextMap.set(each.id, prevRow);
|
||||
return prevRow;
|
||||
}
|
||||
|
||||
nextMap.set(each.id, nextRow);
|
||||
return nextRow;
|
||||
});
|
||||
|
||||
prevRowsRef.current = nextMap;
|
||||
return nextRows;
|
||||
}, [connections]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [relativeNow, setRelativeNow] = useState(() => Date.now());
|
||||
|
||||
const columnDefs = useMemo<ColumnDef<ConnectionRow>[]>(() => {
|
||||
return columns.map((column) => {
|
||||
const baseCell: ColumnDef<ConnectionRow>["cell"] = column.cell
|
||||
const columnDefs = useMemo<ColumnDef<IConnectionsItem>[]>(() => {
|
||||
return baseColumns.map((column) => {
|
||||
const baseCell: ColumnDef<IConnectionsItem>["cell"] = column.cell
|
||||
? (ctx) => column.cell?.(ctx.row.original)
|
||||
: (ctx) => ctx.getValue() as ReactNode;
|
||||
|
||||
const cell: ColumnDef<ConnectionRow>["cell"] =
|
||||
const cell: ColumnDef<IConnectionsItem>["cell"] =
|
||||
column.field === "time"
|
||||
? (ctx) => dayjs(ctx.row.original.time).from(relativeNow)
|
||||
? (ctx) => dayjs(ctx.getValue() as string).from(relativeNow)
|
||||
: baseCell;
|
||||
|
||||
return {
|
||||
id: column.field,
|
||||
accessorKey: column.field,
|
||||
accessorFn: (row) => getConnectionCellValue(column.field, row),
|
||||
header: column.headerName,
|
||||
size: column.width,
|
||||
minSize: column.minWidth ?? 80,
|
||||
enableResizing: true,
|
||||
minSize: column.minWidth,
|
||||
meta: {
|
||||
align: column.align ?? "left",
|
||||
field: column.field,
|
||||
label: column.headerName,
|
||||
},
|
||||
cell,
|
||||
} satisfies ColumnDef<ConnectionRow>;
|
||||
} satisfies ColumnDef<IConnectionsItem>;
|
||||
});
|
||||
}, [columns, relativeNow]);
|
||||
}, [baseColumns, relativeNow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
@@ -450,7 +352,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
const prevState = prev ?? {};
|
||||
const nextState =
|
||||
typeof updater === "function" ? updater(prevState) : updater;
|
||||
const sanitized: Record<string, number> = {};
|
||||
const sanitized: ColumnSizingState = {};
|
||||
Object.entries(nextState).forEach(([key, size]) => {
|
||||
if (typeof size === "number" && Number.isFinite(size)) {
|
||||
sanitized[key] = size;
|
||||
@@ -463,22 +365,45 @@ export const ConnectionTable = (props: Props) => {
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: connRows,
|
||||
data: connections,
|
||||
state: {
|
||||
columnVisibility: columnVisibilityState,
|
||||
columnVisibility: columnVisibilityModel ?? {},
|
||||
columnSizing: columnWidths,
|
||||
columnOrder,
|
||||
sorting,
|
||||
},
|
||||
initialState: {
|
||||
columnOrder: baseColumns.map((col) => col.field),
|
||||
},
|
||||
defaultColumn: {
|
||||
minSize: 80,
|
||||
enableResizing: true,
|
||||
},
|
||||
columnResizeMode: "onChange",
|
||||
enableSortingRemoval: true,
|
||||
getRowId: (row) => row.id,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: sorting.length ? getSortedRowModel() : undefined,
|
||||
onSortingChange: setSorting,
|
||||
onColumnSizingChange: handleColumnSizingChange,
|
||||
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||
onColumnOrderChange: handleColumnOrderChange,
|
||||
columns: columnDefs,
|
||||
});
|
||||
|
||||
const handleManagerOrderChange = useCallback(
|
||||
(order: string[]) => {
|
||||
const baseFields = baseColumns.map((col) => col.field);
|
||||
table.setColumnOrder(reconcileColumnOrder(order, baseFields));
|
||||
},
|
||||
[baseColumns, table],
|
||||
);
|
||||
|
||||
const handleResetColumns = useCallback(() => {
|
||||
table.resetColumnVisibility();
|
||||
table.resetColumnOrder();
|
||||
}, [table]);
|
||||
|
||||
const rows = table.getRowModel().rows;
|
||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
@@ -491,6 +416,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||
const totalSize = rowVirtualizer.getTotalSize();
|
||||
const tableWidth = table.getTotalSize();
|
||||
const managerColumns = table.getAllLeafColumns();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -582,15 +508,10 @@ export const ConnectionTable = (props: Props) => {
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
boxSizing: "border-box",
|
||||
px: 1,
|
||||
py: 1,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "text.secondary",
|
||||
userSelect: "none",
|
||||
justifyContent:
|
||||
meta?.align === "right" ? "flex-end" : "flex-start",
|
||||
gap: 0.25,
|
||||
"&:hover": {
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.action.hover,
|
||||
@@ -599,15 +520,26 @@ export const ConnectionTable = (props: Props) => {
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
onClick={
|
||||
header.column.getCanSort()
|
||||
? header.column.getToggleSortingHandler()
|
||||
: undefined
|
||||
}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent:
|
||||
meta?.align === "right"
|
||||
? "flex-end"
|
||||
: "flex-start",
|
||||
gap: 0.5,
|
||||
px: 1,
|
||||
py: 1,
|
||||
cursor: header.column.getCanSort()
|
||||
? "pointer"
|
||||
: "default",
|
||||
}}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
@@ -620,8 +552,15 @@ export const ConnectionTable = (props: Props) => {
|
||||
</Box>
|
||||
{header.column.getCanResize() && (
|
||||
<Box
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => {
|
||||
event.stopPropagation();
|
||||
header.getResizeHandler()(event);
|
||||
}}
|
||||
onTouchStart={(event) => {
|
||||
event.stopPropagation();
|
||||
header.getResizeHandler()(event);
|
||||
}}
|
||||
sx={{
|
||||
cursor: "col-resize",
|
||||
position: "absolute",
|
||||
@@ -656,7 +595,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
return (
|
||||
<Box
|
||||
key={row.id}
|
||||
onClick={() => onShowDetail(row.original.connectionData)}
|
||||
onClick={() => onShowDetail(row.original)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
@@ -713,9 +652,8 @@ export const ConnectionTable = (props: Props) => {
|
||||
</Box>
|
||||
<ConnectionColumnManager
|
||||
open={columnManagerOpen}
|
||||
columns={columnOptions}
|
||||
columns={managerColumns}
|
||||
onClose={onCloseColumnManager}
|
||||
onToggle={handleToggleColumn}
|
||||
onOrderChange={handleManagerOrderChange}
|
||||
onReset={handleResetColumns}
|
||||
/>
|
||||
|
||||
@@ -4,13 +4,7 @@ import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import {
|
||||
useAppUptime,
|
||||
useClashConfig,
|
||||
useRulesData,
|
||||
useSystemProxyAddress,
|
||||
useSystemProxyData,
|
||||
} from "@/hooks/use-clash-data";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
|
||||
@@ -25,14 +19,7 @@ const formatUptime = (uptimeMs: number) => {
|
||||
export const ClashInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { version: clashVersion } = useClash();
|
||||
const { clashConfig } = useClashConfig();
|
||||
const { sysproxy } = useSystemProxyData();
|
||||
const { rules } = useRulesData();
|
||||
const { uptime } = useAppUptime();
|
||||
const systemProxyAddress = useSystemProxyAddress({
|
||||
clashConfig,
|
||||
sysproxy,
|
||||
});
|
||||
const { clashConfig, rules, uptime, systemProxyAddress } = useAppData();
|
||||
|
||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import { useClashConfig } from "@/hooks/use-clash-data";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { patchClashMode } from "@/services/cmds";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
|
||||
@@ -41,7 +41,7 @@ const MODE_META: Record<
|
||||
export const ClashModeCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { verge } = useVerge();
|
||||
const { clashConfig, refreshClashConfig } = useClashConfig();
|
||||
const { clashConfig, refreshClashConfig } = useAppData();
|
||||
|
||||
// 支持的模式列表
|
||||
const modeList = CLASH_MODES;
|
||||
|
||||
@@ -27,21 +27,22 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import React from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router";
|
||||
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import { EnhancedCard } from "@/components/home/enhanced-card";
|
||||
import {
|
||||
useClashConfig,
|
||||
useProxiesData,
|
||||
useRulesData,
|
||||
} from "@/hooks/use-clash-data";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import delayManager from "@/services/delay";
|
||||
import { debugLog } from "@/utils/debug";
|
||||
|
||||
@@ -50,8 +51,8 @@ const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
|
||||
const STORAGE_KEY_PROXY = "clash-verge-selected-proxy";
|
||||
const STORAGE_KEY_SORT_TYPE = "clash-verge-proxy-sort-type";
|
||||
|
||||
const AUTO_CHECK_INITIAL_DELAY_MS = 1500;
|
||||
const AUTO_CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
||||
const AUTO_CHECK_DEFAULT_INTERVAL_MINUTES = 5;
|
||||
const AUTO_CHECK_INITIAL_DELAY_MS = 100;
|
||||
|
||||
// 代理节点信息接口
|
||||
interface ProxyOption {
|
||||
@@ -105,13 +106,19 @@ export const CurrentProxyCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { proxies, refreshProxy } = useProxiesData();
|
||||
const { clashConfig } = useClashConfig();
|
||||
const { rules } = useRulesData();
|
||||
const { proxies, clashConfig, refreshProxy, rules } = useAppData();
|
||||
const { verge } = useVerge();
|
||||
const { current: currentProfile } = useProfiles();
|
||||
const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false;
|
||||
const defaultLatencyTimeout = verge?.default_latency_timeout;
|
||||
const autoDelayIntervalMs = useMemo(() => {
|
||||
const rawInterval = verge?.auto_delay_detection_interval_minutes;
|
||||
const intervalMinutes =
|
||||
typeof rawInterval === "number" && rawInterval > 0
|
||||
? rawInterval
|
||||
: AUTO_CHECK_DEFAULT_INTERVAL_MINUTES;
|
||||
return Math.max(1, Math.round(intervalMinutes)) * 60 * 1000;
|
||||
}, [verge?.auto_delay_detection_interval_minutes]);
|
||||
const currentProfileId = currentProfile?.uid || null;
|
||||
|
||||
const getProfileStorageKey = useCallback(
|
||||
@@ -598,13 +605,13 @@ export const CurrentProxyCard = () => {
|
||||
if (disposed) return;
|
||||
await checkCurrentProxyDelay();
|
||||
if (disposed) return;
|
||||
intervalTimer = setTimeout(runAndSchedule, AUTO_CHECK_INTERVAL_MS);
|
||||
intervalTimer = setTimeout(runAndSchedule, autoDelayIntervalMs);
|
||||
};
|
||||
|
||||
initialTimer = setTimeout(async () => {
|
||||
await checkCurrentProxyDelay();
|
||||
if (disposed) return;
|
||||
intervalTimer = setTimeout(runAndSchedule, AUTO_CHECK_INTERVAL_MS);
|
||||
intervalTimer = setTimeout(runAndSchedule, autoDelayIntervalMs);
|
||||
}, AUTO_CHECK_INITIAL_DELAY_MS);
|
||||
|
||||
return () => {
|
||||
@@ -614,6 +621,7 @@ export const CurrentProxyCard = () => {
|
||||
};
|
||||
}, [
|
||||
checkCurrentProxyDelay,
|
||||
autoDelayIntervalMs,
|
||||
isDirectMode,
|
||||
state.selection.group,
|
||||
state.selection.proxy,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import { useRefreshAll } from "@/hooks/use-clash-data";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { openWebUrl, updateProfile } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
@@ -281,7 +281,7 @@ export const HomeProfileCard = ({
|
||||
}: HomeProfileCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const refreshAll = useRefreshAll();
|
||||
const { refreshAll } = useAppData();
|
||||
|
||||
// 更新当前订阅
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Box, Button, IconButton, Skeleton, Typography } from "@mui/material";
|
||||
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { getIpInfo } from "@/services/api";
|
||||
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
@@ -55,6 +56,7 @@ const getCountryFlag = (countryCode: string) => {
|
||||
// IP信息卡片组件
|
||||
export const IpInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { clashConfig } = useAppData();
|
||||
const [ipInfo, setIpInfo] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
@@ -90,6 +92,20 @@ export const IpInfoCard = () => {
|
||||
console.warn("Failed to read IP info from sessionStorage:", e);
|
||||
}
|
||||
|
||||
if (typeof navigator !== "undefined" && !navigator.onLine) {
|
||||
setLoading(false);
|
||||
lastFetchRef.current = Date.now();
|
||||
setCountdown(IP_REFRESH_SECONDS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clashConfig) {
|
||||
setLoading(false);
|
||||
lastFetchRef.current = Date.now();
|
||||
setCountdown(IP_REFRESH_SECONDS);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getIpInfo();
|
||||
@@ -113,11 +129,13 @@ export const IpInfoCard = () => {
|
||||
? err.message
|
||||
: t("home.components.ipInfo.errors.load"),
|
||||
);
|
||||
lastFetchRef.current = Date.now();
|
||||
setCountdown(IP_REFRESH_SECONDS);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[t],
|
||||
[t, clashConfig],
|
||||
);
|
||||
|
||||
// 组件加载时获取IP信息并启动基于上次请求时间的倒计时
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
Box,
|
||||
type SnackbarOrigin,
|
||||
} from "@mui/material";
|
||||
import React, { useMemo, useSyncExternalStore } from "react";
|
||||
import React, { useCallback, useMemo, useSyncExternalStore } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
subscribeNotices,
|
||||
hideNotice,
|
||||
getSnapshotNotices,
|
||||
showNotice,
|
||||
} from "@/services/notice-service";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
|
||||
@@ -85,6 +86,45 @@ const resolveNoticeMessage = (
|
||||
});
|
||||
};
|
||||
|
||||
const extractNoticeCopyText = (input: unknown): string | undefined => {
|
||||
if (input === null || input === undefined) return undefined;
|
||||
if (typeof input === "string") return input;
|
||||
if (typeof input === "number" || typeof input === "boolean") {
|
||||
return String(input);
|
||||
}
|
||||
if (input instanceof Error) {
|
||||
return input.message || input.name;
|
||||
}
|
||||
if (React.isValidElement(input)) return undefined;
|
||||
if (typeof input === "object") {
|
||||
const maybeMessage = (input as { message?: unknown }).message;
|
||||
if (typeof maybeMessage === "string") return maybeMessage;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(input);
|
||||
} catch {
|
||||
return String(input);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveNoticeCopyText = (
|
||||
notice: NoticeItem,
|
||||
t: TranslationFn,
|
||||
): string | undefined => {
|
||||
if (
|
||||
notice.i18n?.key === "shared.feedback.notices.prefixedRaw" ||
|
||||
notice.i18n?.key === "shared.feedback.notices.raw"
|
||||
) {
|
||||
const rawText = extractNoticeCopyText(notice.i18n?.params?.message);
|
||||
if (rawText) return rawText;
|
||||
}
|
||||
|
||||
return (
|
||||
extractNoticeCopyText(resolveNoticeMessage(notice, t)) ??
|
||||
extractNoticeCopyText(notice.message)
|
||||
);
|
||||
};
|
||||
|
||||
interface NoticeManagerProps {
|
||||
position?: NoticePosition | null;
|
||||
}
|
||||
@@ -105,6 +145,23 @@ export const NoticeManager: React.FC<NoticeManagerProps> = ({ position }) => {
|
||||
hideNotice(id);
|
||||
};
|
||||
|
||||
const handleNoticeCopy = useCallback(
|
||||
async (notice: NoticeItem) => {
|
||||
const text = resolveNoticeCopyText(notice, t);
|
||||
if (!text) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
showNotice.success(
|
||||
"shared.feedback.notifications.common.copySuccess",
|
||||
1000,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("[NoticeManager] copy to clipboard failed:", error);
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -139,6 +196,11 @@ export const NoticeManager: React.FC<NoticeManagerProps> = ({ position }) => {
|
||||
severity={notice.type}
|
||||
variant="filled"
|
||||
sx={{ width: "100%" }}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleNoticeCopy(notice);
|
||||
}}
|
||||
action={
|
||||
<IconButton
|
||||
size="small"
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { useRef } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { checkUpdateSafe } from "@/services/update";
|
||||
import { DialogRef } from "@/components/base";
|
||||
import { useUpdate } from "@/hooks/use-update";
|
||||
|
||||
import { DialogRef } from "../base";
|
||||
import { UpdateViewer } from "../setting/mods/update-viewer";
|
||||
|
||||
interface Props {
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { styled, Box } from "@mui/material";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { SearchState } from "@/components/base/base-search-box";
|
||||
import type { SearchState } from "@/components/base";
|
||||
|
||||
const Item = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||
padding: "8px 0",
|
||||
|
||||
@@ -48,7 +48,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
|
||||
import { Switch } from "@/components/base";
|
||||
import { BaseSearchBox, Switch } from "@/components/base";
|
||||
import { GroupItem } from "@/components/profile/group-item";
|
||||
import {
|
||||
getNetworkInterfaces,
|
||||
@@ -60,8 +60,6 @@ import { useThemeMode } from "@/services/states";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
import { BaseSearchBox } from "../base/base-search-box";
|
||||
|
||||
interface Props {
|
||||
proxiesUid: string;
|
||||
mergeUid: string;
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
|
||||
import { BaseSearchBox } from "@/components/base";
|
||||
import { ProxyItem } from "@/components/profile/proxy-item";
|
||||
import { readProfileFile, saveProfileFile } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
@@ -47,8 +48,6 @@ import { useThemeMode } from "@/services/states";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import parseUri from "@/utils/uri-parser";
|
||||
|
||||
import { BaseSearchBox } from "../base/base-search-box";
|
||||
|
||||
interface Props {
|
||||
profileUid: string;
|
||||
property: string;
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
|
||||
import { Switch } from "@/components/base";
|
||||
import { BaseSearchBox, Switch } from "@/components/base";
|
||||
import { RuleItem } from "@/components/profile/rule-item";
|
||||
import { readProfileFile, saveProfileFile } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
@@ -50,8 +50,6 @@ import { useThemeMode } from "@/services/states";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
import { BaseSearchBox } from "../base/base-search-box";
|
||||
|
||||
interface Props {
|
||||
groupsUid: string;
|
||||
mergeUid: string;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateProxyProvider } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import { useProxiesData, useProxyProvidersData } from "@/hooks/use-clash-data";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
@@ -48,8 +48,7 @@ const parseExpire = (expire?: number) => {
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { proxyProviders, refreshProxyProviders } = useProxyProvidersData();
|
||||
const { refreshProxy } = useProxiesData();
|
||||
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
|
||||
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 检查是否有提供者
|
||||
@@ -176,8 +175,8 @@ export const ProviderButton = () => {
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(proxyProviders || {})
|
||||
.sort()
|
||||
.map(([key, provider]) => {
|
||||
if (!provider) return null;
|
||||
.map(([key, item]) => {
|
||||
const provider = item;
|
||||
const time = dayjs(provider.updatedAt);
|
||||
const isUpdating = updating[key];
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
selectNodeForGroup,
|
||||
} from "tauri-plugin-mihomo-api";
|
||||
|
||||
import { useProxiesData } from "@/hooks/use-clash-data";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { updateProxyChainConfigInRuntime } from "@/services/cmds";
|
||||
import { debugLog } from "@/utils/debug";
|
||||
|
||||
@@ -199,7 +199,7 @@ export const ProxyChain = ({
|
||||
}: ProxyChainProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { proxies, refreshProxy } = useProxiesData();
|
||||
const { proxies, refreshProxy } = useAppData();
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const markUnsavedChanges = useCallback(() => {
|
||||
onMarkUnsavedChanges?.();
|
||||
@@ -221,7 +221,7 @@ export const ProxyChain = ({
|
||||
}
|
||||
|
||||
const proxyChainGroup = proxies.groups.find(
|
||||
(group) => group.name === selectedGroup,
|
||||
(group: { name: string }) => group.name === selectedGroup,
|
||||
);
|
||||
|
||||
return proxyChainGroup?.now === lastNode.name;
|
||||
|
||||
@@ -15,14 +15,14 @@ import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
||||
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import { useProxiesData } from "@/hooks/use-clash-data";
|
||||
import { BaseEmpty } from "@/components/base";
|
||||
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { updateProxyChainConfigInRuntime } from "@/services/cmds";
|
||||
import delayManager from "@/services/delay";
|
||||
import { debugLog } from "@/utils/debug";
|
||||
|
||||
import { BaseEmpty } from "../base";
|
||||
import { ScrollTopButton } from "../layout/scroll-top-button";
|
||||
|
||||
import { ProxyChain } from "./proxy-chain";
|
||||
@@ -80,7 +80,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
}>({ open: false, message: "" });
|
||||
|
||||
const { verge } = useVerge();
|
||||
const { proxies: proxiesData } = useProxiesData();
|
||||
const { proxies: proxiesData } = useAppData();
|
||||
const groups = proxiesData?.groups;
|
||||
const availableGroups = useMemo(() => {
|
||||
if (!groups) return [];
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Box, IconButton, TextField, SxProps } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseSearchBox } from "@/components/base/base-search-box";
|
||||
import { BaseSearchBox } from "@/components/base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import delayManager from "@/services/delay";
|
||||
import { debugLog } from "@/utils/debug";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useProxiesData } from "@/hooks/use-clash-data";
|
||||
import { useRuntimeConfig } from "@/hooks/use-clash";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { getRuntimeConfig } from "@/services/cmds";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import delayManager from "@/services/delay";
|
||||
import { debugLog } from "@/utils/debug";
|
||||
|
||||
@@ -34,8 +33,24 @@ interface IProxyItem {
|
||||
}
|
||||
|
||||
// 代理组类型
|
||||
type ProxyGroup = IProxyGroupItem & {
|
||||
now?: string;
|
||||
type ProxyGroup = {
|
||||
name: string;
|
||||
type: string;
|
||||
udp: boolean;
|
||||
xudp: boolean;
|
||||
tfo: boolean;
|
||||
mptcp: boolean;
|
||||
smux: boolean;
|
||||
history: {
|
||||
time: string;
|
||||
delay: number;
|
||||
}[];
|
||||
now: string;
|
||||
all: IProxyItem[];
|
||||
hidden?: boolean;
|
||||
icon?: string;
|
||||
testUrl?: string;
|
||||
provider?: string;
|
||||
};
|
||||
|
||||
export interface IRenderItem {
|
||||
@@ -84,21 +99,14 @@ export const useRenderList = (
|
||||
selectedGroup?: string | null,
|
||||
) => {
|
||||
// 使用全局数据提供者
|
||||
const { proxies: proxiesData, refreshProxy } = useProxiesData();
|
||||
const { proxies: proxiesData, refreshProxy } = useAppData();
|
||||
const { verge } = useVerge();
|
||||
const { width } = useWindowWidth();
|
||||
const [headStates, setHeadState] = useHeadStateNew();
|
||||
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(
|
||||
|
||||
@@ -21,10 +21,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateRuleProvider } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import type {
|
||||
useRuleProvidersData,
|
||||
useRulesData,
|
||||
} from "@/hooks/use-clash-data";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
|
||||
// 辅助组件 - 类型框
|
||||
@@ -40,22 +37,10 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
type RuleProvidersHook = ReturnType<typeof useRuleProvidersData>;
|
||||
type RulesHook = ReturnType<typeof useRulesData>;
|
||||
|
||||
interface ProviderButtonProps {
|
||||
ruleProviders: RuleProvidersHook["ruleProviders"];
|
||||
refreshRuleProviders: RuleProvidersHook["refreshRuleProviders"];
|
||||
refreshRules: RulesHook["refreshRules"];
|
||||
}
|
||||
|
||||
export const ProviderButton = ({
|
||||
ruleProviders,
|
||||
refreshRuleProviders,
|
||||
refreshRules,
|
||||
}: ProviderButtonProps) => {
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData();
|
||||
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 检查是否有提供者
|
||||
@@ -178,8 +163,8 @@ export const ProviderButton = ({
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(ruleProviders || {})
|
||||
.sort()
|
||||
.map(([key, provider]) => {
|
||||
if (!provider) return null;
|
||||
.map(([key, item]) => {
|
||||
const provider = item;
|
||||
const time = dayjs(provider.updatedAt);
|
||||
const isUpdating = updating[key];
|
||||
|
||||
|
||||
@@ -16,11 +16,16 @@ import { useTranslation } from "react-i18next";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
import {
|
||||
buildWebdavSignature,
|
||||
getWebdavStatus,
|
||||
setWebdavStatus,
|
||||
} from "@/services/webdav-status";
|
||||
import { isValidUrl } from "@/utils/helper";
|
||||
|
||||
interface BackupConfigViewerProps {
|
||||
onBackupSuccess: () => Promise<void>;
|
||||
onSaveSuccess: () => Promise<void>;
|
||||
onSaveSuccess: (signature?: string) => Promise<void>;
|
||||
onRefresh: () => Promise<void>;
|
||||
onInit: () => Promise<void>;
|
||||
setLoading: (loading: boolean) => void;
|
||||
@@ -35,7 +40,7 @@ export const BackupConfigViewer = memo(
|
||||
setLoading,
|
||||
}: BackupConfigViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { verge } = useVerge();
|
||||
const { verge, mutateVerge } = useVerge();
|
||||
const { webdav_url, webdav_username, webdav_password } = verge || {};
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
@@ -58,6 +63,10 @@ export const BackupConfigViewer = memo(
|
||||
webdav_username !== username ||
|
||||
webdav_password !== password;
|
||||
|
||||
const webdavSignature = buildWebdavSignature(verge);
|
||||
const webdavStatus = getWebdavStatus(webdavSignature);
|
||||
const shouldAutoInit = webdavStatus !== "failed";
|
||||
|
||||
const handleClickShowPassword = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
@@ -66,8 +75,11 @@ export const BackupConfigViewer = memo(
|
||||
if (!webdav_url || !webdav_username || !webdav_password) {
|
||||
return;
|
||||
}
|
||||
if (!shouldAutoInit) {
|
||||
return;
|
||||
}
|
||||
void onInit();
|
||||
}, [webdav_url, webdav_username, webdav_password, onInit]);
|
||||
}, [webdav_url, webdav_username, webdav_password, onInit, shouldAutoInit]);
|
||||
|
||||
const checkForm = () => {
|
||||
const username = usernameRef.current?.value;
|
||||
@@ -97,18 +109,32 @@ export const BackupConfigViewer = memo(
|
||||
|
||||
const save = useLockFn(async (data: IWebDavConfig) => {
|
||||
checkForm();
|
||||
const signature = buildWebdavSignature({
|
||||
webdav_url: data.url,
|
||||
webdav_username: data.username,
|
||||
webdav_password: data.password,
|
||||
});
|
||||
const trimmedUrl = data.url.trim();
|
||||
const trimmedUsername = data.username.trim();
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await saveWebdavConfig(
|
||||
data.url.trim(),
|
||||
data.username.trim(),
|
||||
data.password,
|
||||
).then(() => {
|
||||
showNotice.success(
|
||||
"settings.modals.backup.messages.webdavConfigSaved",
|
||||
);
|
||||
onSaveSuccess();
|
||||
});
|
||||
await saveWebdavConfig(trimmedUrl, trimmedUsername, data.password);
|
||||
await mutateVerge(
|
||||
(current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
webdav_url: trimmedUrl,
|
||||
webdav_username: trimmedUsername,
|
||||
webdav_password: data.password,
|
||||
}
|
||||
: current,
|
||||
false,
|
||||
);
|
||||
setWebdavStatus(signature, "unknown");
|
||||
showNotice.success("settings.modals.backup.messages.webdavConfigSaved");
|
||||
await onSaveSuccess(signature);
|
||||
} catch (error) {
|
||||
showNotice.error(
|
||||
"settings.modals.backup.messages.webdavConfigSaveFailed",
|
||||
@@ -122,16 +148,24 @@ export const BackupConfigViewer = memo(
|
||||
|
||||
const handleBackup = useLockFn(async () => {
|
||||
checkForm();
|
||||
const signature = buildWebdavSignature({
|
||||
webdav_url: url,
|
||||
webdav_username: username,
|
||||
webdav_password: password,
|
||||
});
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await createWebdavBackup().then(async () => {
|
||||
showNotice.success("settings.modals.backup.messages.backupCreated");
|
||||
await onBackupSuccess();
|
||||
});
|
||||
setWebdavStatus(signature, "ready");
|
||||
} catch (error) {
|
||||
showNotice.error("settings.modals.backup.messages.backupFailed", {
|
||||
error,
|
||||
});
|
||||
setWebdavStatus(signature, "failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ import {
|
||||
restoreWebDavBackup,
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
import {
|
||||
buildWebdavSignature,
|
||||
getWebdavStatus,
|
||||
setWebdavStatus,
|
||||
} from "@/services/webdav-status";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
@@ -79,14 +84,17 @@ export const BackupHistoryViewer = ({
|
||||
const { verge } = useVerge();
|
||||
const [rows, setRows] = useState<BackupRow[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isRestoring, setIsRestoring] = useState(false);
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
const isLocal = source === "local";
|
||||
const isWebDavConfigured = Boolean(
|
||||
verge?.webdav_url && verge?.webdav_username && verge?.webdav_password,
|
||||
);
|
||||
const webdavSignature = buildWebdavSignature(verge);
|
||||
const webdavStatus = getWebdavStatus(webdavSignature);
|
||||
const shouldSkipWebDav = !isLocal && !isWebDavConfigured;
|
||||
const pageSize = 8;
|
||||
const isBusy = loading || isRestarting;
|
||||
const isBusy = loading || isRestoring || isRestarting;
|
||||
|
||||
const buildRow = useCallback(
|
||||
(item: ILocalBackupFile | IWebDavFile): BackupRow | null => {
|
||||
@@ -127,33 +135,49 @@ export const BackupHistoryViewer = ({
|
||||
[t],
|
||||
);
|
||||
|
||||
const fetchRows = useCallback(async () => {
|
||||
if (!open) return;
|
||||
if (shouldSkipWebDav) {
|
||||
setRows([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = isLocal ? await listLocalBackup() : await listWebDavBackup();
|
||||
setRows(
|
||||
list
|
||||
.map((item) => buildRow(item))
|
||||
.filter((item): item is BackupRow => item !== null)
|
||||
.sort((a, b) =>
|
||||
a.sort_value === b.sort_value
|
||||
? b.filename.localeCompare(a.filename)
|
||||
: b.sort_value - a.sort_value,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setRows([]);
|
||||
showNotice.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [buildRow, isLocal, open, shouldSkipWebDav]);
|
||||
const fetchRows = useCallback(
|
||||
async (options?: { force?: boolean }) => {
|
||||
if (!open) return;
|
||||
if (shouldSkipWebDav) {
|
||||
setRows([]);
|
||||
return;
|
||||
}
|
||||
if (!isLocal && webdavStatus === "failed" && !options?.force) {
|
||||
setRows([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = isLocal
|
||||
? await listLocalBackup()
|
||||
: await listWebDavBackup();
|
||||
if (!isLocal) {
|
||||
setWebdavStatus(webdavSignature, "ready");
|
||||
}
|
||||
setRows(
|
||||
list
|
||||
.map((item) => buildRow(item))
|
||||
.filter((item): item is BackupRow => item !== null)
|
||||
.sort((a, b) =>
|
||||
a.sort_value === b.sort_value
|
||||
? b.filename.localeCompare(a.filename)
|
||||
: b.sort_value - a.sort_value,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!isLocal) {
|
||||
setWebdavStatus(webdavSignature, "failed");
|
||||
}
|
||||
console.error(error);
|
||||
setRows([]);
|
||||
showNotice.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature, webdavStatus],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchRows();
|
||||
@@ -168,7 +192,7 @@ export const BackupHistoryViewer = ({
|
||||
);
|
||||
|
||||
const summary = useMemo(() => {
|
||||
if (shouldSkipWebDav) {
|
||||
if (shouldSkipWebDav || (!isLocal && webdavStatus === "failed")) {
|
||||
return t("settings.modals.backup.manual.webdav");
|
||||
}
|
||||
if (!total) return t("settings.modals.backup.history.empty");
|
||||
@@ -178,7 +202,7 @@ export const BackupHistoryViewer = ({
|
||||
count: total,
|
||||
recent,
|
||||
});
|
||||
}, [rows, shouldSkipWebDav, t, total]);
|
||||
}, [isLocal, rows, shouldSkipWebDav, t, total, webdavStatus]);
|
||||
|
||||
const handleDelete = useLockFn(async (filename: string) => {
|
||||
if (isRestarting) return;
|
||||
@@ -195,24 +219,32 @@ export const BackupHistoryViewer = ({
|
||||
});
|
||||
|
||||
const handleRestore = useLockFn(async (filename: string) => {
|
||||
if (isRestarting) return;
|
||||
if (isRestoring || isRestarting) return;
|
||||
if (
|
||||
!(await confirmAsync(t("settings.modals.backup.messages.confirmRestore")))
|
||||
)
|
||||
return;
|
||||
if (isLocal) {
|
||||
await restoreLocalBackup(filename);
|
||||
} else {
|
||||
await restoreWebDavBackup(filename);
|
||||
setIsRestoring(true);
|
||||
try {
|
||||
if (isLocal) {
|
||||
await restoreLocalBackup(filename);
|
||||
} else {
|
||||
await restoreWebDavBackup(filename);
|
||||
}
|
||||
showNotice.success("settings.modals.backup.messages.restoreSuccess");
|
||||
setIsRestarting(true);
|
||||
window.setTimeout(() => {
|
||||
void restartApp().catch((err: unknown) => {
|
||||
setIsRestarting(false);
|
||||
showNotice.error(err);
|
||||
});
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showNotice.error(error);
|
||||
} finally {
|
||||
setIsRestoring(false);
|
||||
}
|
||||
showNotice.success("settings.modals.backup.messages.restoreSuccess");
|
||||
setIsRestarting(true);
|
||||
window.setTimeout(() => {
|
||||
void restartApp().catch((err: unknown) => {
|
||||
setIsRestarting(false);
|
||||
showNotice.error(err);
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
const handleExport = useLockFn(async (filename: string) => {
|
||||
@@ -232,7 +264,7 @@ export const BackupHistoryViewer = ({
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (isRestarting) return;
|
||||
void fetchRows();
|
||||
void fetchRows({ force: true });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,12 +14,17 @@ import { useCallback, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import {
|
||||
createLocalBackup,
|
||||
createWebdavBackup,
|
||||
importLocalBackup,
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
import {
|
||||
buildWebdavSignature,
|
||||
setWebdavStatus,
|
||||
} from "@/services/webdav-status";
|
||||
|
||||
import { AutoBackupSettings } from "./auto-backup-settings";
|
||||
import { BackupHistoryViewer } from "./backup-history-viewer";
|
||||
@@ -29,6 +34,7 @@ type BackupSource = "local" | "webdav";
|
||||
|
||||
export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { t } = useTranslation();
|
||||
const { verge } = useVerge();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [busyAction, setBusyAction] = useState<BackupSource | null>(null);
|
||||
const [localImporting, setLocalImporting] = useState(false);
|
||||
@@ -36,6 +42,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const [historySource, setHistorySource] = useState<BackupSource>("local");
|
||||
const [historyPage, setHistoryPage] = useState(0);
|
||||
const [webdavDialogOpen, setWebdavDialogOpen] = useState(false);
|
||||
const webdavSignature = buildWebdavSignature(verge);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setOpen(true),
|
||||
@@ -59,6 +66,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
} else {
|
||||
await createWebdavBackup();
|
||||
showNotice.success("settings.modals.backup.messages.backupCreated");
|
||||
setWebdavStatus(webdavSignature, "ready");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -68,6 +76,9 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
: "settings.modals.backup.messages.backupFailed",
|
||||
target === "local" ? undefined : { error },
|
||||
);
|
||||
if (target === "webdav") {
|
||||
setWebdavStatus(webdavSignature, "failed");
|
||||
}
|
||||
} finally {
|
||||
setBusyAction(null);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,13 @@ import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseDialog, BaseLoadingOverlay } from "@/components/base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { listWebDavBackup } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
import {
|
||||
buildWebdavSignature,
|
||||
setWebdavStatus,
|
||||
} from "@/services/webdav-status";
|
||||
|
||||
import { BackupConfigViewer } from "./backup-config-viewer";
|
||||
|
||||
@@ -22,7 +27,9 @@ export const BackupWebdavDialog = ({
|
||||
setBusy,
|
||||
}: BackupWebdavDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { verge } = useVerge();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const webdavSignature = buildWebdavSignature(verge);
|
||||
|
||||
const handleLoading = useCallback(
|
||||
(value: boolean) => {
|
||||
@@ -33,16 +40,19 @@ export const BackupWebdavDialog = ({
|
||||
);
|
||||
|
||||
const refreshWebdav = useCallback(
|
||||
async (options?: { silent?: boolean }) => {
|
||||
async (options?: { silent?: boolean; signature?: string }) => {
|
||||
const signature = options?.signature ?? webdavSignature;
|
||||
handleLoading(true);
|
||||
try {
|
||||
await listWebDavBackup();
|
||||
setWebdavStatus(signature, "ready");
|
||||
if (!options?.silent) {
|
||||
showNotice.success(
|
||||
"settings.modals.backup.messages.webdavRefreshSuccess",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setWebdavStatus(signature, "failed");
|
||||
showNotice.error(
|
||||
"settings.modals.backup.messages.webdavRefreshFailed",
|
||||
{ error },
|
||||
@@ -51,11 +61,11 @@ export const BackupWebdavDialog = ({
|
||||
handleLoading(false);
|
||||
}
|
||||
},
|
||||
[handleLoading],
|
||||
[handleLoading, webdavSignature],
|
||||
);
|
||||
|
||||
const refreshSilently = useCallback(
|
||||
() => refreshWebdav({ silent: true }),
|
||||
(signature?: string) => refreshWebdav({ silent: true, signature }),
|
||||
[refreshWebdav],
|
||||
);
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
// TODO 减少代码复杂度,性能开支
|
||||
const onSave = useLockFn(async () => {
|
||||
// 端口冲突检测
|
||||
const portList = [
|
||||
@@ -140,14 +141,26 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const port of portList) {
|
||||
const original = originalPortsRef.current;
|
||||
const changedPorts: number[] = [];
|
||||
|
||||
if (mixedPort !== original?.mixedPort) changedPorts.push(mixedPort);
|
||||
if (socksEnabled && socksPort !== original?.socksPort)
|
||||
changedPorts.push(socksPort);
|
||||
if (httpEnabled && httpPort !== original?.httpPort)
|
||||
changedPorts.push(httpPort);
|
||||
if (redirEnabled && redirPort !== original?.redirPort)
|
||||
changedPorts.push(redirPort);
|
||||
if (tproxyEnabled && tproxyPort !== original?.tproxyPort)
|
||||
changedPorts.push(tproxyPort);
|
||||
|
||||
for (const port of changedPorts) {
|
||||
try {
|
||||
const inUse = await isPortInUse(port);
|
||||
if (inUse) {
|
||||
showNotice.error("settings.modals.clashPort.messages.portInUse", {
|
||||
port,
|
||||
});
|
||||
const original = originalPortsRef.current;
|
||||
if (original) {
|
||||
setMixedPort(original.mixedPort);
|
||||
setSocksPort(original.socksPort);
|
||||
@@ -201,7 +214,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
||||
};
|
||||
|
||||
// 提交保存请求
|
||||
await saveSettings({ clashConfig, vergeConfig });
|
||||
saveSettings({ clashConfig, vergeConfig });
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseDialog, Switch } from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { restartCore } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
|
||||
// 定义开发环境的URL列表
|
||||
@@ -134,6 +135,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
),
|
||||
},
|
||||
});
|
||||
await restartCore();
|
||||
await mutateClash();
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,8 +17,7 @@ import { exists } from "@tauri-apps/plugin-fs";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseDialog, DialogRef, Switch } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { BaseDialog, DialogRef, Switch, TooltipIcon } from "@/components/base";
|
||||
import { DEFAULT_HOVER_DELAY } from "@/components/proxy/proxy-group-navigator";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useWindowDecorations } from "@/hooks/use-window";
|
||||
|
||||
@@ -11,8 +11,7 @@ import type { Ref } from "react";
|
||||
import { useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseDialog, DialogRef, Switch } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { BaseDialog, DialogRef, Switch, TooltipIcon } from "@/components/base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { entry_lightweight_mode } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
|
||||
@@ -11,8 +11,7 @@ import { useLockFn } from "ahooks";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseDialog, DialogRef, Switch } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { BaseDialog, DialogRef, Switch, TooltipIcon } from "@/components/base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
|
||||
@@ -30,6 +29,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
enableBuiltinEnhanced: true,
|
||||
proxyLayoutColumn: 6,
|
||||
enableAutoDelayDetection: false,
|
||||
autoDelayDetectionIntervalMinutes: 5,
|
||||
defaultLatencyTest: "",
|
||||
autoLogClean: 2,
|
||||
defaultLatencyTimeout: 10000,
|
||||
@@ -47,6 +47,8 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
||||
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
||||
enableAutoDelayDetection: verge?.enable_auto_delay_detection ?? false,
|
||||
autoDelayDetectionIntervalMinutes:
|
||||
verge?.auto_delay_detection_interval_minutes ?? 5,
|
||||
defaultLatencyTest: verge?.default_latency_test || "",
|
||||
autoLogClean: verge?.auto_log_clean || 0,
|
||||
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
|
||||
@@ -66,6 +68,8 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
enable_builtin_enhanced: values.enableBuiltinEnhanced,
|
||||
proxy_layout_column: values.proxyLayoutColumn,
|
||||
enable_auto_delay_detection: values.enableAutoDelayDetection,
|
||||
auto_delay_detection_interval_minutes:
|
||||
values.autoDelayDetectionIntervalMinutes,
|
||||
default_latency_test: values.defaultLatencyTest,
|
||||
default_latency_timeout: values.defaultLatencyTimeout,
|
||||
auto_log_clean: values.autoLogClean as any,
|
||||
@@ -324,6 +328,44 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"settings.modals.misc.fields.autoDelayDetectionInterval",
|
||||
)}
|
||||
sx={{ maxWidth: "fit-content" }}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
type="number"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
sx={{ width: 160, marginLeft: "auto" }}
|
||||
value={values.autoDelayDetectionIntervalMinutes}
|
||||
disabled={!values.enableAutoDelayDetection}
|
||||
onChange={(e) => {
|
||||
const parsed = parseInt(e.target.value, 10);
|
||||
const intervalMinutes =
|
||||
Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
|
||||
setValues((v) => ({
|
||||
...v,
|
||||
autoDelayDetectionIntervalMinutes: intervalMinutes,
|
||||
}));
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("shared.units.minutes")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText
|
||||
primary={t("settings.modals.misc.fields.defaultLatencyTest")}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,19 +26,15 @@ import { mutate } from "swr";
|
||||
|
||||
import {
|
||||
BaseDialog,
|
||||
BaseFieldset,
|
||||
BaseSplitChipEditor,
|
||||
DialogRef,
|
||||
Switch,
|
||||
TooltipIcon,
|
||||
} from "@/components/base";
|
||||
import { BaseFieldset } from "@/components/base/base-fieldset";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { EditorViewer } from "@/components/profile/editor-viewer";
|
||||
import {
|
||||
useClashConfig,
|
||||
useSystemProxyAddress,
|
||||
useSystemProxyData,
|
||||
} from "@/hooks/use-clash-data";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import {
|
||||
getAutotemProxy,
|
||||
getNetworkInterfacesInfo,
|
||||
@@ -110,9 +106,14 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||
const [hostOptions, setHostOptions] = useState<string[]>([]);
|
||||
|
||||
type SysProxy = Awaited<ReturnType<typeof getSystemProxy>>;
|
||||
const [sysproxy, setSysproxy] = useState<SysProxy>();
|
||||
|
||||
type AutoProxy = Awaited<ReturnType<typeof getAutotemProxy>>;
|
||||
const [autoproxy, setAutoproxy] = useState<AutoProxy>();
|
||||
|
||||
const { clashConfig } = useAppData();
|
||||
|
||||
const {
|
||||
enable_system_proxy: enabled,
|
||||
proxy_auto_config,
|
||||
@@ -148,9 +149,6 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
};
|
||||
|
||||
const { clashConfig } = useClashConfig();
|
||||
const { sysproxy, refreshSysproxy } = useSystemProxyData();
|
||||
|
||||
const prevMixedPortRef = useRef(clashConfig?.mixedPort);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -183,10 +181,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
updateProxy();
|
||||
}, [clashConfig?.mixedPort, value.pac]);
|
||||
|
||||
const systemProxyAddress = useSystemProxyAddress({
|
||||
clashConfig,
|
||||
sysproxy,
|
||||
});
|
||||
const { systemProxyAddress } = useAppData();
|
||||
|
||||
// 为当前状态计算系统代理地址
|
||||
const getSystemProxyAddress = useMemo(() => {
|
||||
@@ -236,7 +231,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
pac_content: pac_file_content ?? DEFAULT_PAC,
|
||||
proxy_host: proxy_host ?? "127.0.0.1",
|
||||
});
|
||||
void refreshSysproxy();
|
||||
getSystemProxy().then((p) => setSysproxy(p));
|
||||
getAutotemProxy().then((p) => setAutoproxy(p));
|
||||
fetchNetworkInterfaces();
|
||||
},
|
||||
|
||||
@@ -12,8 +12,13 @@ import type { Ref } from "react";
|
||||
import { useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseDialog, DialogRef, Switch } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import {
|
||||
BaseDialog,
|
||||
BaseSplitChipEditor,
|
||||
TooltipIcon,
|
||||
DialogRef,
|
||||
Switch,
|
||||
} from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { enhanceProfiles } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
@@ -23,6 +28,12 @@ import { StackModeSwitch } from "./stack-mode-switch";
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
const splitRouteExcludeAddress = (value: string) =>
|
||||
value
|
||||
.split(/[,\n;\r]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -33,6 +44,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
stack: "mixed",
|
||||
device: OS === "macos" ? "utun1024" : "Mihomo",
|
||||
autoRoute: true,
|
||||
routeExcludeAddress: "",
|
||||
autoRedirect: false,
|
||||
autoDetectInterface: true,
|
||||
dnsHijack: ["any:53"],
|
||||
@@ -51,6 +63,9 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
stack: clash?.tun.stack ?? "gvisor",
|
||||
device: clash?.tun.device ?? (OS === "macos" ? "utun1024" : "Mihomo"),
|
||||
autoRoute: nextAutoRoute,
|
||||
routeExcludeAddress: (clash?.tun["route-exclude-address"] ?? []).join(
|
||||
",",
|
||||
),
|
||||
autoRedirect: computedAutoRedirect,
|
||||
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
|
||||
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"],
|
||||
@@ -63,6 +78,9 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
const onSave = useLockFn(async () => {
|
||||
try {
|
||||
const routeExcludeAddress = splitRouteExcludeAddress(
|
||||
values.routeExcludeAddress,
|
||||
);
|
||||
const tun: IConfigData["tun"] = {
|
||||
stack: values.stack,
|
||||
device:
|
||||
@@ -72,6 +90,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
: "Mihomo"
|
||||
: values.device,
|
||||
"auto-route": values.autoRoute,
|
||||
"route-exclude-address": routeExcludeAddress,
|
||||
...(OS === "linux"
|
||||
? {
|
||||
"auto-redirect": values.autoRedirect,
|
||||
@@ -90,13 +109,11 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}),
|
||||
false,
|
||||
);
|
||||
try {
|
||||
await enhanceProfiles();
|
||||
showNotice.success("settings.modals.tun.messages.applied");
|
||||
} catch (err: any) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
setOpen(false);
|
||||
showNotice.success("settings.modals.tun.messages.applied");
|
||||
void enhanceProfiles().catch((err: any) => {
|
||||
showNotice.error(err);
|
||||
});
|
||||
} catch (err: any) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
@@ -123,6 +140,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
: {}),
|
||||
"auto-detect-interface": true,
|
||||
"dns-hijack": ["any:53"],
|
||||
"route-exclude-address": [],
|
||||
"strict-route": false,
|
||||
mtu: 1500,
|
||||
};
|
||||
@@ -130,6 +148,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
stack: "gvisor",
|
||||
device: OS === "macos" ? "utun1024" : "Mihomo",
|
||||
autoRoute: true,
|
||||
routeExcludeAddress: "",
|
||||
autoRedirect: false,
|
||||
autoDetectInterface: true,
|
||||
dnsHijack: ["any:53"],
|
||||
@@ -287,6 +306,26 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<BaseSplitChipEditor
|
||||
value={values.routeExcludeAddress}
|
||||
placeholder="192.168.0.0/16"
|
||||
ariaLabel={t("settings.modals.tun.fields.routeExcludeAddress")}
|
||||
disabled={!values.autoRoute}
|
||||
onChange={(nextValue) =>
|
||||
setValues((v) => ({ ...v, routeExcludeAddress: nextValue }))
|
||||
}
|
||||
renderHeader={(modeToggle) => (
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText
|
||||
primary={t("settings.modals.tun.fields.routeExcludeAddress")}
|
||||
/>
|
||||
{modeToggle ? (
|
||||
<Box sx={{ marginLeft: "auto" }}>{modeToggle}</Box>
|
||||
) : null}
|
||||
</ListItem>
|
||||
)}
|
||||
/>
|
||||
</List>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -6,8 +6,7 @@ import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateGeo } from "tauri-plugin-mihomo-api";
|
||||
|
||||
import { DialogRef, Switch } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { DialogRef, Switch, TooltipIcon } from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { useClashLog } from "@/hooks/use-clash-log";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
|
||||
@@ -2,8 +2,7 @@ import React, { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { mutate } from "swr";
|
||||
|
||||
import { DialogRef, Switch } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { DialogRef, Switch, TooltipIcon } from "@/components/base";
|
||||
import ProxyControlSwitches from "@/components/shared/proxy-control-switches";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Typography } from "@mui/material";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { DialogRef } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { DialogRef, TooltipIcon } from "@/components/base";
|
||||
import {
|
||||
exitApp,
|
||||
exportDiagnosticInfo,
|
||||
|
||||
@@ -4,8 +4,7 @@ import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { DialogRef } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { DialogRef, TooltipIcon } from "@/components/base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { navItems } from "@/pages/_routers";
|
||||
import { copyClashEnv } from "@/services/cmds";
|
||||
|
||||
@@ -11,8 +11,7 @@ import { useLockFn } from "ahooks";
|
||||
import React, { useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { DialogRef, Switch } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { DialogRef, Switch, TooltipIcon } from "@/components/base";
|
||||
import { GuardState } from "@/components/setting/mods/guard-state";
|
||||
import { SysproxyViewer } from "@/components/setting/mods/sysproxy-viewer";
|
||||
import { TunViewer } from "@/components/setting/mods/tun-viewer";
|
||||
|
||||
@@ -19,157 +19,162 @@ export interface TestViewerRef {
|
||||
}
|
||||
|
||||
// create or edit the test item
|
||||
export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { verge, patchVerge } = useVerge();
|
||||
const testList = verge?.test_list ?? [];
|
||||
const { control, ...formIns } = useForm<IVergeTestItem>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
icon: "",
|
||||
url: "",
|
||||
},
|
||||
});
|
||||
|
||||
const patchTestList = async (uid: string, patch: Partial<IVergeTestItem>) => {
|
||||
const newList = testList.map((x) => {
|
||||
if (x.uid === uid) {
|
||||
return { ...x, ...patch };
|
||||
}
|
||||
return x;
|
||||
export const TestViewer = forwardRef<TestViewerRef, Props>(
|
||||
({ onChange }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { verge, patchVerge } = useVerge();
|
||||
const testList = verge?.test_list ?? [];
|
||||
const { control, ...formIns } = useForm<IVergeTestItem>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
icon: "",
|
||||
url: "",
|
||||
},
|
||||
});
|
||||
await patchVerge({ test_list: newList });
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
create: () => {
|
||||
setOpenType("new");
|
||||
setOpen(true);
|
||||
},
|
||||
edit: (item) => {
|
||||
if (item) {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
formIns.setValue(key as any, value);
|
||||
});
|
||||
}
|
||||
setOpenType("edit");
|
||||
setOpen(true);
|
||||
},
|
||||
}));
|
||||
|
||||
const handleOk = useLockFn(
|
||||
formIns.handleSubmit(async (form) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!form.name) throw new Error("`Name` should not be null");
|
||||
if (!form.url) throw new Error("`Url` should not be null");
|
||||
|
||||
let newList;
|
||||
let uid;
|
||||
|
||||
if (form.icon && form.icon.startsWith("<svg")) {
|
||||
// 移除 icon 中的注释
|
||||
if (form.icon) {
|
||||
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, "");
|
||||
}
|
||||
const doc = new DOMParser().parseFromString(
|
||||
form.icon,
|
||||
"image/svg+xml",
|
||||
);
|
||||
if (doc.querySelector("parsererror")) {
|
||||
throw new Error("`Icon`svg format error");
|
||||
}
|
||||
const patchTestList = async (
|
||||
uid: string,
|
||||
patch: Partial<IVergeTestItem>,
|
||||
) => {
|
||||
const newList = testList.map((x) => {
|
||||
if (x.uid === uid) {
|
||||
return { ...x, ...patch };
|
||||
}
|
||||
return x;
|
||||
});
|
||||
await patchVerge({ test_list: newList });
|
||||
};
|
||||
|
||||
if (openType === "new") {
|
||||
uid = nanoid();
|
||||
const item = { ...form, uid };
|
||||
newList = [...testList, item];
|
||||
await patchVerge({ test_list: newList });
|
||||
props.onChange(uid);
|
||||
} else {
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
uid = form.uid;
|
||||
|
||||
await patchTestList(uid, form);
|
||||
props.onChange(uid, form);
|
||||
useImperativeHandle(ref, () => ({
|
||||
create: () => {
|
||||
setOpenType("new");
|
||||
setOpen(true);
|
||||
},
|
||||
edit: (item) => {
|
||||
if (item) {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
formIns.setValue(key as any, value);
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
} catch (err: any) {
|
||||
showNotice.error(err);
|
||||
setLoading(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
setOpenType("edit");
|
||||
setOpen(true);
|
||||
},
|
||||
}));
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
};
|
||||
const handleOk = useLockFn(
|
||||
formIns.handleSubmit(async (form) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!form.name) throw new Error("`Name` should not be null");
|
||||
if (!form.url) throw new Error("`Url` should not be null");
|
||||
|
||||
const text = {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
margin: "normal",
|
||||
variant: "outlined",
|
||||
autoComplete: "off",
|
||||
autoCorrect: "off",
|
||||
} as const;
|
||||
let newList;
|
||||
let uid;
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={
|
||||
openType === "new"
|
||||
? t("tests.modals.test.title.create")
|
||||
: t("tests.modals.test.title.edit")
|
||||
}
|
||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
onOk={handleOk}
|
||||
loading={loading}
|
||||
>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("shared.labels.name")} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
maxRows={5}
|
||||
label={t("shared.labels.icon")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
maxRows={3}
|
||||
label={t("tests.modals.test.fields.url")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
||||
if (form.icon && form.icon.startsWith("<svg")) {
|
||||
// 移除 icon 中的注释
|
||||
if (form.icon) {
|
||||
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, "");
|
||||
}
|
||||
const doc = new DOMParser().parseFromString(
|
||||
form.icon,
|
||||
"image/svg+xml",
|
||||
);
|
||||
if (doc.querySelector("parsererror")) {
|
||||
throw new Error("`Icon`svg format error");
|
||||
}
|
||||
}
|
||||
|
||||
if (openType === "new") {
|
||||
uid = nanoid();
|
||||
const item = { ...form, uid };
|
||||
newList = [...testList, item];
|
||||
await patchVerge({ test_list: newList });
|
||||
onChange(uid);
|
||||
} else {
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
uid = form.uid;
|
||||
|
||||
await patchTestList(uid, form);
|
||||
onChange(uid, form);
|
||||
}
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
} catch (err: any) {
|
||||
showNotice.error(err);
|
||||
setLoading(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
};
|
||||
|
||||
const text = {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
margin: "normal",
|
||||
variant: "outlined",
|
||||
autoComplete: "off",
|
||||
autoCorrect: "off",
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={
|
||||
openType === "new"
|
||||
? t("tests.modals.test.title.create")
|
||||
: t("tests.modals.test.title.edit")
|
||||
}
|
||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
onOk={handleOk}
|
||||
loading={loading}
|
||||
>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("shared.labels.name")} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
maxRows={5}
|
||||
label={t("shared.labels.icon")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
maxRows={3}
|
||||
label={t("tests.modals.test.fields.url")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import {
|
||||
getBaseConfig,
|
||||
getRuleProviders,
|
||||
getRules,
|
||||
} from "tauri-plugin-mihomo-api";
|
||||
|
||||
import {
|
||||
calcuProxies,
|
||||
calcuProxyProviders,
|
||||
getAppUptime,
|
||||
getSystemProxy,
|
||||
} from "@/services/cmds";
|
||||
import { SWR_DEFAULTS, SWR_REALTIME, SWR_SLOW_POLL } from "@/services/config";
|
||||
|
||||
import { useSharedSWRPoller } from "./use-shared-swr-poller";
|
||||
import { useVerge } from "./use-verge";
|
||||
|
||||
export const useProxiesData = () => {
|
||||
const { mutate: globalMutate } = useSWRConfig();
|
||||
const { data, error, isLoading } = useSWR("getProxies", calcuProxies, {
|
||||
...SWR_REALTIME,
|
||||
refreshInterval: 0,
|
||||
onError: (err) => console.warn("[AppData] Proxy fetch failed:", err),
|
||||
});
|
||||
|
||||
const refreshProxy = useCallback(
|
||||
() => globalMutate("getProxies"),
|
||||
[globalMutate],
|
||||
);
|
||||
const pollerRefresh = useCallback(() => {
|
||||
void globalMutate("getProxies");
|
||||
}, [globalMutate]);
|
||||
|
||||
useSharedSWRPoller("getProxies", SWR_REALTIME.refreshInterval, pollerRefresh);
|
||||
|
||||
return {
|
||||
proxies: data,
|
||||
refreshProxy,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useClashConfig = () => {
|
||||
const { mutate: globalMutate } = useSWRConfig();
|
||||
const { data, error, isLoading } = useSWR("getClashConfig", getBaseConfig, {
|
||||
...SWR_SLOW_POLL,
|
||||
refreshInterval: 0,
|
||||
});
|
||||
|
||||
const refreshClashConfig = useCallback(
|
||||
() => globalMutate("getClashConfig"),
|
||||
[globalMutate],
|
||||
);
|
||||
const pollerRefresh = useCallback(() => {
|
||||
void globalMutate("getClashConfig");
|
||||
}, [globalMutate]);
|
||||
|
||||
useSharedSWRPoller(
|
||||
"getClashConfig",
|
||||
SWR_SLOW_POLL.refreshInterval,
|
||||
pollerRefresh,
|
||||
);
|
||||
|
||||
return {
|
||||
clashConfig: data,
|
||||
refreshClashConfig,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProxyProvidersData = () => {
|
||||
const { data, error, isLoading, mutate } = useSWR(
|
||||
"getProxyProviders",
|
||||
calcuProxyProviders,
|
||||
SWR_DEFAULTS,
|
||||
);
|
||||
|
||||
const refreshProxyProviders = useCallback(() => mutate(), [mutate]);
|
||||
|
||||
return {
|
||||
proxyProviders: data || {},
|
||||
refreshProxyProviders,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useRuleProvidersData = () => {
|
||||
const { data, error, isLoading, mutate } = useSWR(
|
||||
"getRuleProviders",
|
||||
getRuleProviders,
|
||||
SWR_DEFAULTS,
|
||||
);
|
||||
|
||||
const refreshRuleProviders = useCallback(() => mutate(), [mutate]);
|
||||
|
||||
return {
|
||||
ruleProviders: data?.providers || {},
|
||||
refreshRuleProviders,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useRulesData = () => {
|
||||
const { data, error, isLoading, mutate } = useSWR(
|
||||
"getRules",
|
||||
getRules,
|
||||
SWR_DEFAULTS,
|
||||
);
|
||||
|
||||
const refreshRules = useCallback(() => mutate(), [mutate]);
|
||||
|
||||
return {
|
||||
rules: data?.rules || [],
|
||||
refreshRules,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSystemProxyData = () => {
|
||||
const { data, error, isLoading, mutate } = useSWR(
|
||||
"getSystemProxy",
|
||||
getSystemProxy,
|
||||
SWR_DEFAULTS,
|
||||
);
|
||||
|
||||
const refreshSysproxy = useCallback(() => mutate(), [mutate]);
|
||||
|
||||
return {
|
||||
sysproxy: data,
|
||||
refreshSysproxy,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
type ClashConfig = Awaited<ReturnType<typeof getBaseConfig>>;
|
||||
type SystemProxy = Awaited<ReturnType<typeof getSystemProxy>>;
|
||||
|
||||
interface SystemProxyAddressParams {
|
||||
clashConfig?: ClashConfig | null;
|
||||
sysproxy?: SystemProxy | null;
|
||||
}
|
||||
|
||||
export const useSystemProxyAddress = ({
|
||||
clashConfig,
|
||||
sysproxy,
|
||||
}: SystemProxyAddressParams) => {
|
||||
const { verge } = useVerge();
|
||||
|
||||
return useMemo(() => {
|
||||
if (!verge || !clashConfig) return "-";
|
||||
|
||||
const isPacMode = verge.proxy_auto_config ?? false;
|
||||
|
||||
if (isPacMode) {
|
||||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||||
const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
||||
return [proxyHost, proxyPort].join(":");
|
||||
}
|
||||
|
||||
const systemServer = sysproxy?.server;
|
||||
if (systemServer && systemServer !== "-" && !systemServer.startsWith(":")) {
|
||||
return systemServer;
|
||||
}
|
||||
|
||||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||||
const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
||||
return [proxyHost, proxyPort].join(":");
|
||||
}, [clashConfig, sysproxy, verge]);
|
||||
};
|
||||
|
||||
export const useAppUptime = () => {
|
||||
const { data, error, isLoading } = useSWR("appUptime", getAppUptime, {
|
||||
...SWR_DEFAULTS,
|
||||
refreshInterval: 3000,
|
||||
errorRetryCount: 1,
|
||||
});
|
||||
|
||||
return {
|
||||
uptime: data || 0,
|
||||
error,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export const useRefreshAll = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
return useCallback(async () => {
|
||||
await Promise.all([
|
||||
mutate("getProxies"),
|
||||
mutate("getClashConfig"),
|
||||
mutate("getRules"),
|
||||
mutate("getSystemProxy"),
|
||||
mutate("getProxyProviders"),
|
||||
mutate("getRuleProviders"),
|
||||
]);
|
||||
}, [mutate]);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
76
src/hooks/use-current-proxy.ts
Normal file
76
src/hooks/use-current-proxy.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
|
||||
// 定义代理组类型
|
||||
interface ProxyGroup {
|
||||
name: string;
|
||||
now: string;
|
||||
}
|
||||
|
||||
// 获取当前代理节点信息的自定义Hook
|
||||
export const useCurrentProxy = () => {
|
||||
// 从AppDataProvider获取数据
|
||||
const { proxies, clashConfig, refreshProxy } = useAppData();
|
||||
|
||||
// 获取当前模式
|
||||
const currentMode = clashConfig?.mode?.toLowerCase() || "rule";
|
||||
|
||||
// 获取当前代理节点信息
|
||||
const currentProxyInfo = useMemo(() => {
|
||||
if (!proxies) return { currentProxy: null, primaryGroupName: null };
|
||||
|
||||
const { global, groups, records } = proxies;
|
||||
|
||||
// 默认信息
|
||||
let primaryGroupName = "GLOBAL";
|
||||
let currentName = global?.now;
|
||||
|
||||
// 在规则模式下,寻找主要代理组(通常是第一个或者名字包含特定关键词的组)
|
||||
if (currentMode === "rule" && groups.length > 0) {
|
||||
// 查找主要的代理组(优先级:包含关键词 > 第一个非GLOBAL组)
|
||||
const primaryKeywords = [
|
||||
"auto",
|
||||
"select",
|
||||
"proxy",
|
||||
"节点选择",
|
||||
"自动选择",
|
||||
];
|
||||
const primaryGroup =
|
||||
groups.find((group: ProxyGroup) =>
|
||||
primaryKeywords.some((keyword) =>
|
||||
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||
),
|
||||
) || groups.filter((g: ProxyGroup) => g.name !== "GLOBAL")[0];
|
||||
|
||||
if (primaryGroup) {
|
||||
primaryGroupName = primaryGroup.name;
|
||||
currentName = primaryGroup.now;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找不到当前节点,返回null
|
||||
if (!currentName) return { currentProxy: null, primaryGroupName };
|
||||
|
||||
// 获取完整的节点信息
|
||||
const currentProxy = records[currentName] || {
|
||||
name: currentName,
|
||||
type: "Unknown",
|
||||
udp: false,
|
||||
xudp: false,
|
||||
tfo: false,
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
history: [],
|
||||
};
|
||||
|
||||
return { currentProxy, primaryGroupName };
|
||||
}, [proxies, currentMode]);
|
||||
|
||||
return {
|
||||
currentProxy: currentProxyInfo.currentProxy,
|
||||
primaryGroupName: currentProxyInfo.primaryGroupName,
|
||||
mode: currentMode,
|
||||
refreshProxy,
|
||||
};
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { mutate, type MutatorCallback } from "swr";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { type Message, type MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
||||
|
||||
export const RECONNECT_DELAY_MS = 500;
|
||||
export const RECONNECT_DELAY_MS = 100;
|
||||
|
||||
type NextFn<T> = (error?: any, data?: T | MutatorCallback<T>) => void;
|
||||
|
||||
|
||||
22
src/hooks/use-network.ts
Normal file
22
src/hooks/use-network.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user