mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 22:00:56 +08:00
Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c423fe90e2 | ||
|
|
623461b649 | ||
|
|
1f9ba4de5c | ||
|
|
10f792b8bc | ||
|
|
357b64e9a0 | ||
|
|
f3ad6cee41 | ||
|
|
972310ea4e | ||
|
|
f5315fd737 | ||
|
|
c40c1960ed | ||
|
|
c8ae37811c | ||
|
|
15f05748a5 | ||
|
|
0a0a25913b | ||
|
|
945999889c | ||
|
|
ce7e1d6456 | ||
|
|
72c4b9de8a | ||
|
|
a8257e8cb2 | ||
|
|
15d3c765ac | ||
|
|
e7d3d7e0ae | ||
|
|
c0f3d35e13 | ||
|
|
fc30fab9cd | ||
|
|
a67e8388a9 | ||
|
|
ac7307b1f7 | ||
|
|
4068e5ec9c | ||
|
|
ea71181692 | ||
|
|
9b46c1a991 | ||
|
|
26acce94a4 | ||
|
|
2a0e1e206e | ||
|
|
f83d1d1582 | ||
|
|
77a090d5eb | ||
|
|
a9561e0ded | ||
|
|
dd505e4d58 | ||
|
|
dd0e9d4e1b | ||
|
|
8bc451ff08 | ||
|
|
032e5bf32e | ||
|
|
41ba8f6c7a | ||
|
|
799783d3ef | ||
|
|
77fb40506f | ||
|
|
5858f05c13 | ||
|
|
0432cad112 | ||
|
|
47ec8a348c | ||
|
|
eef348b8dc | ||
|
|
e78a02d0ba | ||
|
|
4e54b61380 | ||
|
|
cc39b2734e | ||
|
|
16eaccb10e | ||
|
|
dbc6f54a77 | ||
|
|
9bd5950f60 | ||
|
|
2c7c416f60 | ||
|
|
3eb0ab4197 | ||
|
|
b7cb511032 | ||
|
|
18850bb51a | ||
|
|
4d718e0cea | ||
|
|
3a2f567055 | ||
|
|
4f474e1098 | ||
|
|
22eac1a832 | ||
|
|
07738dd82d | ||
|
|
f07d508018 | ||
|
|
b591a90100 | ||
|
|
db3cfdf66f | ||
|
|
5cf3e1a817 | ||
|
|
25cfd162f6 | ||
|
|
7001ef2030 | ||
|
|
cbcab72a7c | ||
|
|
d05b1c3130 | ||
|
|
fbdcda9a7f | ||
|
|
0ebc482698 | ||
|
|
09969d95de | ||
|
|
689042df60 | ||
|
|
564fe15df2 | ||
|
|
30015dfd67 | ||
|
|
698c47da69 | ||
|
|
46b7a520cd | ||
|
|
cadd6c3497 | ||
|
|
c4682ab6e9 | ||
|
|
59594855b8 | ||
|
|
44b7af0c6e | ||
|
|
80f550d67e | ||
|
|
44f085604a | ||
|
|
1144f8017a | ||
|
|
d8fa17a3e7 | ||
|
|
bf868e0ae2 | ||
|
|
e1dac63f69 | ||
|
|
2657234761 | ||
|
|
4a345fdd41 | ||
|
|
fee6a586d7 | ||
|
|
7c738483b7 | ||
|
|
c3d8ed28a2 | ||
|
|
f47b4b961b | ||
|
|
7763abf6c2 | ||
|
|
9d9d078346 |
@@ -0,0 +1,58 @@
|
||||
name: I18N / 多语言相关
|
||||
title: "[I18N] "
|
||||
description: 用于多语言翻译、国际化相关问题或建议 / For issues or suggestions related to translations and internationalization
|
||||
labels: ["I18n"]
|
||||
type: "Task"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## I18N 相关问题/建议
|
||||
请用此模板提交翻译错误、缺失、建议或新增语言请求。
|
||||
Please use this template for translation errors, missing translations, suggestions, or new language requests.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述 / Description
|
||||
description: 详细描述你的 I18N 问题或建议 / Please describe your I18N issue or suggestion in detail
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: language
|
||||
attributes:
|
||||
label: 相关语言 / Language
|
||||
description: 例如 zh, en, jp, ru, ... / e.g. zh, en, jp, ru, ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: 建议或修正内容 / Suggestion or Correction
|
||||
description: 如果是翻译修正或建议,请填写建议的内容 / If this is a translation correction or suggestion, please provide the suggested content
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: i18n-type
|
||||
attributes:
|
||||
label: 问题类型 / Issue Type
|
||||
description: 请选择适用类型(可多选) / Please select the applicable type(s)
|
||||
options:
|
||||
- label: 翻译错误 / Translation error
|
||||
- label: 翻译缺失 / Missing translation
|
||||
- label: 建议优化 / Suggestion
|
||||
- label: 新增语言 / New language
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: verge-version
|
||||
attributes:
|
||||
label: 软件版本 / Verge Version
|
||||
description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using
|
||||
validations:
|
||||
required: true
|
||||
+52
-11
@@ -1,10 +1,24 @@
|
||||
name: Alpha Build
|
||||
|
||||
on:
|
||||
# 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境
|
||||
# 所以不再使用 workflow_dispatch 触发
|
||||
# 应当通过 git tag 来触发构建
|
||||
# TODO 手动控制版本号
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*-alpha*"
|
||||
# inputs:
|
||||
# tag_name:
|
||||
# description: "Alpha tag name (e.g. v1.2.3-alpha.1)"
|
||||
# required: true
|
||||
# type: string
|
||||
|
||||
# push:
|
||||
# # 应当限制在 dev 分支上触发发布。
|
||||
# branches:
|
||||
# - dev
|
||||
# # 应当限制 v*.*.*-alpha* 的 tag 来触发发布。
|
||||
# tags:
|
||||
# - "v*.*.*-alpha*"
|
||||
permissions: write-all
|
||||
env:
|
||||
TAG_NAME: alpha
|
||||
@@ -15,8 +29,37 @@ concurrency:
|
||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||
|
||||
jobs:
|
||||
check_alpha_tag:
|
||||
name: Check Alpha Tag package.json Version Consistency
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check tag and package.json version
|
||||
id: check_tag
|
||||
run: |
|
||||
TAG_REF="${GITHUB_REF##*/}"
|
||||
echo "Current tag: $TAG_REF"
|
||||
if [[ ! "$TAG_REF" =~ -alpha ]]; then
|
||||
echo "Current tag is not an alpha tag."
|
||||
exit 1
|
||||
fi
|
||||
PKG_VERSION=$(jq -r .version package.json)
|
||||
echo "package.json version: $PKG_VERSION"
|
||||
if [[ "$PKG_VERSION" != *alpha* ]]; then
|
||||
echo "package.json version is not an alpha version."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
|
||||
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Alpha tag and package.json version are consistent."
|
||||
|
||||
delete_old_assets:
|
||||
name: Delete Old Alpha Release Assets and Tags
|
||||
needs: check_alpha_tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete Old Alpha Tags Except Latest
|
||||
@@ -243,8 +286,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: false
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -265,7 +307,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
@@ -320,7 +362,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -335,7 +377,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
@@ -448,8 +490,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -464,7 +505,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -191,20 +191,21 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
@@ -260,21 +261,23 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
@@ -388,22 +391,23 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Clippy Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
- os: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Build Web Assets
|
||||
run: pnpm run web:build
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
|
||||
- name: Run Clippy
|
||||
run: cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Cross Platform Cargo Check
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# pull_request:
|
||||
# push:
|
||||
# branches: [main, dev]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cargo-check:
|
||||
# Treat all Rust compiler warnings as errors
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
|
||||
- name: Cargo Check (deny warnings)
|
||||
working-directory: src-tauri
|
||||
run: |
|
||||
cargo check --target ${{ matrix.target }} --workspace --all-features
|
||||
@@ -42,8 +42,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -58,7 +57,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: Check Formatting
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install Rust stable and rustfmt
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: run cargo fmt
|
||||
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
|
||||
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: pnpm format:check
|
||||
|
||||
# taplo:
|
||||
# name: taplo (.toml files)
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
|
||||
# - name: install Rust stable
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# - name: install taplo-cli
|
||||
# uses: taiki-e/install-action@v2
|
||||
# with:
|
||||
# tool: taplo-cli
|
||||
|
||||
# - run: taplo fmt --check --diff
|
||||
@@ -1,7 +1,16 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# ! 为了避免重复发布版本,应当通过独特 git tag 触发。
|
||||
# ! 不再使用 workflow_dispatch 触发。
|
||||
# workflow_dispatch:
|
||||
push:
|
||||
# 应当限制在 main 分支上触发发布。
|
||||
branches:
|
||||
- main
|
||||
# 应当限制 v*.*.* 的 tag 触发发布。
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
@@ -12,7 +21,28 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
check_tag_version:
|
||||
name: Check Release Tag and package.json Version Consistency
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check tag and package.json version
|
||||
run: |
|
||||
TAG_REF="${GITHUB_REF##*/}"
|
||||
echo "Current tag: $TAG_REF"
|
||||
PKG_VERSION=$(jq -r .version package.json)
|
||||
echo "package.json version: $PKG_VERSION"
|
||||
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
|
||||
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Tag and package.json version are consistent."
|
||||
|
||||
release:
|
||||
name: Release Build
|
||||
needs: check_tag_version
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -43,8 +73,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: false
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -65,7 +94,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
@@ -88,6 +117,7 @@ jobs:
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
release-for-linux-arm:
|
||||
name: Release Build for Linux ARM
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -113,7 +143,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -128,7 +158,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: "Setup for linux"
|
||||
run: |-
|
||||
@@ -209,6 +239,7 @@ jobs:
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
|
||||
release-for-fixed-webview2:
|
||||
name: Release Build for Fixed WebView2
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -231,8 +262,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -247,7 +277,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
@@ -303,6 +333,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-update:
|
||||
name: Release Update
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release-for-linux-arm]
|
||||
steps:
|
||||
@@ -353,6 +384,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
submit-to-winget:
|
||||
name: Submit to Winget
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-update]
|
||||
steps:
|
||||
|
||||
+17
-9
@@ -2,17 +2,25 @@
|
||||
|
||||
#pnpm pretty-quick --staged
|
||||
|
||||
# 运行 clippy fmt
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "rustfmt failed to format the code. Please fix the issues and try again."
|
||||
exit 1
|
||||
if git diff --cached --name-only | grep -q '^src/'; then
|
||||
pnpm format:check
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Code format check failed in src/. Please fix formatting issues."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
cd ..
|
||||
|
||||
git add .
|
||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "rustfmt failed to format the code. Please fix the issues and try again."
|
||||
exit 1
|
||||
fi
|
||||
cd ..
|
||||
fi
|
||||
|
||||
#git add .
|
||||
|
||||
# 允许提交
|
||||
exit 0
|
||||
|
||||
+22
-8
@@ -1,13 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 运行 clippy
|
||||
# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
|
||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
cargo clippy --manifest-path ./src-tauri/Cargo.toml
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Clippy found issues in src-tauri. Please fix them before pushing."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 如果 clippy 失败,阻止 push
|
||||
# if [ $? -ne 0 ]; then
|
||||
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
|
||||
# exit 1
|
||||
# fi
|
||||
remote_name="$1"
|
||||
remote_url=$(git remote get-url "$remote_name")
|
||||
|
||||
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
|
||||
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
|
||||
echo "[pre-push] Running pnpm format:check..."
|
||||
|
||||
pnpm format:check
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Code format check failed. Please fix formatting before pushing."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "[pre-push] Not pushing to target repo. Skipping format check."
|
||||
fi
|
||||
|
||||
# 允许 push
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# README.md
|
||||
# UPDATELOG.md
|
||||
# CONTRIBUTING.md
|
||||
|
||||
pnpm-lock.yaml
|
||||
|
||||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"experimentalOperatorPosition": "start"
|
||||
}
|
||||
+18
-10
@@ -33,12 +33,15 @@ npm install pnpm -g
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
Install node packages
|
||||
|
||||
```shell
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Install apt packages ONLY for Ubuntu
|
||||
|
||||
```shell
|
||||
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
```
|
||||
@@ -49,9 +52,9 @@ You have two options for downloading the clash binary:
|
||||
|
||||
- Automatically download it via the provided script:
|
||||
```shell
|
||||
pnpm run check
|
||||
pnpm run prebuild
|
||||
# Use '--force' to force update to the latest version
|
||||
# pnpm run check --force
|
||||
# pnpm run prebuild --force
|
||||
```
|
||||
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
|
||||
@@ -105,20 +108,25 @@ pnpm portable
|
||||
|
||||
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
|
||||
|
||||
1. Code style formatting
|
||||
1. Code quailty checks
|
||||
|
||||
```bash
|
||||
# For rust backend
|
||||
$ clash-verge-rev: pnpm clippy
|
||||
# For frontend (not yet).
|
||||
```
|
||||
|
||||
2. Code style formatting
|
||||
|
||||
```bash
|
||||
# For rust backend
|
||||
$ clash-verge-rev: cd src-tauri
|
||||
$ clash-verge-rev/src-tauri: cargo fmt
|
||||
# For frontend
|
||||
$ clash-verge-rev: pnpm format:check
|
||||
$ clash-verge-rev: pnpm format
|
||||
```
|
||||
|
||||
2. Code quailty checks
|
||||
|
||||
```bash
|
||||
$ clash-verge-rev: pnpm clippy
|
||||
```
|
||||
|
||||
|
||||
Once you have made your changes:
|
||||
|
||||
1. Fork the repository.
|
||||
|
||||
@@ -18,10 +18,18 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
||||
## Install
|
||||
|
||||
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
||||
Go to the [release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
||||
Go to the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
||||
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
### 安装说明和常见问题,请到[文档页](https://clash-verge-rev.github.io/)查看:[Doc](https://clash-verge-rev.github.io/)
|
||||
#### 我应当怎样选择发行版
|
||||
|
||||
| 版本 | 特征 | 链接 |
|
||||
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
||||
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
||||
|
||||
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
|
||||
|
||||
---
|
||||
|
||||
@@ -41,11 +49,12 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
- 解锁流媒体及 ChatGPT
|
||||
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
|
||||
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
|
||||
|
||||
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
|
||||
|
||||
🧩 YXVM 独立服务器优势:
|
||||
|
||||
- 🌎 优质网络,回程优化,下载快到飞起
|
||||
- 🔧 物理机独享资源,非VPS可比,性能拉满
|
||||
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
|
||||
|
||||
+196
-129
@@ -1,143 +1,192 @@
|
||||
## v2.3.1
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 增加配置文件校验,修复从古老版本升级上来的"No such file or directory (os error 2)"错误
|
||||
- 修复扩展脚本转义错误
|
||||
- 修复 macOS Intel X86 架构构建错误导致无法运行
|
||||
- 修复 Linux 下界面边框白边问题
|
||||
- 修复 托盘 无响应问题
|
||||
- 修复 托盘 无法从轻量模式退出并恢复窗口
|
||||
- 修复 快速切换订阅可能导致的卡死问题
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- 新增 window-state 窗口状态管理和恢复
|
||||
|
||||
### 🚀 优化改进
|
||||
|
||||
- 优化 托盘 统一响应
|
||||
- 优化 静默启动+自启动轻量模式 运行方式
|
||||
- 升级依赖
|
||||
|
||||
## v2.3.0
|
||||
|
||||
尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。
|
||||
**发行代号:御**
|
||||
代号释义: 「御」,象征掌控与守护,寓意本次版本对系统稳定性、安全性与用户体验的全面驾驭与提升。
|
||||
|
||||
#### ⚠️ 已知问题
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||
- 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆。
|
||||
- MacOS 下卸载服务后需手动重启软件才能与内核通信。
|
||||
尽管 `external-controller` 密钥现已自动补全默认值且不允许为空,**仍建议手动修改密钥以提高安全性**。
|
||||
|
||||
### 2.3.0 相对于 2.2.3
|
||||
#### 🐞 修复问题
|
||||
- 首页"代理模式"快速切换导致的卡死问题
|
||||
- 解锁测试报错信息
|
||||
- Macos 快捷键关闭窗口无法启用自动轻量模式
|
||||
- 静默启动异常窗口创建和关闭流程
|
||||
- Windows 错误的全局快捷键 `Ctrl+Q` 注册
|
||||
- Vless URL 解码时网络类型错误
|
||||
- 切换自定义代理地址导致系统代理状态异常
|
||||
- Macos TUN 默认无效网卡名称
|
||||
- 托盘更改订阅后 UI 不同步的问题
|
||||
- 修复提权漏洞,改用带认证的 IPC 通信
|
||||
- 编辑器中连字符问题
|
||||
- 安装服务模式后无法立即开启 TUN 模式
|
||||
- 同步更新多语言翻译
|
||||
- 修复 .window-state.json 无法删除的问题
|
||||
- 无法修改配置更新 HTTP 请求超时
|
||||
- 修复 getDelayFix 钩子问题
|
||||
- 使用外部扩展脚本覆写代理组时首页无法显示代理组
|
||||
- 导出诊断 Verge 版本与设置页面不同步
|
||||
### ⚠️ 已知问题
|
||||
|
||||
#### ✨ 新增功能
|
||||
- Mihomo(Meta)内核升级至 1.19.10
|
||||
- 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好
|
||||
- 关闭系统代理时关闭已建立的网络连接
|
||||
- 托盘显示当前轻量模式状态
|
||||
- Webdav 请求加入 UA
|
||||
- Webdav 支持目录重定向
|
||||
- Webdav 备份目录检查和文件上传重试机制
|
||||
- 系统代理守卫可检查意外设置变更并恢复
|
||||
- 定时自动订阅更新也能自动回退使用代理
|
||||
- 订阅请求超时机制,防止订阅更新的时候卡死
|
||||
- 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示
|
||||
- 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死
|
||||
- 更新依赖,替换弃用元素
|
||||
- 首页当前节点增加排序功能
|
||||
- DNS 覆写下增加 Hosts 设置功能
|
||||
- 修复服务模式安装后无法立即开启 TUN 模式的问题
|
||||
- 支持手动卸载服务模式,回退到 Sidecar 模式
|
||||
- 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持
|
||||
- 卸载服务的按钮
|
||||
- 添加了Zashboard的一键跳转URL
|
||||
- 使用操作系统默认的窗口管理器
|
||||
- 切换、升级、重启内核的状态管理
|
||||
- 更精细化控制自动日志清理,新增1天选项
|
||||
- Winodws 快捷键名称改为 `Clash Verge`
|
||||
- 配置加载阶段自动补全 external-controller secret 字段。
|
||||
- 添加 TCP Concurrency (TCP并发)
|
||||
- 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
|
||||
- macOS:
|
||||
|
||||
#### 🚀 优化改进
|
||||
- 系统代理 Bypass 设置
|
||||
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题
|
||||
- 切换到规则页面时自动刷新规则数据
|
||||
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新
|
||||
- 编辑非激活订阅的时候不在触发当前订阅配置重载
|
||||
- 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程
|
||||
- 优化系统代理设置更新逻辑
|
||||
- 重构前端通知系统分离通知线程防止前端卡死
|
||||
- 优化网络请求和错误处理
|
||||
- 重构通知系统
|
||||
- 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题
|
||||
- MacOS 下默认关闭托盘速率显示
|
||||
- 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验
|
||||
- 优化了其他语言的翻译问题
|
||||
- Mihomo 内核默认日志等级为 warn
|
||||
- Clash Verge Rev 应用默认日志等级为 warn
|
||||
- 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题
|
||||
- 对轮询检测机制进行了优化,引入洗牌算法来增强随机性
|
||||
- 对获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息
|
||||
- 优化窗口状态初始化逻辑和添加缺失的权限设置
|
||||
- 异步化配置:优化端口查找和配置保存逻辑
|
||||
- 重构事件通知机制到独立线程,避免前端卡死
|
||||
- 优化端口设置,每个端口可随机设置端口号
|
||||
- 优化了保存机制,使用平滑函数,防止客户端卡死
|
||||
- 优化端口设置退出和保存机制
|
||||
- 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖
|
||||
- 修改 端口检测范围 (1111-65536)
|
||||
- 配置文件缺失 secret 字段时自动填充默认值 set-your-secret
|
||||
- 优化异步处理,防止部分组件 UI 阻塞
|
||||
- 关闭 DNS 启用
|
||||
- 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204
|
||||
- MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
|
||||
- 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
|
||||
- 彩色 Tray 图标颜色偏淡;
|
||||
|
||||
#### 🗑️ 移除内容
|
||||
- 窗口状态管理器
|
||||
- Webdav 跨平台备份恢复限制
|
||||
- 已确认窗口状态管理器存在上游缺陷,已暂时移除窗口大小与位置记忆功能。
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复首页“代理模式”快速切换导致的卡死问题
|
||||
- 修复 MacOS 快捷键关闭窗口无法启用自动轻量模式
|
||||
- 修复静默启动异常窗口的创建与关闭流程
|
||||
- 修复 Windows 下错误注册的全局快捷键 `Ctrl+Q`
|
||||
- 修复解锁测试报错信息与 VLESS URL 解码时的网络类型错误
|
||||
- 修复切换自定义代理地址后系统代理状态异常
|
||||
- 修复 macOS TUN 默认无效网卡名称
|
||||
- 修复更改订阅后托盘 UI 不同步的问题
|
||||
- 修复服务模式安装后无法立即开启 TUN 模式
|
||||
- 修复无法删除 `.window-state.json`
|
||||
- 修复无法修改配置更新 HTTP 请求超时问题
|
||||
- 修复 `getDelayFix` 钩子异常
|
||||
- 修复外部扩展脚本覆写代理组时首页无法显示代理组
|
||||
- 修复 Verge 导出诊断版本与设置页面不同步
|
||||
- 修复切换语言时设置页面可能加载失败
|
||||
- 修复编辑器中连字符处理问题
|
||||
- 修复提权漏洞,改用带认证的 IPC 通信机制
|
||||
- 修复静默启动无法使用自动轻量模式
|
||||
- 修复 JS 脚本转义特殊字符报错
|
||||
- 修复 macOS 静默启动时异常启动 Dock 栏图标
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.10**
|
||||
- 支持设置代理地址为非 `127.0.0.1`,提升 WSL 兼容性
|
||||
- 系统代理守卫:可检测意外变更并自动恢复
|
||||
- 托盘新增当前轻量模式状态显示
|
||||
- 关闭系统代理时同时断开已建立的连接
|
||||
- 新增 WebDAV 功能:
|
||||
|
||||
- 加入 UA 请求头
|
||||
- 支持目录重定向
|
||||
- 备份目录检查与上传重试机制
|
||||
|
||||
- 自动订阅更新机制:
|
||||
|
||||
- 加入请求超时机制防止卡死
|
||||
- 支持在代理状态下自动重试订阅更新
|
||||
- 支持订阅卡片点击切换下次自动更新时间,并显示更新结果提示
|
||||
|
||||
- DNS 设置新增 Hosts 配置功能
|
||||
- 首页代理节点支持排序
|
||||
- 支持服务模式手动卸载,回退至 Sidecar 模式
|
||||
- 核心状态管理支持切换、升级、重启
|
||||
- 配置加载阶段自动补全 `external-controller secret`
|
||||
- 新增日志自动清理周期选项(含1天)
|
||||
- 新增 Zashboard 一键跳转入口
|
||||
- 使用系统默认窗口管理器
|
||||
|
||||
### 🚀 优化改进
|
||||
|
||||
- **系统相关:**
|
||||
|
||||
- 系统代理 Bypass 设置优化
|
||||
- 优化代理设置更新逻辑与守卫机制
|
||||
- Windows 启动方式调整为 Startup 文件夹,解决管理员模式下自启问题
|
||||
|
||||
- **性能与稳定性:**
|
||||
|
||||
- 全面异步化处理配置加载、UI 启动、事件通知等关键流程,解决卡顿问题
|
||||
- 优化 MihomoManager 实现与窗口创建流程
|
||||
- 改进内核日志等级为 `warn`,减少噪音输出
|
||||
- 重构主进程与通知系统,提升响应性与分离度
|
||||
- 优化网络请求与错误处理机制
|
||||
- 添加网络管理器防止资源竞争引发 UI 卡死
|
||||
- 优化配置文件加载内存使用
|
||||
- 优化缓存 Mihomo proxy 和 providers 信息内存使用
|
||||
|
||||
- **前端与界面体验:**
|
||||
|
||||
- 切换规则页自动刷新数据
|
||||
- 非激活订阅编辑时不再触发配置重载
|
||||
- 优化托盘速率显示,macOS 下默认关闭
|
||||
- Windows 快捷键名称更名为 `Clash Verge`
|
||||
- 更新失败可回退至使用代理重试
|
||||
- 支持异步端口查找与保存,端口支持随机生成
|
||||
- 修改端口检测范围至 `1111-65536`
|
||||
- 优化保存机制,使用平滑函数防止卡顿
|
||||
|
||||
- **配置增强与安全性:**
|
||||
|
||||
- 配置缺失 `secret` 字段时自动补全为 `set-your-secret`
|
||||
- 强制为 Mihomo 配置补全 `external-controller-cors` 字段(默认不允许跨域,限制本地访问)计划后续支持自定义 cors
|
||||
- 优化窗口权限设置与状态初始化逻辑
|
||||
- 网络延迟测试替换为 HTTPS 协议:`https://cp.cloudflare.com/generate_204`
|
||||
- 优化 IP 信息获取流程,添加去重机制与轮询检测算法
|
||||
|
||||
- 同步修复翻译错误与不一致项,优化整体语言体验
|
||||
- 加强语言切换后的页面稳定性,避免加载异常
|
||||
|
||||
### 🗑️ 移除内容
|
||||
|
||||
- 窗口状态管理器(上游存在缺陷)
|
||||
- WebDAV 跨平台备份恢复限制
|
||||
|
||||
---
|
||||
|
||||
## v2.2.3
|
||||
|
||||
#### 已知问题
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
|
||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
|
||||
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
|
||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
|
||||
|
||||
### 2.2.3 相对于 2.2.2
|
||||
|
||||
#### 修复了:
|
||||
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
|
||||
- “开机自启”和“DNS覆写”开关跳动问题
|
||||
- 自定义托盘图标未能应用更改
|
||||
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
|
||||
- MacOS 托盘速率显示不全
|
||||
- Linux 在系统服务模式下无法拉起 Mihomo 内核
|
||||
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
|
||||
- 相同节点名称可能导致的页面渲染出错
|
||||
- URL Schemes被截断的问题
|
||||
- 首页流量统计卡更好的时间戳范围
|
||||
- 静默启动无法触发自动轻量化计时器
|
||||
|
||||
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
|
||||
- “开机自启”和“DNS覆写”开关跳动问题
|
||||
- 自定义托盘图标未能应用更改
|
||||
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
|
||||
- MacOS 托盘速率显示不全
|
||||
- Linux 在系统服务模式下无法拉起 Mihomo 内核
|
||||
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
|
||||
- 相同节点名称可能导致的页面渲染出错
|
||||
- URL Schemes被截断的问题
|
||||
- 首页流量统计卡更好的时间戳范围
|
||||
- 静默启动无法触发自动轻量化计时器
|
||||
|
||||
#### 新增了:
|
||||
- Mihomo(Meta)内核升级至 1.19.4
|
||||
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
|
||||
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
|
||||
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
|
||||
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
|
||||
- 代理组显示节点数量
|
||||
- 统一运行模式检测,支持管理员模式下开启TUN模式
|
||||
- 托盘切换代理模式会根据设置自动断开之前连接
|
||||
- 如订阅获取失败回退使用Clash内核代理再次尝试
|
||||
|
||||
- Mihomo(Meta)内核升级至 1.19.4
|
||||
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
|
||||
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
|
||||
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
|
||||
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
|
||||
- 代理组显示节点数量
|
||||
- 统一运行模式检测,支持管理员模式下开启TUN模式
|
||||
- 托盘切换代理模式会根据设置自动断开之前连接
|
||||
- 如订阅获取失败回退使用Clash内核代理再次尝试
|
||||
|
||||
#### 移除了:
|
||||
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
|
||||
|
||||
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
|
||||
|
||||
#### 优化了:
|
||||
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
||||
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
|
||||
- 优化首页流量图表代码,调整图表文字边距
|
||||
- MacOS 托盘速率更好的显示样式和更新逻辑
|
||||
- 首页仅在有流量图表时显示流量图表区域
|
||||
- 更新DNS默认覆写配置
|
||||
- 移除测试目录,简化资源初始化逻辑
|
||||
|
||||
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
||||
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
|
||||
- 优化首页流量图表代码,调整图表文字边距
|
||||
- MacOS 托盘速率更好的显示样式和更新逻辑
|
||||
- 首页仅在有流量图表时显示流量图表区域
|
||||
- 更新DNS默认覆写配置
|
||||
- 移除测试目录,简化资源初始化逻辑
|
||||
|
||||
## v2.2.2
|
||||
|
||||
@@ -148,23 +197,29 @@
|
||||
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
||||
|
||||
#### 已知问题
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
|
||||
### 2.2.2 相对于 2.2.1(已下架不再提供)
|
||||
|
||||
#### 修复了:
|
||||
- 弹黑框的问题(原因是服务崩溃触发重装机制)
|
||||
- MacOS进入轻量模式以后隐藏Dock图标
|
||||
- 增加轻量模式缺失的tray翻译
|
||||
- Linux下的窗口边框被削掉的问题
|
||||
|
||||
- 弹黑框的问题(原因是服务崩溃触发重装机制)
|
||||
- MacOS进入轻量模式以后隐藏Dock图标
|
||||
- 增加轻量模式缺失的tray翻译
|
||||
- Linux下的窗口边框被削掉的问题
|
||||
|
||||
#### 新增了:
|
||||
- 加强服务检测和重装逻辑
|
||||
- 增强内核与服务保活机制
|
||||
- 增加服务模式下的僵尸进程清理机制
|
||||
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
||||
|
||||
- 加强服务检测和重装逻辑
|
||||
- 增强内核与服务保活机制
|
||||
- 增加服务模式下的僵尸进程清理机制
|
||||
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
||||
|
||||
### 2.2.1 相对于 2.2.0(已下架不再提供)
|
||||
|
||||
#### 修复了:
|
||||
|
||||
1. **首页**
|
||||
- 修复 Direct 模式首页无法渲染
|
||||
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
||||
@@ -181,6 +236,7 @@
|
||||
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
|
||||
|
||||
#### 新增了:
|
||||
|
||||
1. **首页**
|
||||
- 首页文本过长自动截断
|
||||
2. **轻量模式**
|
||||
@@ -197,7 +253,9 @@
|
||||
## 2.2.0(已下架不再提供)
|
||||
|
||||
#### 新增功能
|
||||
|
||||
1. **首页**
|
||||
|
||||
- 新增首页功能,默认启动页面改为首页。
|
||||
- 首页流量图卡片显示上传/下载名称。
|
||||
- 首页支持轻量模式切换。
|
||||
@@ -205,17 +263,21 @@
|
||||
- 限制首页配置文件卡片URL长度。
|
||||
|
||||
2. **DNS 设置与覆写**
|
||||
|
||||
- 新增 DNS 覆写功能。
|
||||
- 默认启用 DNS 覆写。
|
||||
|
||||
3. **解锁测试**
|
||||
|
||||
- 新增解锁测试页面。
|
||||
|
||||
4. **轻量模式**
|
||||
|
||||
- 新增轻量模式及设置。
|
||||
- 添加自动轻量模式定时器。
|
||||
|
||||
5. **系统支持**
|
||||
|
||||
- Mihomo(meta)内核升级 1.19.3
|
||||
- macOS 支持 CMD+W 关闭窗口。
|
||||
- 新增 macOS 应用菜单。
|
||||
@@ -228,7 +290,9 @@
|
||||
- 新增代理命令。
|
||||
|
||||
#### 修复
|
||||
|
||||
1. **系统**
|
||||
|
||||
- 修复 Windows 热键崩溃。
|
||||
- 修复 macOS 无框标题。
|
||||
- 修复 macOS 静默启动崩溃。
|
||||
@@ -241,7 +305,9 @@
|
||||
- 修复构建失败问题。
|
||||
|
||||
#### 优化
|
||||
|
||||
1. **性能**
|
||||
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化首页组件性能。
|
||||
- 优化流量图表资源使用。
|
||||
@@ -254,6 +320,7 @@
|
||||
- 优化修改verge配置性能。
|
||||
|
||||
2. **重构**
|
||||
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化定时器管理。
|
||||
- 重构 MihomoManager 处理流量。
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
files:
|
||||
- source: /src/locales/en.json
|
||||
translation: /src/locales
|
||||
multilingual: 1
|
||||
+45
-44
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.3.0-alpha.0",
|
||||
"version": "2.3.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
@@ -11,17 +11,18 @@
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsc --noEmit && vite build",
|
||||
"web:serve": "vite preview",
|
||||
"check": "node scripts/check.mjs",
|
||||
"prebuild": "node scripts/prebuild.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||
"portable": "node scripts/portable.mjs",
|
||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||
"release-version": "node scripts/release-version.mjs",
|
||||
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
||||
"prepare": "husky",
|
||||
"publish-version": "node scripts/publish-version.mjs",
|
||||
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
||||
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml"
|
||||
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -30,77 +31,77 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.0.2",
|
||||
"@mui/lab": "7.0.0-beta.12",
|
||||
"@mui/material": "^7.0.2",
|
||||
"@mui/x-data-grid": "^8.2.0",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/lab": "7.0.0-beta.13",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-data-grid": "^8.5.2",
|
||||
"@tauri-apps/api": "2.5.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-fs": "^2.2.1",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||
"@tauri-apps/plugin-notification": "^2.2.2",
|
||||
"@tauri-apps/plugin-process": "^2.2.1",
|
||||
"@tauri-apps/plugin-shell": "2.2.1",
|
||||
"@tauri-apps/plugin-updater": "2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
|
||||
"@tauri-apps/plugin-notification": "^2.2.3",
|
||||
"@tauri-apps/plugin-process": "^2.2.2",
|
||||
"@tauri-apps/plugin-shell": "2.2.2",
|
||||
"@tauri-apps/plugin-updater": "2.8.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.3",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.8.3",
|
||||
"chart.js": "^4.4.9",
|
||||
"ahooks": "^3.8.5",
|
||||
"axios": "^1.10.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"cli-color": "^2.0.4",
|
||||
"d3-shape": "^3.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.44",
|
||||
"glob": "^11.0.1",
|
||||
"i18next": "^25.0.2",
|
||||
"foxact": "^0.2.49",
|
||||
"glob": "^11.0.3",
|
||||
"i18next": "^25.2.1",
|
||||
"js-base64": "^3.7.7",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "^5.3.1",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"peggy": "^5.0.0",
|
||||
"peggy": "^5.0.3",
|
||||
"react": "19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-i18next": "15.5.1",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "15.5.3",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.58.0",
|
||||
"react-router-dom": "7.6.0",
|
||||
"react-virtuoso": "^4.12.7",
|
||||
"react-router-dom": "7.6.2",
|
||||
"react-virtuoso": "^4.13.0",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.3.3",
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.3"
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.0",
|
||||
"@tauri-apps/cli": "2.2.7",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@tauri-apps/cli": "2.5.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "19.1.4",
|
||||
"@types/react-dom": "19.1.5",
|
||||
"@vitejs/plugin-legacy": "^6.0.2",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@vitejs/plugin-legacy": "^6.1.1",
|
||||
"@vitejs/plugin-react": "4.5.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"commander": "^14.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"meta-json-schema": "^1.19.3",
|
||||
"meta-json-schema": "^1.19.10",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-quick": "^4.1.1",
|
||||
"sass": "^1.86.0",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.2",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"sass": "^1.89.2",
|
||||
"terser": "^5.43.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
@@ -112,4 +113,4 @@
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.13.2"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+719
-744
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"extends": ["config:recommended"],
|
||||
"baseBranches": ["dev"],
|
||||
"enabledManagers": ["cargo", "npm"],
|
||||
"labels": ["dependencies"],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**",
|
||||
"**/bower_components/**",
|
||||
"**/vendor/**",
|
||||
"**/__tests__/**",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/__fixtures__/**",
|
||||
"**/crate/**",
|
||||
"shared/**"
|
||||
],
|
||||
"rangeStrategy": "bump",
|
||||
"packageRules": [
|
||||
{
|
||||
"semanticCommitType": "chore",
|
||||
"matchPackageNames": ["*"]
|
||||
},
|
||||
{
|
||||
"description": "Disable node/pnpm version updates",
|
||||
"matchPackageNames": ["node", "pnpm"],
|
||||
"matchDepTypes": ["engines", "packageManager"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Group all cargo dependencies into a single PR",
|
||||
"matchManagers": ["cargo"],
|
||||
"groupName": "cargo dependencies"
|
||||
},
|
||||
{
|
||||
"description": "Group all npm dependencies into a single PR",
|
||||
"matchManagers": ["npm"],
|
||||
"groupName": "npm dependencies"
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"ignoreDeps": ["serde_yaml"]
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const LOCALES_DIR = path.resolve(__dirname, '../src/locales');
|
||||
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
|
||||
const SRC_DIRS = [
|
||||
path.resolve(__dirname, '../src'),
|
||||
path.resolve(__dirname, '../src-tauri')
|
||||
path.resolve(__dirname, "../src"),
|
||||
path.resolve(__dirname, "../src-tauri"),
|
||||
];
|
||||
const exts = ['.js', '.ts', '.tsx', '.jsx', '.vue', '.rs'];
|
||||
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
|
||||
|
||||
// 递归获取所有文件
|
||||
function getAllFiles(dir, exts) {
|
||||
let files = [];
|
||||
fs.readdirSync(dir).forEach(file => {
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
const full = path.join(dir, file);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
files = files.concat(getAllFiles(full, exts));
|
||||
@@ -28,21 +28,21 @@ function getAllFiles(dir, exts) {
|
||||
|
||||
// 读取所有源码内容为一个大字符串
|
||||
function getAllSourceContent() {
|
||||
const files = SRC_DIRS.flatMap(dir => getAllFiles(dir, exts));
|
||||
return files.map(f => fs.readFileSync(f, 'utf8')).join('\n');
|
||||
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
|
||||
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
|
||||
}
|
||||
|
||||
// 白名单 key,不检查这些 key 是否被使用
|
||||
const WHITELIST_KEYS = [
|
||||
'theme.light',
|
||||
'theme.dark',
|
||||
'theme.system',
|
||||
"Already Using Latest Core Version"
|
||||
"theme.light",
|
||||
"theme.dark",
|
||||
"theme.system",
|
||||
"Already Using Latest Core Version",
|
||||
];
|
||||
|
||||
// 主流程
|
||||
function processI18nFile(i18nPath, lang, allSource) {
|
||||
const i18n = JSON.parse(fs.readFileSync(i18nPath, 'utf8'));
|
||||
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
|
||||
const keys = Object.keys(i18n);
|
||||
|
||||
const used = {};
|
||||
@@ -50,7 +50,7 @@ function processI18nFile(i18nPath, lang, allSource) {
|
||||
|
||||
let checked = 0;
|
||||
const total = keys.length;
|
||||
keys.forEach(key => {
|
||||
keys.forEach((key) => {
|
||||
if (WHITELIST_KEYS.includes(key)) {
|
||||
used[key] = i18n[key];
|
||||
} else {
|
||||
@@ -65,8 +65,10 @@ function processI18nFile(i18nPath, lang, allSource) {
|
||||
checked++;
|
||||
if (checked % 20 === 0 || checked === total) {
|
||||
const percent = ((checked / total) * 100).toFixed(1);
|
||||
process.stdout.write(`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`);
|
||||
if (checked === total) process.stdout.write('\n');
|
||||
process.stdout.write(
|
||||
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
|
||||
);
|
||||
if (checked === total) process.stdout.write("\n");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -74,25 +76,27 @@ function processI18nFile(i18nPath, lang, allSource) {
|
||||
console.log(`\n[${lang}] Unused keys:`, unused);
|
||||
|
||||
// 备份原文件
|
||||
const oldPath = i18nPath + '.old';
|
||||
const oldPath = i18nPath + ".old";
|
||||
fs.renameSync(i18nPath, oldPath);
|
||||
|
||||
// 写入精简后的 i18n 文件(保留原文件名)
|
||||
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), 'utf8');
|
||||
console.log(`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`);
|
||||
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
|
||||
console.log(
|
||||
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
|
||||
);
|
||||
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
|
||||
const files = fs.readdirSync(LOCALES_DIR).filter(f =>
|
||||
/^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith('.old')
|
||||
);
|
||||
const files = fs
|
||||
.readdirSync(LOCALES_DIR)
|
||||
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
|
||||
const allSource = getAllSourceContent();
|
||||
files.forEach(file => {
|
||||
const lang = path.basename(file, '.json');
|
||||
files.forEach((file) => {
|
||||
const lang = path.basename(file, ".json");
|
||||
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
main();
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// scripts/publish-version.mjs
|
||||
import { spawn } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const scriptPath = path.join(rootDir, "scripts", "release-version.mjs");
|
||||
|
||||
if (!existsSync(scriptPath)) {
|
||||
console.error("release-version.mjs not found!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const versionArg = process.argv[2];
|
||||
if (!versionArg) {
|
||||
console.error("Usage: pnpm publish-version <version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 1. 调用 release-version.mjs
|
||||
const runRelease = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const child = spawn("node", [scriptPath, versionArg], { stdio: "inherit" });
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error("release-version failed"));
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 判断是否需要打 tag
|
||||
function isSemver(version) {
|
||||
return /^v?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(version);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
await runRelease();
|
||||
|
||||
let tag = null;
|
||||
if (versionArg === "alpha") {
|
||||
// 读取 package.json 里的主版本
|
||||
const pkg = await import(path.join(rootDir, "package.json"), {
|
||||
assert: { type: "json" },
|
||||
});
|
||||
tag = `v${pkg.default.version}-alpha`;
|
||||
} else if (isSemver(versionArg)) {
|
||||
// 1.2.3 或 v1.2.3
|
||||
tag = versionArg.startsWith("v") ? versionArg : `v${versionArg}`;
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
// 打 tag 并推送
|
||||
const { execSync } = await import("child_process");
|
||||
try {
|
||||
execSync(`git tag ${tag}`, { stdio: "inherit" });
|
||||
execSync(`git push origin ${tag}`, { stdio: "inherit" });
|
||||
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
|
||||
} catch (e) {
|
||||
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log("[INFO]: No git tag created for this version.");
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
+63
-23
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
/**
|
||||
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
|
||||
*
|
||||
@@ -30,19 +28,35 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { program } from "commander";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
/**
|
||||
* 生成短时间戳(格式:YYMMDDHHMM)
|
||||
* 获取当前 git 短 commit hash
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateShortTimestamp() {
|
||||
function getGitShortCommit() {
|
||||
try {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
} catch (e) {
|
||||
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
|
||||
return "nogit";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
|
||||
* @param {boolean} withCommit 是否带 commit
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateShortTimestamp(withCommit = false) {
|
||||
const now = new Date();
|
||||
const year = String(now.getFullYear()).slice(-2);
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
const hours = String(now.getHours()).padStart(2, "0");
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
return `${year}${month}${day}${hours}${minutes}`;
|
||||
if (withCommit) {
|
||||
const gitShort = getGitShortCommit();
|
||||
return `${month}${day}.${gitShort}`;
|
||||
}
|
||||
return `${month}${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +65,9 @@ function generateShortTimestamp() {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidVersion(version) {
|
||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(version);
|
||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
||||
version,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,8 +85,8 @@ function normalizeVersion(version) {
|
||||
* @returns {string}
|
||||
*/
|
||||
function getBaseVersion(version) {
|
||||
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, '');
|
||||
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, '');
|
||||
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
|
||||
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
|
||||
return base;
|
||||
}
|
||||
|
||||
@@ -85,10 +101,21 @@ async function updatePackageVersion(newVersion) {
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
|
||||
console.log("[INFO]: Current package.json version is: ", packageJson.version);
|
||||
packageJson.version = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
|
||||
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf8");
|
||||
console.log(`[INFO]: package.json version updated to: ${packageJson.version}`);
|
||||
console.log(
|
||||
"[INFO]: Current package.json version is: ",
|
||||
packageJson.version,
|
||||
);
|
||||
packageJson.version = newVersion.startsWith("v")
|
||||
? newVersion.slice(1)
|
||||
: newVersion;
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(
|
||||
`[INFO]: package.json version updated to: ${packageJson.version}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error updating package.json version:", error);
|
||||
throw error;
|
||||
@@ -105,12 +132,17 @@ async function updateCargoVersion(newVersion) {
|
||||
try {
|
||||
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||
const lines = data.split("\n");
|
||||
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
|
||||
const versionWithoutV = newVersion.startsWith("v")
|
||||
? newVersion.slice(1)
|
||||
: newVersion;
|
||||
const baseVersion = getBaseVersion(versionWithoutV);
|
||||
|
||||
const updatedLines = lines.map((line) => {
|
||||
if (line.trim().startsWith("version =")) {
|
||||
return line.replace(/version\s*=\s*"[^"]+"/, `version = "${baseVersion}"`);
|
||||
return line.replace(
|
||||
/version\s*=\s*"[^"]+"/,
|
||||
`version = "${baseVersion}"`,
|
||||
);
|
||||
}
|
||||
return line;
|
||||
});
|
||||
@@ -133,12 +165,21 @@ async function updateTauriConfigVersion(newVersion) {
|
||||
try {
|
||||
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||
const tauriConfig = JSON.parse(data);
|
||||
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
|
||||
const versionWithoutV = newVersion.startsWith("v")
|
||||
? newVersion.slice(1)
|
||||
: newVersion;
|
||||
const baseVersion = getBaseVersion(versionWithoutV);
|
||||
|
||||
console.log("[INFO]: Current tauri.conf.json version is: ", tauriConfig.version);
|
||||
console.log(
|
||||
"[INFO]: Current tauri.conf.json version is: ",
|
||||
tauriConfig.version,
|
||||
);
|
||||
tauriConfig.version = baseVersion;
|
||||
await fs.writeFile(tauriConfigPath, JSON.stringify(tauriConfig, null, 2), "utf8");
|
||||
await fs.writeFile(
|
||||
tauriConfigPath,
|
||||
JSON.stringify(tauriConfig, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating tauri.conf.json version:", error);
|
||||
@@ -180,8 +221,8 @@ async function main(versionArg) {
|
||||
const baseVersion = getBaseVersion(currentVersion);
|
||||
|
||||
if (versionArg.toLowerCase() === "autobuild") {
|
||||
const timestamp = generateShortTimestamp();
|
||||
newVersion = `${baseVersion}+autobuild.${timestamp}`;
|
||||
// 格式: 2.3.0+autobuild.250613.cc39b27
|
||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
|
||||
} else {
|
||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||
}
|
||||
@@ -210,4 +251,3 @@ program
|
||||
.argument("<version>", "version tag or full version")
|
||||
.action(main)
|
||||
.parse(process.argv);
|
||||
|
||||
|
||||
Generated
+175
-159
@@ -1042,7 +1042,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"aes-gcm",
|
||||
@@ -1057,7 +1057,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"futures",
|
||||
"gethostname 1.0.2",
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"hex",
|
||||
"hmac",
|
||||
"image",
|
||||
@@ -1100,8 +1100,8 @@ dependencies = [
|
||||
"tauri-plugin-window-state",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.26.2",
|
||||
"tungstenite 0.26.2",
|
||||
"tokio-tungstenite 0.27.0",
|
||||
"tungstenite 0.27.0",
|
||||
"users",
|
||||
"warp",
|
||||
"winapi",
|
||||
@@ -1337,21 +1337,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
@@ -1666,7 +1651,7 @@ dependencies = [
|
||||
"tonic-health",
|
||||
"tonic-web",
|
||||
"tower 0.4.13",
|
||||
"tower-http",
|
||||
"tower-http 0.4.4",
|
||||
"tower-layer",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
@@ -2100,11 +2085,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-rs-sys",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
@@ -2462,9 +2448,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -2587,9 +2573,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
|
||||
[[package]]
|
||||
name = "global-hotkey"
|
||||
version = "0.6.4"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fbb3a4e56c901ee66c190fdb3fa08344e6d09593cc6c61f8eb9add7144b271"
|
||||
checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"keyboard-types",
|
||||
@@ -2599,7 +2585,8 @@ dependencies = [
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"windows-sys 0.59.0",
|
||||
"x11-dl",
|
||||
"x11rb",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2939,7 +2926,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa 1.0.15",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.8",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -2981,7 +2968,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3014,21 +3001,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.10"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
|
||||
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.6.0",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.8",
|
||||
"socket2 0.5.10",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3380,6 +3374,16 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
@@ -3583,9 +3587,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
version = "0.2.173"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
@@ -3607,6 +3611,26 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liblzma"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66352d7a8ac12d4877b6e6ea5a9b7650ee094257dc40889955bea5bc5b08c1d0"
|
||||
dependencies = [
|
||||
"liblzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liblzma-sys"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5839bad90c3cc2e0b8c4ed8296b80e86040240f81d46b9c0e9bc8dd51ddd3af1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.11"
|
||||
@@ -3624,6 +3648,15 @@ dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-rs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
|
||||
dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.3.8"
|
||||
@@ -3668,9 +3701,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
@@ -3743,27 +3776,6 @@ dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-sys"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -4777,9 +4789,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
@@ -4787,9 +4799,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.10"
|
||||
version = "0.9.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
@@ -5381,7 +5393,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.8",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -5395,7 +5407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"rand 0.9.0",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
@@ -5417,7 +5429,7 @@ dependencies = [
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.8",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -5527,7 +5539,7 @@ version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5727,9 +5739,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.15"
|
||||
version = "0.12.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -5746,36 +5758,32 @@ dependencies = [
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tower-http 0.6.6",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry 0.4.0",
|
||||
"webpki-roots 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5967,15 +5975,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.11.0"
|
||||
@@ -6521,9 +6520,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.8"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -6708,9 +6707,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.35.0"
|
||||
version = "0.35.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422"
|
||||
checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
@@ -6978,8 +6977,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-autostart"
|
||||
version = "2.3.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#9bc4b2230ebb32bd30a4c0c2a21077829a729193"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9641831518c56775a364a8750e0eed8852adee87e0f11006d043b9ebba0bf5"
|
||||
dependencies = [
|
||||
"auto-launch",
|
||||
"serde",
|
||||
@@ -6991,9 +6991,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-clipboard-manager"
|
||||
version = "2.2.2"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ab4cb42fdf745229b768802e9180920a4be63122cf87ed1c879103f7609d98e"
|
||||
checksum = "11fa4f17a6d380490597f7632aca40b65d379cb374cb92bd9d80f333309b7fd7"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"log",
|
||||
@@ -7006,9 +7006,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.2.1"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dba4412f30eaff6f5d210e20383c2d6835593977402092e95b72497a4f8632fa"
|
||||
checksum = "e4976ac728ebc0487515aa956cfdf200abcc52b784e441493fc544bc6ce369c8"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"rust-ini",
|
||||
@@ -7020,7 +7020,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry 0.5.1",
|
||||
"windows-registry",
|
||||
"windows-result 0.3.2",
|
||||
]
|
||||
|
||||
@@ -7053,9 +7053,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.2.1"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcaf6e5d6062423a0f711a23c2a573ccba222b6a16a9322d8499928f27e41376"
|
||||
checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
@@ -7071,9 +7071,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.2.1"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682"
|
||||
checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
@@ -7089,14 +7089,13 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-global-shortcut"
|
||||
version = "2.2.0"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00f646a09511e8d283267dcdaa08c2ef27c4116bf271d9114849d9ca215606c3"
|
||||
checksum = "31919f3c07bcb585afef217c0c33cde80da9ebccf5b8e2c90e0e0a535b14ab47"
|
||||
dependencies = [
|
||||
"global-hotkey",
|
||||
"log",
|
||||
@@ -7109,9 +7108,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
version = "2.2.1"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57da5888533e802b6206b9685091f8714aa1f5266dc80051a82388449558b773"
|
||||
checksum = "4d870adae9408be585abd56eade2b5def2660339512b7c8de5ddf21238b67a34"
|
||||
dependencies = [
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
@@ -7119,9 +7118,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.2.1"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69d5eb3368b959937ad2aeaf6ef9a8f5d11e01ffe03629d3530707bbcb27ff5d"
|
||||
checksum = "d34e525a448b80ad5d906fcbd93838ac3ba37985b29ac699a045b5da9b0a1a22"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
@@ -7140,9 +7139,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.7.1"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f05c38afd77a4b8fd98e8fb6f1cdbb5fbb8a46ba181eb2758b05321e3c6209"
|
||||
checksum = "b068673e9037376ca9906f99b00ae5f9e6eb62f456f900b4435c38d57cfa73e4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs 6.0.0",
|
||||
@@ -7166,15 +7165,15 @@ dependencies = [
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
version = "2.2.2"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a27a3fe49de72adbe0d84aee33c89a0b059722cd0b42aaeab29eaaee7f7535cd"
|
||||
checksum = "136e5ce5e61edc8eeeaca70080811bbdcdd890cac9c4070cb4db9cc3de1da449"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"log",
|
||||
@@ -7285,12 +7284,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.19.1"
|
||||
version = "3.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
|
||||
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
|
||||
dependencies = [
|
||||
"fastrand 2.3.0",
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix 1.0.3",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -7514,9 +7513,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.44.2"
|
||||
version = "1.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -7525,7 +7524,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.8",
|
||||
"socket2 0.5.10",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -7597,14 +7596,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.26.2"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
||||
checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite 0.26.2",
|
||||
"tungstenite 0.27.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7730,7 +7729,7 @@ dependencies = [
|
||||
"pin-project",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tower-http",
|
||||
"tower-http 0.4.4",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -7789,6 +7788,24 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower 0.5.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -7937,9 +7954,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.26.2"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
|
||||
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
@@ -8144,7 +8161,7 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -8514,6 +8531,15 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.37.0"
|
||||
@@ -8761,17 +8787,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.1"
|
||||
@@ -8811,15 +8826,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.0"
|
||||
@@ -8865,6 +8871,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
@@ -8913,9 +8928,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.0"
|
||||
version = "0.53.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
||||
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
@@ -9313,21 +9328,18 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkeysym"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4"
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.5"
|
||||
@@ -9514,32 +9526,36 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.6.1"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
|
||||
checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"deflate64",
|
||||
"flate2",
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"hmac",
|
||||
"indexmap 2.8.0",
|
||||
"lzma-rs",
|
||||
"liblzma",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"sha1",
|
||||
"time",
|
||||
"xz2",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.1"
|
||||
|
||||
+28
-28
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -16,33 +16,33 @@ identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3"
|
||||
warp = "0.3.7"
|
||||
anyhow = "1.0.98"
|
||||
dirs = "6.0"
|
||||
open = "5.3"
|
||||
log = "0.4"
|
||||
dunce = "1.0"
|
||||
log4rs = "1"
|
||||
open = "5.3.2"
|
||||
log = "0.4.27"
|
||||
dunce = "1.0.5"
|
||||
log4rs = "1.3.0"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.41"
|
||||
sysinfo = "0.35.0"
|
||||
sysinfo = "0.35.2"
|
||||
boa_engine = "0.20.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.34"
|
||||
serde_yaml = "0.9.34-deprecated"
|
||||
once_cell = "1.21.3"
|
||||
lazy_static = "1.5.0"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12.3"
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3.1"
|
||||
tokio = { version = "1.44.2", features = [
|
||||
tokio = { version = "1.45.1", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
reqwest = { version = "0.12.15", features = ["json", "rustls-tls", "cookies"] }
|
||||
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] }
|
||||
regex = "1.11.1"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
image = "0.25.6"
|
||||
@@ -55,27 +55,27 @@ tauri = { version = "2.5.1", features = [
|
||||
"image-png",
|
||||
] }
|
||||
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.1"
|
||||
tauri-plugin-fs = "2.2.1"
|
||||
tauri-plugin-process = "2.2.1"
|
||||
tauri-plugin-clipboard-manager = "2.2.2"
|
||||
tauri-plugin-deep-link = "2.2.1"
|
||||
tauri-plugin-shell = "2.2.2"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-fs = "2.3.0"
|
||||
tauri-plugin-process = "2.2.2"
|
||||
tauri-plugin-clipboard-manager = "2.2.3"
|
||||
tauri-plugin-deep-link = "2.3.0"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
zip = "2.6.1"
|
||||
tauri-plugin-window-state = "2.2.3"
|
||||
zip = "4.1.0"
|
||||
reqwest_dav = "0.2.1"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
getrandom = "0.3.2"
|
||||
tokio-tungstenite = "0.26.2"
|
||||
getrandom = "0.3.3"
|
||||
tokio-tungstenite = "0.27.0"
|
||||
futures = "0.3.31"
|
||||
sys-locale = "0.3.2"
|
||||
async-trait = "0.1.88"
|
||||
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
||||
ab_glyph = "0.2.29"
|
||||
tungstenite = "0.26.2"
|
||||
libc = "0.2.172"
|
||||
tungstenite = "0.27.0"
|
||||
libc = "0.2.173"
|
||||
gethostname = "1.0.2"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.9"
|
||||
@@ -85,7 +85,7 @@ hex = "0.4.3"
|
||||
runas = "=1.2.0"
|
||||
deelevate = "0.2.0"
|
||||
winreg = "0.55.0"
|
||||
winapi = { version = "0.3", features = [
|
||||
winapi = { version = "0.3.9", features = [
|
||||
"winbase",
|
||||
"fileapi",
|
||||
"winnt",
|
||||
@@ -99,9 +99,9 @@ winapi = { version = "0.3", features = [
|
||||
users = "0.11.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-global-shortcut = "2.2.0"
|
||||
tauri-plugin-updater = "2.7.1"
|
||||
tauri-plugin-autostart = "2.4.0"
|
||||
tauri-plugin-global-shortcut = "2.2.1"
|
||||
tauri-plugin-updater = "2.8.1"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
@@ -136,7 +136,7 @@ name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.19.1"
|
||||
tempfile = "3.20.0"
|
||||
|
||||
[workspace]
|
||||
members = ["src_crates/crate_mihomo_api"]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"identifier": "desktop-windows-capability",
|
||||
"description": "permissions for desktop windows applications",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:webview:allow-create-webview",
|
||||
"core:webview:allow-create-webview-window"
|
||||
]
|
||||
}
|
||||
@@ -916,9 +916,9 @@ FunctionEnd
|
||||
!macroend
|
||||
|
||||
Section Uninstall
|
||||
;删除 .window-state.json 文件
|
||||
;删除 window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
@@ -1015,9 +1015,9 @@ Section Uninstall
|
||||
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||
${EndIf}
|
||||
|
||||
;删除 .window-state.json 文件
|
||||
;删除 window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
|
||||
${GetOptions} $CMDLINE "/P" $R0
|
||||
IfErrors +2 0
|
||||
|
||||
@@ -11,4 +11,3 @@ merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
imports_granularity = "Crate"
|
||||
|
||||
@@ -12,45 +12,89 @@ use tokio::sync::Mutex;
|
||||
// 添加全局互斥锁防止并发配置更新
|
||||
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
||||
|
||||
/// 获取配置文件列表
|
||||
/// 获取配置文件避免锁竞争
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
let profiles_result = tokio::time::timeout(
|
||||
Duration::from_secs(3), // 3秒超时
|
||||
tokio::task::spawn_blocking(move || Config::profiles().data().clone()),
|
||||
// 策略1: 尝试快速获取latest数据
|
||||
let latest_result = tokio::time::timeout(
|
||||
Duration::from_millis(500),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let profiles = Config::profiles();
|
||||
let latest = profiles.latest();
|
||||
IProfiles {
|
||||
current: latest.current.clone(),
|
||||
items: latest.items.clone(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
match profiles_result {
|
||||
Ok(Ok(profiles)) => Ok(profiles),
|
||||
match latest_result {
|
||||
Ok(Ok(profiles)) => {
|
||||
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Ok(Err(join_err)) => {
|
||||
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err);
|
||||
Ok(IProfiles {
|
||||
current: None,
|
||||
items: Some(vec![]),
|
||||
})
|
||||
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
|
||||
}
|
||||
Err(_) => {
|
||||
// 超时情况
|
||||
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
|
||||
}
|
||||
}
|
||||
|
||||
// 策略2: 如果快速获取失败,尝试获取data()
|
||||
let data_result = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let profiles = Config::profiles();
|
||||
let data = profiles.data();
|
||||
IProfiles {
|
||||
current: data.current.clone(),
|
||||
items: data.items.clone(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
match data_result {
|
||||
Ok(Ok(profiles)) => {
|
||||
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Ok(Err(join_err)) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"获取配置列表超时(3秒),可能存在锁竞争"
|
||||
"获取draft配置任务失败: {}",
|
||||
join_err
|
||||
);
|
||||
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, true, "使用latest()成功获取配置");
|
||||
Ok(profiles)
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置");
|
||||
Ok(IProfiles {
|
||||
current: None,
|
||||
items: Some(vec![]),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
|
||||
}
|
||||
}
|
||||
|
||||
// 策略3: fallback,尝试重新创建配置
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"所有获取配置策略都失败,尝试fallback"
|
||||
);
|
||||
|
||||
match tokio::task::spawn_blocking(IProfiles::new).await {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, true, "使用fallback配置成功");
|
||||
Ok(profiles)
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
|
||||
// 返回空配置避免崩溃
|
||||
Ok(IProfiles {
|
||||
current: None,
|
||||
items: Some(vec![]),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +137,10 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||
|
||||
// 删除后自动清理冗余文件
|
||||
let _ = Config::profiles().latest().auto_cleanup();
|
||||
|
||||
if should_update {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -242,6 +290,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
Config::profiles().apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||
log::warn!(target: "app", "强制刷新代理缓存失败: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = Tray::global().update_tooltip() {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
|
||||
|
||||
+77
-49
@@ -1,71 +1,99 @@
|
||||
use super::CmdResult;
|
||||
use crate::module::mihomo::MihomoManager;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState};
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
sync::Mutex,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri::Manager;
|
||||
|
||||
static LAST_REFRESH_TIME: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None));
|
||||
static IS_REFRESHING: AtomicBool = AtomicBool::new(false);
|
||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3);
|
||||
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
|
||||
manager
|
||||
.refresh_proxies()
|
||||
.await
|
||||
.map(|_| manager.get_proxies())
|
||||
.or_else(|_| Ok(manager.get_proxies()))
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
let should_refresh = {
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL {
|
||||
state.need_refresh = true;
|
||||
state.last_refresh_time = now;
|
||||
}
|
||||
state.need_refresh
|
||||
};
|
||||
|
||||
if should_refresh {
|
||||
let proxies = manager.get_refresh_proxies().await?;
|
||||
{
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
state.proxies = Box::new(proxies);
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "proxies刷新成功");
|
||||
}
|
||||
|
||||
let proxies = {
|
||||
let state = cmd_proxy_state.lock().unwrap();
|
||||
state.proxies.clone()
|
||||
};
|
||||
Ok(*proxies)
|
||||
}
|
||||
|
||||
/// 强制刷新代理缓存用于profile切换
|
||||
#[tauri::command]
|
||||
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
log::debug!(target: "app", "强制刷新代理缓存");
|
||||
|
||||
let proxies = manager.get_refresh_proxies().await?;
|
||||
|
||||
{
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
state.proxies = Box::new(proxies.clone());
|
||||
state.need_refresh = false;
|
||||
state.last_refresh_time = Instant::now();
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "强制刷新代理缓存完成");
|
||||
Ok(proxies)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
let cached_data = manager.get_providers_proxies();
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
let safe_data = if cached_data.is_null() {
|
||||
serde_json::json!({
|
||||
"providers": {}
|
||||
})
|
||||
} else {
|
||||
cached_data
|
||||
};
|
||||
|
||||
// 检查是否需要刷新
|
||||
let should_refresh = {
|
||||
let last_refresh = LAST_REFRESH_TIME.lock();
|
||||
match *last_refresh {
|
||||
Some(last_time) => last_time.elapsed() > REFRESH_INTERVAL,
|
||||
None => true,
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL {
|
||||
state.need_refresh = true;
|
||||
state.last_refresh_time = now;
|
||||
}
|
||||
state.need_refresh
|
||||
};
|
||||
|
||||
if should_refresh && !IS_REFRESHING.load(Ordering::Acquire) {
|
||||
IS_REFRESHING.store(true, Ordering::Release);
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
let manager = MihomoManager::global();
|
||||
match manager.refresh_providers_proxies().await {
|
||||
Ok(_) => {
|
||||
log::debug!(target: "app", "providers_proxies静默后台刷新成功");
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "providers_proxies后台刷新失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut last_refresh = LAST_REFRESH_TIME.lock();
|
||||
*last_refresh = Some(Instant::now());
|
||||
}
|
||||
|
||||
IS_REFRESHING.store(false, Ordering::Release);
|
||||
});
|
||||
if should_refresh {
|
||||
let manager = MihomoManager::global();
|
||||
let providers = manager.get_providers_proxies().await?;
|
||||
{
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
state.providers_proxies = Box::new(providers);
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "providers_proxies刷新成功");
|
||||
}
|
||||
|
||||
Ok(safe_data)
|
||||
let providers_proxies = {
|
||||
let state = cmd_proxy_state.lock().unwrap();
|
||||
state.providers_proxies.clone()
|
||||
};
|
||||
Ok(*providers_proxies)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core::*, utils::dirs, wrap_err};
|
||||
use crate::{
|
||||
config::*,
|
||||
core::*,
|
||||
logging,
|
||||
utils::{dirs, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use std::fs;
|
||||
|
||||
/// 保存profiles的配置
|
||||
@@ -26,29 +32,54 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
println!(
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
|
||||
file_path_str, is_merge_file
|
||||
file_path_str,
|
||||
is_merge_file
|
||||
);
|
||||
|
||||
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||
if is_merge_file {
|
||||
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 检测到merge文件,只进行语法验证"
|
||||
);
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, Some(true))
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] merge文件语法验证通过");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] merge文件语法验证通过"
|
||||
);
|
||||
// 成功后尝试更新整体配置
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
|
||||
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 更新整体配置时发生错误: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] merge文件语法验证失败: {}",
|
||||
error_msg
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
// 发送合并文件专用错误通知
|
||||
@@ -57,7 +88,13 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 验证过程发生错误: {}",
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
return Err(e.to_string());
|
||||
@@ -71,11 +108,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] 验证成功");
|
||||
logging!(info, Type::Config, true, "[cmd配置save] 验证成功");
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] 验证失败: {}", error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 验证失败: {}",
|
||||
error_msg
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
|
||||
@@ -90,24 +133,30 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||
{
|
||||
// 普通YAML错误使用YAML通知处理
|
||||
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||
} else if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
println!("[cmd配置save] 脚本文件验证失败,发送通知");
|
||||
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
// 普通配置错误使用一般通知
|
||||
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 验证过程发生错误: {}",
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
Err(e.to_string())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::CmdResult;
|
||||
use crate::core::*;
|
||||
use crate::{core::*, logging, utils::logging::Type};
|
||||
|
||||
/// 发送脚本验证通知消息
|
||||
#[tauri::command]
|
||||
@@ -28,7 +28,14 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
"config_validate::script_error"
|
||||
};
|
||||
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"{} 验证失败: {}",
|
||||
file_type,
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +43,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
/// 验证指定脚本文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
||||
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
|
||||
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path, None)
|
||||
@@ -48,7 +55,13 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"验证脚本文件过程发生错误: {}",
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
@@ -60,7 +73,14 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
if !result.0 {
|
||||
let error_msg = &result.1;
|
||||
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[通知] 处理{}验证错误: {}",
|
||||
file_type,
|
||||
error_msg
|
||||
);
|
||||
|
||||
// 检查是否为merge文件
|
||||
let is_merge_file = file_type.contains("合并");
|
||||
@@ -97,8 +117,22 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
}
|
||||
};
|
||||
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"{} 验证失败: {}",
|
||||
file_type,
|
||||
error_msg
|
||||
);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[通知] 发送通知: status={}, msg={}",
|
||||
status,
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{config::*, feat, wrap_err};
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
Ok(IVergeResponse::from(verge_data))
|
||||
Ok(IVergeResponse::from(*verge_data))
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
|
||||
@@ -63,6 +63,8 @@ impl IClashTemp {
|
||||
vec![
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
// Only enable this in dev mode
|
||||
#[cfg(feature = "verge-dev")]
|
||||
"http://localhost:3000",
|
||||
"https://yacd.metacubex.one",
|
||||
"https://metacubex.github.io",
|
||||
@@ -103,6 +105,8 @@ impl IClashTemp {
|
||||
vec![
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
// Only enable this in dev mode
|
||||
#[cfg(feature = "verge-dev")]
|
||||
"http://localhost:3000",
|
||||
"https://yacd.metacubex.one",
|
||||
"https://metacubex.github.io",
|
||||
|
||||
@@ -15,10 +15,10 @@ pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
|
||||
pub struct Config {
|
||||
clash_config: Draft<IClashTemp>,
|
||||
verge_config: Draft<IVerge>,
|
||||
profiles_config: Draft<IProfiles>,
|
||||
runtime_config: Draft<IRuntime>,
|
||||
clash_config: Draft<Box<IClashTemp>>,
|
||||
verge_config: Draft<Box<IVerge>>,
|
||||
profiles_config: Draft<Box<IProfiles>>,
|
||||
runtime_config: Draft<Box<IRuntime>>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -26,26 +26,26 @@ impl Config {
|
||||
static CONFIG: OnceCell<Config> = OnceCell::new();
|
||||
|
||||
CONFIG.get_or_init(|| Config {
|
||||
clash_config: Draft::from(IClashTemp::new()),
|
||||
verge_config: Draft::from(IVerge::new()),
|
||||
profiles_config: Draft::from(IProfiles::new()),
|
||||
runtime_config: Draft::from(IRuntime::new()),
|
||||
clash_config: Draft::from(Box::new(IClashTemp::new())),
|
||||
verge_config: Draft::from(Box::new(IVerge::new())),
|
||||
profiles_config: Draft::from(Box::new(IProfiles::new())),
|
||||
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clash() -> Draft<IClashTemp> {
|
||||
pub fn clash() -> Draft<Box<IClashTemp>> {
|
||||
Self::global().clash_config.clone()
|
||||
}
|
||||
|
||||
pub fn verge() -> Draft<IVerge> {
|
||||
pub fn verge() -> Draft<Box<IVerge>> {
|
||||
Self::global().verge_config.clone()
|
||||
}
|
||||
|
||||
pub fn profiles() -> Draft<IProfiles> {
|
||||
pub fn profiles() -> Draft<Box<IProfiles>> {
|
||||
Self::global().profiles_config.clone()
|
||||
}
|
||||
|
||||
pub fn runtime() -> Draft<IRuntime> {
|
||||
pub fn runtime() -> Draft<Box<IRuntime>> {
|
||||
Self::global().runtime_config.clone()
|
||||
}
|
||||
|
||||
@@ -149,11 +149,11 @@ impl Config {
|
||||
pub async fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
*Config::runtime().draft() = Box::new(IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
};
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -164,3 +164,42 @@ pub enum ConfigType {
|
||||
Run,
|
||||
Check,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::mem;
|
||||
|
||||
#[test]
|
||||
fn test_prfitem_from_merge_size() {
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap();
|
||||
dbg!(&merge_item);
|
||||
let prfitem_size = mem::size_of_val(&merge_item);
|
||||
dbg!(prfitem_size);
|
||||
// Boxed version
|
||||
let boxed_merge_item = Box::new(merge_item);
|
||||
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
|
||||
dbg!(box_prfitem_size);
|
||||
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
|
||||
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
|
||||
assert!(box_prfitem_size < prfitem_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draft_size_non_boxed() {
|
||||
let draft = Draft::from(IRuntime::new());
|
||||
let iruntime_size = std::mem::size_of_val(&draft);
|
||||
dbg!(iruntime_size);
|
||||
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draft_size_boxed() {
|
||||
let draft = Draft::from(Box::new(IRuntime::new()));
|
||||
let box_iruntime_size = std::mem::size_of_val(&draft);
|
||||
dbg!(box_iruntime_size);
|
||||
assert_eq!(
|
||||
box_iruntime_size,
|
||||
std::mem::size_of::<Draft<Box<IRuntime>>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,21 @@ pub struct Draft<T: Clone + ToOwned> {
|
||||
|
||||
macro_rules! draft_define {
|
||||
($id: ident) => {
|
||||
impl Draft<$id> {
|
||||
impl From<$id> for Draft<$id> {
|
||||
fn from(data: $id) -> Self {
|
||||
Draft {
|
||||
inner: Arc::new(Mutex::new((data, None))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Draft<Box<$id>> {
|
||||
#[allow(unused)]
|
||||
pub fn data(&self) -> MappedMutexGuard<$id> {
|
||||
pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||
}
|
||||
|
||||
pub fn latest(&self) -> MappedMutexGuard<$id> {
|
||||
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
&mut inner.0
|
||||
@@ -25,7 +33,7 @@ macro_rules! draft_define {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
||||
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
inner.1 = Some(inner.0.clone());
|
||||
@@ -35,7 +43,7 @@ macro_rules! draft_define {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(&self) -> Option<$id> {
|
||||
pub fn apply(&self) -> Option<Box<$id>> {
|
||||
let mut inner = self.inner.lock();
|
||||
|
||||
match inner.1.take() {
|
||||
@@ -48,14 +56,14 @@ macro_rules! draft_define {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn discard(&self) -> Option<$id> {
|
||||
pub fn discard(&self) -> Option<Box<$id>> {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.1.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$id> for Draft<$id> {
|
||||
fn from(data: $id) -> Self {
|
||||
impl From<Box<$id>> for Draft<Box<$id>> {
|
||||
fn from(data: Box<$id>) -> Self {
|
||||
Draft {
|
||||
inner: Arc::new(Mutex::new((data, None))),
|
||||
}
|
||||
@@ -71,12 +79,12 @@ draft_define!(IRuntime);
|
||||
draft_define!(IVerge);
|
||||
|
||||
#[test]
|
||||
fn test_draft() {
|
||||
let verge = IVerge {
|
||||
fn test_draft_box() {
|
||||
let verge = Box::new(IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..IVerge::default()
|
||||
};
|
||||
});
|
||||
|
||||
let draft = Draft::from(verge);
|
||||
|
||||
@@ -86,10 +94,11 @@ fn test_draft() {
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(false);
|
||||
d.enable_tun_mode = Some(true);
|
||||
drop(d);
|
||||
{
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(false);
|
||||
d.enable_tun_mode = Some(true);
|
||||
}
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||
@@ -109,18 +118,17 @@ fn test_draft() {
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(true);
|
||||
drop(d);
|
||||
{
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(true);
|
||||
}
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||
|
||||
assert!(draft.discard().is_some());
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
|
||||
assert!(draft.discard().is_none());
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::utils::{dirs, help};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::{fs, io::Write};
|
||||
use std::{collections::HashSet, fs, io::Write};
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -15,6 +15,14 @@ pub struct IProfiles {
|
||||
pub items: Option<Vec<PrfItem>>,
|
||||
}
|
||||
|
||||
/// 清理结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CleanupResult {
|
||||
pub total_files: usize,
|
||||
pub deleted_files: Vec<String>,
|
||||
pub failed_deletions: Vec<String>,
|
||||
}
|
||||
|
||||
macro_rules! patch {
|
||||
($lv: expr, $rv: expr, $key: tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
@@ -485,4 +493,197 @@ impl IProfiles {
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// 以 app 中的 profile 列表为准,删除不再需要的文件
|
||||
pub fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
|
||||
let profiles_dir = dirs::app_profiles_dir()?;
|
||||
|
||||
if !profiles_dir.exists() {
|
||||
return Ok(CleanupResult {
|
||||
total_files: 0,
|
||||
deleted_files: vec![],
|
||||
failed_deletions: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有 active profile 的文件名集合
|
||||
let active_files = self.get_all_active_files();
|
||||
|
||||
// 添加全局扩展配置文件到保护列表
|
||||
let protected_files = self.get_protected_global_files();
|
||||
|
||||
// 扫描 profiles 目录下的所有文件
|
||||
let mut total_files = 0;
|
||||
let mut deleted_files = vec![];
|
||||
let mut failed_deletions = vec![];
|
||||
|
||||
for entry in std::fs::read_dir(&profiles_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
total_files += 1;
|
||||
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if Self::is_profile_file(file_name) {
|
||||
// 检查是否为全局扩展文件
|
||||
if protected_files.contains(file_name) {
|
||||
log::debug!(target: "app", "保护全局扩展配置文件: {}", file_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否为活跃文件
|
||||
if !active_files.contains(file_name) {
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(_) => {
|
||||
deleted_files.push(file_name.to_string());
|
||||
log::info!(target: "app", "已清理冗余文件: {}", file_name);
|
||||
}
|
||||
Err(e) => {
|
||||
failed_deletions.push(format!("{}: {}", file_name, e));
|
||||
log::warn!(target: "app", "清理文件失败: {} - {}", file_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = CleanupResult {
|
||||
total_files,
|
||||
deleted_files,
|
||||
failed_deletions,
|
||||
};
|
||||
|
||||
log::info!(
|
||||
target: "app",
|
||||
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
|
||||
result.total_files,
|
||||
result.deleted_files.len(),
|
||||
result.failed_deletions.len()
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 不删除全局扩展配置
|
||||
fn get_protected_global_files(&self) -> HashSet<String> {
|
||||
let mut protected_files = HashSet::new();
|
||||
|
||||
protected_files.insert("Merge.yaml".to_string());
|
||||
protected_files.insert("Script.js".to_string());
|
||||
|
||||
protected_files
|
||||
}
|
||||
|
||||
/// 获取所有 active profile 关联的文件名
|
||||
fn get_all_active_files(&self) -> HashSet<String> {
|
||||
let mut active_files = HashSet::new();
|
||||
|
||||
if let Some(items) = &self.items {
|
||||
for item in items {
|
||||
// 收集所有类型 profile 的文件
|
||||
if let Some(file) = &item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
|
||||
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
|
||||
if let Some(itype) = &item.itype {
|
||||
if itype == "remote" || itype == "local" {
|
||||
if let Some(option) = &item.option {
|
||||
// 收集关联的扩展文件
|
||||
if let Some(merge_uid) = &option.merge {
|
||||
if let Ok(merge_item) = self.get_item(merge_uid) {
|
||||
if let Some(file) = &merge_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(script_uid) = &option.script {
|
||||
if let Ok(script_item) = self.get_item(script_uid) {
|
||||
if let Some(file) = &script_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rules_uid) = &option.rules {
|
||||
if let Ok(rules_item) = self.get_item(rules_uid) {
|
||||
if let Some(file) = &rules_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(proxies_uid) = &option.proxies {
|
||||
if let Ok(proxies_item) = self.get_item(proxies_uid) {
|
||||
if let Some(file) = &proxies_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(groups_uid) = &option.groups {
|
||||
if let Ok(groups_item) = self.get_item(groups_uid) {
|
||||
if let Some(file) = &groups_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
active_files
|
||||
}
|
||||
|
||||
/// 检查文件名是否符合 profile 文件的命名规则
|
||||
fn is_profile_file(filename: &str) -> bool {
|
||||
// 匹配各种 profile 文件格式
|
||||
// R12345678.yaml (remote)
|
||||
// L12345678.yaml (local)
|
||||
// m12345678.yaml (merge)
|
||||
// s12345678.js (script)
|
||||
// r12345678.yaml (rules)
|
||||
// p12345678.yaml (proxies)
|
||||
// g12345678.yaml (groups)
|
||||
|
||||
let patterns = [
|
||||
r"^[RL][a-zA-Z0-9]+\.yaml$", // Remote/Local profiles
|
||||
r"^m[a-zA-Z0-9]+\.yaml$", // Merge files
|
||||
r"^s[a-zA-Z0-9]+\.js$", // Script files
|
||||
r"^[rpg][a-zA-Z0-9]+\.yaml$", // Rules/Proxies/Groups files
|
||||
];
|
||||
|
||||
patterns.iter().any(|pattern| {
|
||||
regex::Regex::new(pattern)
|
||||
.map(|re| re.is_match(filename))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn auto_cleanup(&self) -> Result<()> {
|
||||
match self.cleanup_orphaned_files() {
|
||||
Ok(result) => {
|
||||
if !result.deleted_files.is_empty() {
|
||||
log::info!(
|
||||
target: "app",
|
||||
"自动清理完成,删除了 {} 个冗余文件",
|
||||
result.deleted_files.len()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "自动清理失败: {}", e);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
|
||||
utils::{dirs, help, i18n},
|
||||
logging,
|
||||
utils::{dirs, help, i18n, logging::Type},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use log::LevelFilter;
|
||||
@@ -200,6 +201,9 @@ pub struct IVerge {
|
||||
/// 自动进入轻量模式的延迟(分钟)
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
|
||||
/// 启用代理页面自动滚动
|
||||
pub enable_hover_jump_navigator: Option<bool>,
|
||||
|
||||
/// 服务状态跟踪
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
@@ -229,6 +233,93 @@ pub struct IVergeTheme {
|
||||
}
|
||||
|
||||
impl IVerge {
|
||||
/// 有效的clash核心名称
|
||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
||||
|
||||
/// 验证并修正配置文件中的clash_core值
|
||||
pub fn validate_and_fix_config() -> Result<()> {
|
||||
let config_path = dirs::verge_path()?;
|
||||
let mut config = match help::read_yaml::<IVerge>(&config_path) {
|
||||
Ok(config) => config,
|
||||
Err(_) => Self::template(),
|
||||
};
|
||||
|
||||
let mut needs_fix = false;
|
||||
|
||||
if let Some(ref core) = config.clash_core {
|
||||
let core_str = core.trim();
|
||||
if core_str.is_empty() || !Self::VALID_CLASH_CORES.contains(&core_str) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
|
||||
core
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".to_string());
|
||||
needs_fix = true;
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".to_string());
|
||||
needs_fix = true;
|
||||
}
|
||||
|
||||
// 修正后保存配置
|
||||
if needs_fix {
|
||||
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
|
||||
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"配置文件修正完成,需要重新加载配置"
|
||||
);
|
||||
|
||||
Self::reload_config_after_fix(config)?;
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"clash_core配置验证通过: {:?}",
|
||||
config.clash_core
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 配置修正后重新加载配置
|
||||
fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
|
||||
use crate::config::Config;
|
||||
|
||||
let config_draft = Config::verge();
|
||||
*config_draft.draft() = Box::new(updated_config.clone());
|
||||
config_draft.apply();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"内存配置已强制更新,新的clash_core: {:?}",
|
||||
updated_config.clash_core
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_valid_clash_core(&self) -> String {
|
||||
self.clash_core
|
||||
.clone()
|
||||
.unwrap_or_else(|| "verge-mihomo".to_string())
|
||||
}
|
||||
|
||||
fn get_system_language() -> String {
|
||||
let sys_lang = sys_locale::get_locale()
|
||||
.unwrap_or_else(|| String::from("en"))
|
||||
@@ -275,6 +366,7 @@ impl IVerge {
|
||||
tun_tray_icon: Some(false),
|
||||
enable_auto_launch: Some(false),
|
||||
enable_silent_start: Some(false),
|
||||
enable_hover_jump_navigator: Some(true),
|
||||
enable_system_proxy: Some(false),
|
||||
proxy_auto_config: Some(false),
|
||||
pac_file_content: Some(DEFAULT_PAC.into()),
|
||||
@@ -351,6 +443,7 @@ impl IVerge {
|
||||
patch!(enable_tun_mode);
|
||||
patch!(enable_auto_launch);
|
||||
patch!(enable_silent_start);
|
||||
patch!(enable_hover_jump_navigator);
|
||||
patch!(enable_random_port);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
patch!(verge_redir_port);
|
||||
@@ -492,11 +585,14 @@ pub struct IVergeResponse {
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub enable_hover_jump_navigator: Option<bool>,
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
impl From<IVerge> for IVergeResponse {
|
||||
fn from(verge: IVerge) -> Self {
|
||||
// 先获取验证后的clash_core值,避免后续借用冲突
|
||||
let valid_clash_core = verge.get_valid_clash_core();
|
||||
Self {
|
||||
app_log_level: verge.app_log_level,
|
||||
language: verge.language,
|
||||
@@ -528,7 +624,7 @@ impl From<IVerge> for IVergeResponse {
|
||||
proxy_host: verge.proxy_host,
|
||||
theme_setting: verge.theme_setting,
|
||||
web_ui_list: verge.web_ui_list,
|
||||
clash_core: verge.clash_core,
|
||||
clash_core: Some(valid_clash_core),
|
||||
hotkeys: verge.hotkeys,
|
||||
auto_close_connection: verge.auto_close_connection,
|
||||
auto_check_update: verge.auto_check_update,
|
||||
@@ -561,6 +657,7 @@ impl From<IVerge> for IVergeResponse {
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
enable_dns_settings: verge.enable_dns_settings,
|
||||
home_cards: verge.home_cards,
|
||||
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
||||
service_state: verge.service_state,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ impl fmt::Display for RunningMode {
|
||||
}
|
||||
}
|
||||
|
||||
const CLASH_CORES: [&str; 2] = ["verge-mihomo", "verge-mihomo-alpha"];
|
||||
use crate::config::IVerge;
|
||||
|
||||
impl CoreManager {
|
||||
/// 检查文件是否为脚本文件
|
||||
@@ -140,11 +140,11 @@ impl CoreManager {
|
||||
/// 使用默认配置
|
||||
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
*Config::runtime().draft() = Box::new(IRuntime {
|
||||
config: Some(Config::clash().latest().0.clone()),
|
||||
exists_keys: vec![],
|
||||
chain_logs: Default::default(),
|
||||
};
|
||||
});
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
@@ -249,8 +249,7 @@ impl CoreManager {
|
||||
config_path
|
||||
);
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
@@ -442,11 +441,7 @@ impl CoreManager {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
|
||||
let clash_core = Config::verge()
|
||||
.latest()
|
||||
.clash_core
|
||||
.clone()
|
||||
.unwrap_or("verge-mihomo".to_string());
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
|
||||
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
|
||||
@@ -804,7 +799,7 @@ impl CoreManager {
|
||||
return Err(error_message.to_string());
|
||||
}
|
||||
let core: &str = &clash_core.clone().unwrap();
|
||||
if !CLASH_CORES.contains(&core) {
|
||||
if !IVerge::VALID_CLASH_CORES.contains(&core) {
|
||||
let error_message = format!("Clash core invalid name: {}", core);
|
||||
logging!(error, Type::Core, true, "{}", error_message);
|
||||
return Err(error_message);
|
||||
|
||||
@@ -151,6 +151,10 @@ impl NotificationSystem {
|
||||
match window.emit(event_name_str, payload) {
|
||||
Ok(_) => {
|
||||
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||
// 记录成功发送的事件
|
||||
if log::log_enabled!(log::Level::Debug) {
|
||||
log::debug!("Successfully emitted event: {}", event_name_str);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to emit event {}: {}", event_name_str, e);
|
||||
@@ -224,12 +228,27 @@ impl NotificationSystem {
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) {
|
||||
log::info!("NotificationSystem shutdown initiated");
|
||||
self.is_running = false;
|
||||
self.sender = None;
|
||||
|
||||
if let Some(handle) = self.worker_handle.take() {
|
||||
let _ = handle.join();
|
||||
// 先关闭发送端,让接收端知道不会再有新消息
|
||||
if let Some(sender) = self.sender.take() {
|
||||
drop(sender);
|
||||
}
|
||||
|
||||
// 设置超时避免无限等待
|
||||
if let Some(handle) = self.worker_handle.take() {
|
||||
match handle.join() {
|
||||
Ok(_) => {
|
||||
log::info!("NotificationSystem worker thread joined successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("NotificationSystem worker thread join failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("NotificationSystem shutdown completed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,35 +153,76 @@ impl Hotkey {
|
||||
"=== Hotkey Dashboard Window Operation Start ==="
|
||||
);
|
||||
|
||||
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||
AsyncHandler::spawn_blocking(|| {
|
||||
logging!(debug, Type::Hotkey, "Toggle dashboard window visibility");
|
||||
|
||||
// 检查窗口是否存在
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
// 如果窗口可见,则隐藏它
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
logging!(info, Type::Window, "Window is visible, hiding it");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
// 如果窗口不可见,则显示它
|
||||
logging!(info, Type::Window, "Window is hidden, showing it");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
} else {
|
||||
// 如果窗口不存在,创建一个新窗口
|
||||
// 检查是否在轻量模式下,如果是,需要同步处理
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"In lightweight mode, calling open_or_close_dashboard directly"
|
||||
);
|
||||
crate::feat::open_or_close_dashboard();
|
||||
} else {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
"Window does not exist, creating a new one"
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Toggle dashboard window visibility (async)"
|
||||
);
|
||||
resolve::create_window(true);
|
||||
}
|
||||
});
|
||||
|
||||
// 检查窗口是否存在
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
// 如果窗口可见,则隐藏
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
if visible {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window is visible, hiding it"
|
||||
);
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
// 如果窗口不可见,则显示
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window is hidden, showing it"
|
||||
);
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"Failed to check window visibility: {}",
|
||||
e
|
||||
);
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果窗口不存在,创建一个新窗口
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window does not exist, creating a new one"
|
||||
);
|
||||
resolve::create_window(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
@@ -231,10 +272,11 @@ impl Hotkey {
|
||||
|
||||
if is_enable_global_hotkey {
|
||||
f();
|
||||
} else if let Some(window) = app_handle.get_webview_window("main") {
|
||||
} else {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||
let is_visible = window.is_visible().unwrap_or(false);
|
||||
let is_focused = window.is_focused().unwrap_or(false);
|
||||
let is_visible = WindowManager::is_main_window_visible();
|
||||
let is_focused = WindowManager::is_main_window_focused();
|
||||
|
||||
if is_focused && is_visible {
|
||||
f();
|
||||
@@ -289,9 +331,9 @@ impl Hotkey {
|
||||
let func = iter.next();
|
||||
let key = iter.next();
|
||||
|
||||
if func.is_some() && key.is_some() {
|
||||
let func = func.unwrap().trim();
|
||||
let key = key.unwrap().trim();
|
||||
if let (Some(func), Some(key)) = (func, key) {
|
||||
let func = func.trim();
|
||||
let key = key.trim();
|
||||
map.insert(key, func);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -742,8 +742,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
|
||||
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
|
||||
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
|
||||
let clash_bin = format!("{clash_core}{bin_ext}");
|
||||
@@ -845,11 +844,9 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
if version_check {
|
||||
if is_service_available().await.is_ok() {
|
||||
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
if version_check && is_service_available().await.is_ok() {
|
||||
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
if !version_check {
|
||||
|
||||
@@ -235,13 +235,11 @@ impl Sysopt {
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Err(e) = startup_shortcut::remove_shortcut() {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
if let Err(e) = startup_shortcut::remove_shortcut() {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+157
-67
@@ -6,11 +6,7 @@ use crate::{
|
||||
cmd,
|
||||
config::Config,
|
||||
feat, logging,
|
||||
module::{
|
||||
lightweight::{entry_lightweight_mode, is_in_lightweight_mode},
|
||||
mihomo::Rate,
|
||||
},
|
||||
resolve,
|
||||
module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
|
||||
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
|
||||
Type,
|
||||
};
|
||||
@@ -205,33 +201,38 @@ impl Tray {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘菜单(带频率限制)
|
||||
/// 更新托盘菜单
|
||||
pub fn update_menu(&self) -> Result<()> {
|
||||
// 检查是否正在更新或距离上次更新太近
|
||||
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(500);
|
||||
// 调整最小更新间隔,确保状态及时刷新
|
||||
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
// 检查是否已有更新任务在执行
|
||||
// 检查是否正在更新
|
||||
if self.menu_updating.load(Ordering::Acquire) {
|
||||
log::debug!(target: "app", "托盘菜单正在更新中,跳过本次更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 检查更新频率
|
||||
{
|
||||
let last_update = self.last_menu_update.lock();
|
||||
if let Some(last_time) = *last_update {
|
||||
if last_time.elapsed() < MIN_UPDATE_INTERVAL {
|
||||
log::debug!(target: "app", "托盘菜单更新频率过高,跳过本次更新");
|
||||
return Ok(());
|
||||
// 检查更新频率,但允许重要事件跳过频率限制
|
||||
let should_force_update = match std::thread::current().name() {
|
||||
Some("main") => true,
|
||||
_ => {
|
||||
let last_update = self.last_menu_update.lock();
|
||||
if let Some(last_time) = *last_update {
|
||||
last_time.elapsed() >= MIN_UPDATE_INTERVAL
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !should_force_update {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
|
||||
return Ok(()); // 早期返回,避免panic
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -248,6 +249,7 @@ impl Tray {
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
@@ -270,7 +272,7 @@ impl Tray {
|
||||
match app_handle.tray_by_id("main") {
|
||||
Some(tray) => {
|
||||
let _ = tray.set_menu(Some(create_tray_menu(
|
||||
&app_handle,
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
@@ -288,6 +290,7 @@ impl Tray {
|
||||
}
|
||||
|
||||
/// 更新托盘图标
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
@@ -315,58 +318,94 @@ impl Tray {
|
||||
(false, true) => TrayState::get_tun_tray_icon(),
|
||||
(false, false) => TrayState::get_common_tray_icon(),
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false);
|
||||
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
||||
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
let is_colorful = colorful == "colorful";
|
||||
|
||||
if !enable_tray_speed {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
return Ok(());
|
||||
}
|
||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false);
|
||||
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
||||
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
let is_colorful = colorful == "colorful";
|
||||
|
||||
let rate = if let Some(rate) = rate {
|
||||
Some(rate)
|
||||
} else {
|
||||
let guard = self.speed_rate.lock();
|
||||
if let Some(guard) = guard.as_ref() {
|
||||
if let Some(rate) = guard.get_curent_rate() {
|
||||
Some(rate)
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
if !enable_tray_speed {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rate = if let Some(rate) = rate {
|
||||
Some(rate)
|
||||
} else {
|
||||
let guard = self.speed_rate.lock();
|
||||
if let Some(guard) = guard.as_ref() {
|
||||
if let Some(rate) = guard.get_curent_rate() {
|
||||
Some(rate)
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
};
|
||||
|
||||
let mut rate_guard = self.rate_cache.lock();
|
||||
if *rate_guard != rate {
|
||||
*rate_guard = rate;
|
||||
|
||||
let bytes = if enable_tray_icon {
|
||||
Some(icon_bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut rate_guard = self.rate_cache.lock();
|
||||
if *rate_guard != rate {
|
||||
*rate_guard = rate;
|
||||
|
||||
let bytes = if enable_tray_icon {
|
||||
Some(icon_bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rate = rate_guard.as_ref();
|
||||
if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
|
||||
}
|
||||
let rate = rate_guard.as_ref();
|
||||
if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let tray = match app_handle.tray_by_id("main") {
|
||||
Some(tray) => tray,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest().clone();
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
(true, true) => TrayState::get_tun_tray_icon(),
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
||||
(false, true) => TrayState::get_tun_tray_icon(),
|
||||
(false, false) => TrayState::get_common_tray_icon(),
|
||||
};
|
||||
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘显示状态的函数
|
||||
pub fn update_tray_display(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let _tray = app_handle.tray_by_id("main").unwrap();
|
||||
|
||||
// 更新菜单
|
||||
self.update_menu()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘提示
|
||||
@@ -431,6 +470,8 @@ impl Tray {
|
||||
self.update_menu()?;
|
||||
self.update_icon(None)?;
|
||||
self.update_tooltip()?;
|
||||
// 更新轻量模式显示状态
|
||||
self.update_tray_display()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -591,6 +632,12 @@ impl Tray {
|
||||
let icon_bytes = TrayState::get_common_tray_icon().1;
|
||||
let icon = tauri::image::Image::from_bytes(&icon_bytes)?;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let builder = TrayIconBuilder::with_id("main")
|
||||
.icon(icon)
|
||||
.icon_as_template(false);
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let mut builder = TrayIconBuilder::with_id("main")
|
||||
.icon(icon)
|
||||
.icon_as_template(false);
|
||||
@@ -621,10 +668,14 @@ impl Tray {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"main_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "Tray点击事件: 显示主窗口");
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
let _ = resolve::create_window(true);
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {:?}", result);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -634,6 +685,17 @@ impl Tray {
|
||||
log::info!(target: "app", "系统托盘创建成功");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 托盘统一的状态更新函数
|
||||
pub fn update_all_states(&self) -> Result<()> {
|
||||
// 确保所有状态更新完成
|
||||
self.update_menu()?;
|
||||
self.update_icon(None)?;
|
||||
self.update_tooltip()?;
|
||||
self.update_tray_display()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tray_menu(
|
||||
@@ -885,15 +947,23 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
feat::change_clash_mode(mode.into());
|
||||
}
|
||||
"open_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
||||
// 如果在轻量模式中,先退出轻量模式
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
// 然后创建窗口
|
||||
let _ = resolve::create_window(true);
|
||||
// 使用统一的窗口管理器显示窗口
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {:?}", result);
|
||||
}
|
||||
"system_proxy" => {
|
||||
feat::toggle_system_proxy();
|
||||
}
|
||||
"tun_mode" => {
|
||||
feat::toggle_tun_mode(None);
|
||||
}
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => {
|
||||
let _ = cmd::open_app_dir();
|
||||
@@ -906,7 +976,22 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"entry_lightweight_mode" => entry_lightweight_mode(),
|
||||
"entry_lightweight_mode" => {
|
||||
// 处理轻量模式的切换
|
||||
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
|
||||
if was_lightweight {
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
} else {
|
||||
crate::module::lightweight::entry_lightweight_mode();
|
||||
}
|
||||
|
||||
// 退出轻量模式后显示主窗口
|
||||
if was_lightweight {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {:?}", result);
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
feat::quit();
|
||||
}
|
||||
@@ -916,4 +1001,9 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 统一调用状态更新
|
||||
if let Err(e) = Tray::global().update_all_states() {
|
||||
log::warn!(target: "app", "更新托盘状态失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.clash_core.clone(),
|
||||
Some(verge.get_valid_clash_core()),
|
||||
verge.enable_tun_mode.unwrap_or(false),
|
||||
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||
verge.verge_socks_enabled.unwrap_or(false),
|
||||
|
||||
@@ -33,37 +33,40 @@ pub fn use_script(
|
||||
}
|
||||
let _ = context.eval(Source::from_bytes(
|
||||
r#"var console = Object.freeze({
|
||||
log(data){__verge_log__("log",JSON.stringify(data))},
|
||||
info(data){__verge_log__("info",JSON.stringify(data))},
|
||||
error(data){__verge_log__("error",JSON.stringify(data))},
|
||||
debug(data){__verge_log__("debug",JSON.stringify(data))},
|
||||
log(data){__verge_log__("log",JSON.stringify(data, null, 2))},
|
||||
info(data){__verge_log__("info",JSON.stringify(data, null, 2))},
|
||||
error(data){__verge_log__("error",JSON.stringify(data, null, 2))},
|
||||
debug(data){__verge_log__("debug",JSON.stringify(data, null, 2))},
|
||||
warn(data){__verge_log__("warn",JSON.stringify(data, null, 2))},
|
||||
table(data){__verge_log__("table",JSON.stringify(data, null, 2))},
|
||||
});"#,
|
||||
));
|
||||
|
||||
let config = use_lowercase(config.clone());
|
||||
let config_str = serde_json::to_string(&config)?;
|
||||
|
||||
// 仅处理 name 参数中的特殊字符
|
||||
let safe_name = escape_js_string_for_single_quote(&name);
|
||||
|
||||
let code = format!(
|
||||
r#"try{{
|
||||
{script};
|
||||
JSON.stringify(main({config_str},'{name}')||'')
|
||||
JSON.stringify(main({config_str},'{safe_name}')||'')
|
||||
}} catch(err) {{
|
||||
`__error_flag__ ${{err.toString()}}`
|
||||
}}"#
|
||||
);
|
||||
|
||||
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
|
||||
if !result.is_string() {
|
||||
anyhow::bail!("main function should return object");
|
||||
}
|
||||
let result = result.to_string(&mut context).unwrap();
|
||||
let result = result.to_std_string().unwrap();
|
||||
if result.starts_with("__error_flag__") {
|
||||
anyhow::bail!(result[15..].to_owned());
|
||||
}
|
||||
if result == "\"\"" {
|
||||
anyhow::bail!("main function should return object");
|
||||
}
|
||||
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
||||
|
||||
// 直接解析JSON结果,不做其他解析
|
||||
let res: Result<Mapping, Error> = parse_json_safely(&result);
|
||||
|
||||
let mut out = outputs.lock().unwrap();
|
||||
match res {
|
||||
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||
@@ -77,6 +80,27 @@ pub fn use_script(
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_json_safely(json_str: &str) -> Result<Mapping, Error> {
|
||||
let json_str = strip_outer_quotes(json_str);
|
||||
|
||||
Ok(serde_json::from_str::<Mapping>(json_str)?)
|
||||
}
|
||||
|
||||
// 移除字符串外层的引号
|
||||
fn strip_outer_quotes(s: &str) -> &str {
|
||||
let s = s.trim();
|
||||
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
|
||||
&s[1..s.len() - 1]
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
// 转义单引号和反斜杠,用于单引号包裹的JavaScript字符串
|
||||
fn escape_js_string_for_single_quote(s: &str) -> String {
|
||||
s.replace('\\', "\\\\").replace('\'', "\\'")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script() {
|
||||
let script = r#"
|
||||
@@ -104,6 +128,31 @@ fn test_script() {
|
||||
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
|
||||
|
||||
let _ = serde_yaml::to_string(&config).unwrap();
|
||||
|
||||
let yaml_config_size = std::mem::size_of_val(&config);
|
||||
dbg!(yaml_config_size);
|
||||
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
|
||||
dbg!(box_yaml_config_size);
|
||||
dbg!(results);
|
||||
assert!(box_yaml_config_size < yaml_config_size);
|
||||
}
|
||||
|
||||
// 测试特殊字符转义功能
|
||||
#[test]
|
||||
fn test_escape_unescape() {
|
||||
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
||||
let escaped = escape_js_string_for_single_quote(test_string);
|
||||
println!("Original: {}", test_string);
|
||||
println!("Escaped: {}", escaped);
|
||||
|
||||
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
||||
let parsed = parse_json_safely(json_str).unwrap();
|
||||
|
||||
assert!(parsed.contains_key("key"));
|
||||
assert!(parsed.contains_key("nested"));
|
||||
|
||||
let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#;
|
||||
let parsed_quoted = parse_json_safely(quoted_json_str).unwrap();
|
||||
|
||||
assert!(parsed_quoted.contains_key("key"));
|
||||
assert!(parsed_quoted.contains_key("nested"));
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod service;
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -84,7 +84,7 @@ pub fn change_clash_mode(mode: String) {
|
||||
after_change_clash_mode();
|
||||
}
|
||||
}
|
||||
Err(err) => println!("{err}"),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ use crate::{
|
||||
cmd,
|
||||
config::{Config, PrfItem, PrfOption},
|
||||
core::{handle, CoreManager, *},
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
@@ -29,7 +31,7 @@ pub async fn update_profile(
|
||||
option: Option<PrfOption>,
|
||||
auto_refresh: Option<bool>,
|
||||
) -> Result<()> {
|
||||
println!("[订阅更新] 开始更新订阅 {}", uid);
|
||||
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
|
||||
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
|
||||
|
||||
let url_opt = {
|
||||
@@ -39,13 +41,13 @@ pub async fn update_profile(
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||
log::info!(target: "app", "[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
println!("[订阅更新] {} 缺少URL,无法更新", uid);
|
||||
log::warn!(target: "app", "[订阅更新] {} 缺少URL,无法更新", uid);
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
println!(
|
||||
log::info!(target: "app",
|
||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||
uid,
|
||||
item.url.clone().unwrap()
|
||||
@@ -56,24 +58,24 @@ pub async fn update_profile(
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
println!("[订阅更新] 开始下载新的订阅内容");
|
||||
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
|
||||
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
|
||||
|
||||
// 尝试使用正常设置更新
|
||||
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||
Ok(item) => {
|
||||
println!("[订阅更新] 更新订阅配置成功");
|
||||
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(err) => {
|
||||
// 首次更新失败,尝试使用Clash代理
|
||||
println!("[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
|
||||
log::warn!(target: "app", "[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
|
||||
|
||||
// 发送通知
|
||||
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
||||
@@ -90,7 +92,7 @@ pub async fn update_profile(
|
||||
// 使用Clash代理重试
|
||||
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
|
||||
Ok(mut item) => {
|
||||
println!("[订阅更新] 使用Clash代理更新成功");
|
||||
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
|
||||
|
||||
// 恢复原始代理设置到item
|
||||
if let Some(option) = item.option.as_mut() {
|
||||
@@ -110,11 +112,11 @@ pub async fn update_profile(
|
||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(retry_err) => {
|
||||
println!("[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
|
||||
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
|
||||
handle::Handle::notice_message(
|
||||
"update_failed_even_with_clash",
|
||||
format!("{}", retry_err),
|
||||
@@ -129,14 +131,14 @@ pub async fn update_profile(
|
||||
};
|
||||
|
||||
if should_update {
|
||||
println!("[订阅更新] 更新内核配置");
|
||||
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
println!("[订阅更新] 更新成功");
|
||||
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[订阅更新] 更新失败: {}", err);
|
||||
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
|
||||
handle::Handle::notice_message("update_failed", format!("{err}"));
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
|
||||
+156
-63
@@ -3,55 +3,37 @@ use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, sysopt, CoreManager},
|
||||
logging,
|
||||
module::mihomo::MihomoManager,
|
||||
utils::resolve,
|
||||
utils::logging::Type,
|
||||
};
|
||||
|
||||
/// Open or close the dashboard window
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard() {
|
||||
println!("Attempting to open/close dashboard");
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
println!("Found existing window");
|
||||
log::info!(target: "app", "Found existing window");
|
||||
|
||||
// 如果窗口存在,则切换其显示状态
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
println!("Window visibility status: {}", visible);
|
||||
log::info!(target: "app", "Window visibility status: {}", visible);
|
||||
|
||||
if visible {
|
||||
println!("Attempting to hide window");
|
||||
log::info!(target: "app", "Attempting to hide window");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
println!("Attempting to show and focus window");
|
||||
log::info!(target: "app", "Attempting to show and focus window");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get window visibility: {:?}", e);
|
||||
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No existing window found, creating new window");
|
||||
log::info!(target: "app", "No existing window found, creating new window");
|
||||
resolve::create_window(true);
|
||||
// 检查是否在轻量模式下
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "Window operation result: {:?}", result);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用统一的窗口管理器切换窗口状态
|
||||
let result = WindowManager::toggle_main_window();
|
||||
log::info!(target: "app", "Window toggle result: {:?}", result);
|
||||
}
|
||||
|
||||
/// 优化的应用退出函数
|
||||
/// 异步优化的应用退出函数
|
||||
pub fn quit() {
|
||||
log::debug!(target: "app", "启动退出流程");
|
||||
use crate::process::AsyncHandler;
|
||||
logging!(debug, Type::System, true, "启动退出流程");
|
||||
|
||||
// 获取应用句柄并设置退出标志
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
@@ -60,55 +42,166 @@ pub fn quit() {
|
||||
// 优先关闭窗口,提供立即反馈
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
log::info!(target: "app", "窗口已隐藏");
|
||||
}
|
||||
|
||||
// 在单独线程中处理资源清理,避免阻塞主线程
|
||||
std::thread::spawn(move || {
|
||||
let cleanup_result = clean();
|
||||
app_handle.exit(match cleanup_result {
|
||||
true => 0,
|
||||
false => 1,
|
||||
});
|
||||
// 使用异步任务处理资源清理,避免阻塞
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::System, true, "开始异步清理资源");
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"资源清理完成,退出代码: {}",
|
||||
if cleanup_result { 0 } else { 1 }
|
||||
);
|
||||
app_handle.exit(if cleanup_result { 0 } else { 1 });
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clean() -> bool {
|
||||
async fn clean_async() -> bool {
|
||||
use tokio::time::{timeout, Duration};
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let cleanup_result = rt.block_on(async {
|
||||
// 1. 处理TUN模式
|
||||
let tun_success = if Config::verge().data().enable_tun_mode.unwrap_or(false) {
|
||||
|
||||
logging!(info, Type::System, true, "开始执行异步清理操作...");
|
||||
|
||||
// 1. 处理TUN模式
|
||||
let tun_task = async {
|
||||
if Config::verge().data().enable_tun_mode.unwrap_or(false) {
|
||||
let disable_tun = serde_json::json!({
|
||||
"tun": {
|
||||
"enable": false
|
||||
}
|
||||
});
|
||||
timeout(
|
||||
Duration::from_secs(1),
|
||||
match timeout(
|
||||
Duration::from_secs(2),
|
||||
MihomoManager::global().patch_configs(disable_tun),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "TUN模式已禁用");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "禁用TUN模式超时");
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 顺序执行关键清理
|
||||
let proxy_res = timeout(
|
||||
Duration::from_secs(1),
|
||||
// 2. 系统代理重置
|
||||
let proxy_task = async {
|
||||
match timeout(
|
||||
Duration::from_secs(3),
|
||||
sysopt::Sysopt::global().reset_sysproxy(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "系统代理已重置");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "重置系统代理超时");
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let core_res = timeout(Duration::from_secs(1), CoreManager::global().stop_core()).await;
|
||||
// 3. 核心服务停止
|
||||
let core_task = async {
|
||||
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "核心服务已停止");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "停止核心服务超时");
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 平台特定清理
|
||||
#[cfg(target_os = "macos")]
|
||||
let _dns_res = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await;
|
||||
// 4. DNS恢复(仅macOS)
|
||||
#[cfg(target_os = "macos")]
|
||||
let dns_task = async {
|
||||
match timeout(
|
||||
Duration::from_millis(1000),
|
||||
crate::utils::resolve::restore_public_dns(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "DNS设置已恢复");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "恢复DNS设置超时");
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tun_success && proxy_res.is_ok() && core_res.is_ok()
|
||||
// 并行执行所有清理任务
|
||||
let (tun_success, proxy_success, core_success) = tokio::join!(tun_task, proxy_task, core_task);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let dns_success = dns_task.await;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let dns_success = true;
|
||||
|
||||
let all_success = tun_success && proxy_success && core_success && dns_success;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
|
||||
tun_success,
|
||||
proxy_success,
|
||||
core_success,
|
||||
dns_success,
|
||||
all_success
|
||||
);
|
||||
|
||||
all_success
|
||||
}
|
||||
|
||||
pub fn clean() -> bool {
|
||||
use crate::process::AsyncHandler;
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::System, true, "开始执行清理操作...");
|
||||
|
||||
// 使用已有的异步清理函数
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
// 发送结果
|
||||
let _ = tx.send(cleanup_result);
|
||||
});
|
||||
cleanup_result
|
||||
|
||||
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
|
||||
Ok(result) => {
|
||||
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
|
||||
result
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::System,
|
||||
true,
|
||||
"清理操作超时,返回成功状态避免阻塞"
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
+22
-11
@@ -2,10 +2,10 @@ mod cmd;
|
||||
mod config;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod error;
|
||||
mod feat;
|
||||
mod module;
|
||||
mod process;
|
||||
mod state;
|
||||
mod utils;
|
||||
use crate::{
|
||||
core::hotkey,
|
||||
@@ -15,8 +15,8 @@ use crate::{
|
||||
use config::Config;
|
||||
use std::sync::{Mutex, Once};
|
||||
use tauri::AppHandle;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::Manager;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tokio::time::{timeout, Duration};
|
||||
@@ -90,17 +90,20 @@ pub fn run() {
|
||||
|
||||
let _ = utils::dirs::init_portable_flag();
|
||||
|
||||
// 单例检测
|
||||
let app_exists: bool = AsyncHandler::block_on(move || async move {
|
||||
// 异步单例检测
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
||||
match timeout(Duration::from_secs(3), server::check_singleton()).await {
|
||||
Ok(result) => {
|
||||
if result.is_err() {
|
||||
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
||||
true
|
||||
if let Some(app_handle) = AppHandleManager::global().get() {
|
||||
app_handle.exit(0);
|
||||
} else {
|
||||
std::process::exit(0);
|
||||
}
|
||||
} else {
|
||||
logging!(info, Type::Setup, true, "未检测到其他应用实例");
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -110,13 +113,9 @@ pub fn run() {
|
||||
true,
|
||||
"单例检查超时,假定没有其他实例运行"
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
if app_exists {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
@@ -163,6 +162,14 @@ pub fn run() {
|
||||
});
|
||||
});
|
||||
|
||||
// 窗口管理
|
||||
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
|
||||
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
||||
.with_filename("window_state.json")
|
||||
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
|
||||
.build();
|
||||
let _ = app.handle().plugin(window_state_plugin);
|
||||
|
||||
// 异步处理
|
||||
let app_handle = app.handle().clone();
|
||||
AsyncHandler::spawn(move || async move {
|
||||
@@ -205,6 +212,9 @@ pub fn run() {
|
||||
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
|
||||
}
|
||||
|
||||
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
|
||||
app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||
Ok(())
|
||||
})
|
||||
@@ -253,6 +263,7 @@ pub fn run() {
|
||||
cmd::invoke_uwp_tool,
|
||||
cmd::copy_clash_env,
|
||||
cmd::get_proxies,
|
||||
cmd::force_refresh_proxies,
|
||||
cmd::get_providers_proxies,
|
||||
cmd::save_dns_config,
|
||||
cmd::apply_dns_config,
|
||||
@@ -366,7 +377,7 @@ pub fn run() {
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
println!("closing window...");
|
||||
log::info!(target: "app", "closing window...");
|
||||
api.prevent_close();
|
||||
if let Some(window) = core::handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
|
||||
@@ -1,47 +1,92 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, timer::Timer},
|
||||
log_err, logging, logging_error,
|
||||
core::{handle, timer::Timer, tray::Tray},
|
||||
log_err, logging,
|
||||
state::lightweight::LightWeightState,
|
||||
utils::logging::Type,
|
||||
AppHandleManager,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::logging_error;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::TaskBuilder;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{Listener, Manager};
|
||||
|
||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||
|
||||
// 轻量模式状态标志
|
||||
static IS_LIGHTWEIGHT_MODE: OnceCell<Arc<RwLock<bool>>> = OnceCell::new();
|
||||
|
||||
// 添加一个锁来防止并发退出轻量模式
|
||||
static EXIT_LOCK: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
|
||||
|
||||
fn get_lightweight_mode() -> &'static Arc<RwLock<bool>> {
|
||||
IS_LIGHTWEIGHT_MODE.get_or_init(|| Arc::new(RwLock::new(false)))
|
||||
fn with_lightweight_status<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut LightWeightState) -> R,
|
||||
{
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let state = app_handle.state::<Mutex<LightWeightState>>();
|
||||
let mut guard = state.lock().unwrap();
|
||||
f(&mut guard)
|
||||
}
|
||||
|
||||
fn get_exit_lock() -> &'static Mutex<(bool, Instant)> {
|
||||
EXIT_LOCK.get_or_init(|| Mutex::new((false, Instant::now())))
|
||||
pub fn run_once_auto_lightweight() {
|
||||
LightWeightState::default().run_once_time(|| {
|
||||
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true);
|
||||
let enable_auto = Config::verge()
|
||||
.data()
|
||||
.enable_auto_light_weight_mode
|
||||
.unwrap_or(true);
|
||||
if enable_auto && is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"正常创建窗口和添加定时器监听器"
|
||||
);
|
||||
set_lightweight_mode(false);
|
||||
disable_auto_light_weight_mode();
|
||||
|
||||
// 触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn auto_lightweight_mode_init() {
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
let _ = app_handle.state::<Mutex<LightWeightState>>();
|
||||
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
|
||||
let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
|
||||
|
||||
if enable_auto && is_silent_start {
|
||||
logging!(info, Type::Lightweight, true, "自动轻量模式静默启动");
|
||||
set_lightweight_mode(true);
|
||||
enable_auto_light_weight_mode();
|
||||
|
||||
// 确保托盘状态更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否处于轻量模式
|
||||
pub fn is_in_lightweight_mode() -> bool {
|
||||
*get_lightweight_mode().read()
|
||||
with_lightweight_status(|state| state.is_lightweight)
|
||||
}
|
||||
|
||||
// 设置轻量模式状态
|
||||
fn set_lightweight_mode(value: bool) {
|
||||
let mut mode = get_lightweight_mode().write();
|
||||
*mode = value;
|
||||
logging!(info, Type::Lightweight, true, "轻量模式状态: {}", value);
|
||||
with_lightweight_status(|state| {
|
||||
state.set_lightweight_mode(value);
|
||||
});
|
||||
|
||||
// 触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_auto_light_weight_mode() {
|
||||
@@ -58,10 +103,18 @@ pub fn disable_auto_light_weight_mode() {
|
||||
}
|
||||
|
||||
pub fn entry_lightweight_mode() {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
let result = WindowManager::hide_main_window();
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"轻量模式隐藏窗口结果: {:?}",
|
||||
result
|
||||
);
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
let _ = window.hide();
|
||||
}
|
||||
if let Some(webview) = window.get_webview_window("main") {
|
||||
let _ = webview.destroy();
|
||||
}
|
||||
@@ -69,49 +122,36 @@ pub fn entry_lightweight_mode() {
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
}
|
||||
// 标记已进入轻量模式
|
||||
set_lightweight_mode(true);
|
||||
let _ = cancel_light_weight_timer();
|
||||
|
||||
// 更新托盘显示
|
||||
let _tray = crate::core::tray::Tray::global();
|
||||
}
|
||||
|
||||
// 添加从轻量模式恢复的函数
|
||||
pub fn exit_lightweight_mode() {
|
||||
// 获取锁,检查是否已经有退出操作在进行中
|
||||
let mut exit_lock = get_exit_lock().lock();
|
||||
let (is_exiting, last_exit_time) = *exit_lock;
|
||||
let now = Instant::now();
|
||||
|
||||
// 如果已经有一个退出操作在进行,并且距离上次退出时间不超过2秒,跳过本次退出
|
||||
if is_exiting && now.duration_since(last_exit_time) < Duration::from_secs(2) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"已有退出轻量模式操作正在进行中,跳过本次请求"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
*exit_lock = (true, now);
|
||||
|
||||
// 确保当前确实处于轻量模式才执行退出操作
|
||||
if !is_in_lightweight_mode() {
|
||||
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
|
||||
exit_lock.0 = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记退出轻量模式
|
||||
set_lightweight_mode(false);
|
||||
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
|
||||
|
||||
// macOS激活策略
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
|
||||
// 重置UI就绪状态
|
||||
crate::utils::resolve::reset_ui_ready();
|
||||
|
||||
// 释放锁
|
||||
exit_lock.0 = false;
|
||||
// 更新托盘显示
|
||||
let _tray = crate::core::tray::Tray::global();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn add_light_weight_timer() {
|
||||
logging_error!(Type::Lightweight, setup_light_weight_timer());
|
||||
}
|
||||
@@ -156,19 +196,20 @@ fn cancel_window_close_listener() {
|
||||
|
||||
fn setup_light_weight_timer() -> Result<()> {
|
||||
Timer::global().init()?;
|
||||
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
let mut timer_count = Timer::global().timer_count.lock();
|
||||
|
||||
let task_id = *timer_count;
|
||||
*timer_count += 1;
|
||||
|
||||
let once_by_minutes = Config::verge()
|
||||
.latest()
|
||||
.auto_light_weight_minutes
|
||||
.unwrap_or(10);
|
||||
|
||||
// 获取task_id
|
||||
let task_id = {
|
||||
let mut timer_count = Timer::global().timer_count.lock();
|
||||
let id = *timer_count;
|
||||
*timer_count += 1;
|
||||
id
|
||||
};
|
||||
|
||||
// 创建任务
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(task_id)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
@@ -179,17 +220,24 @@ fn setup_light_weight_timer() -> Result<()> {
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
delay_timer
|
||||
.add_task(task)
|
||||
.context("failed to add timer task")?;
|
||||
// 添加任务到定时器
|
||||
{
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
delay_timer
|
||||
.add_task(task)
|
||||
.context("failed to add timer task")?;
|
||||
}
|
||||
|
||||
let timer_task = crate::core::timer::TimerTask {
|
||||
task_id,
|
||||
interval_minutes: once_by_minutes,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
|
||||
// 更新任务映射
|
||||
{
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let timer_task = crate::core::timer::TimerTask {
|
||||
task_id,
|
||||
interval_minutes: once_by_minutes,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
|
||||
@@ -3,7 +3,9 @@ use mihomo_api;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::http::{HeaderMap, HeaderValue};
|
||||
use tauri::http::HeaderMap;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::http::HeaderValue;
|
||||
|
||||
// 缓存的最大有效期(5秒)
|
||||
const CACHE_TTL: Duration = Duration::from_secs(5);
|
||||
@@ -105,6 +107,7 @@ impl MihomoManager {
|
||||
}
|
||||
|
||||
// 提供默认值的版本,避免在connection_info为None时panic
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_clash_client_info_or_default() -> (String, HeaderMap) {
|
||||
Self::get_clash_client_info().unwrap_or_else(|| {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
@@ -11,20 +11,4 @@ impl AsyncHandler {
|
||||
{
|
||||
async_runtime::spawn(f())
|
||||
}
|
||||
|
||||
pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
|
||||
where
|
||||
F: FnOnce() -> R + Send + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
async_runtime::spawn_blocking(f)
|
||||
}
|
||||
|
||||
pub fn block_on<F, Fut, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = R>,
|
||||
{
|
||||
async_runtime::block_on(f())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use std::sync::{Arc, Once, OnceLock};
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LightWeightState {
|
||||
#[allow(unused)]
|
||||
once: Arc<Once>,
|
||||
pub is_lightweight: bool,
|
||||
}
|
||||
|
||||
impl LightWeightState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
once: Arc::new(Once::new()),
|
||||
is_lightweight: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn run_once_time<F>(&self, f: F)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
self.once.call_once(f);
|
||||
}
|
||||
|
||||
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
|
||||
self.is_lightweight = value;
|
||||
if value {
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
} else {
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LightWeightState {
|
||||
fn default() -> Self {
|
||||
static INSTANCE: OnceLock<LightWeightState> = OnceLock::new();
|
||||
INSTANCE.get_or_init(LightWeightState::new).clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
|
||||
// https://tauri.app/develop/state-management/#do-you-need-arc
|
||||
|
||||
pub mod lightweight;
|
||||
pub mod proxy;
|
||||
@@ -0,0 +1,19 @@
|
||||
use serde_json::Value;
|
||||
|
||||
pub struct CmdProxyState {
|
||||
pub last_refresh_time: std::time::Instant,
|
||||
pub need_refresh: bool,
|
||||
pub proxies: Box<Value>,
|
||||
pub providers_proxies: Box<Value>,
|
||||
}
|
||||
|
||||
impl Default for CmdProxyState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_refresh_time: std::time::Instant::now(),
|
||||
need_refresh: true,
|
||||
proxies: Box::new(Value::Null),
|
||||
providers_proxies: Box::new(Value::Null),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,8 +204,8 @@ pub fn format_bytes_speed(speed: u64) -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_format_bytes_speed() {
|
||||
assert_eq!(format_bytes_speed(0), "0B/s");
|
||||
assert_eq!(format_bytes_speed(1023), "1023B/s");
|
||||
assert_eq!(format_bytes_speed(0), "0.0B/s");
|
||||
assert_eq!(format_bytes_speed(1023), "1.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
|
||||
|
||||
@@ -286,6 +286,9 @@ pub fn init_config() -> Result<()> {
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
// 验证并修正verge.yaml中的clash_core配置
|
||||
crate::log_err!(IVerge::validate_and_fix_config());
|
||||
|
||||
crate::log_err!(dirs::profiles_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
|
||||
|
||||
@@ -8,3 +8,4 @@ pub mod network;
|
||||
pub mod resolve;
|
||||
pub mod server;
|
||||
pub mod tmpl;
|
||||
pub mod window_manager;
|
||||
|
||||
+193
-47
@@ -1,8 +1,10 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::{Config, IVerge, PrfItem},
|
||||
core::*,
|
||||
logging, logging_error,
|
||||
module::lightweight,
|
||||
module::lightweight::{self, auto_lightweight_mode_init},
|
||||
process::AsyncHandler,
|
||||
utils::{init, logging::Type, server},
|
||||
wrap_err,
|
||||
@@ -142,7 +144,8 @@ pub async fn find_unused_port() -> Result<u16> {
|
||||
|
||||
/// 异步方式处理启动后的额外任务
|
||||
pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
logging!(info, Type::Setup, true, "执行异步设置任务...");
|
||||
let start_time = std::time::Instant::now();
|
||||
logging!(info, Type::Setup, true, "开始执行异步设置任务...");
|
||||
|
||||
if VERSION.get().is_none() {
|
||||
let version = app_handle.package_info().version.to_string();
|
||||
@@ -169,6 +172,15 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
logging!(trace, Type::Config, true, "初始化配置...");
|
||||
logging_error!(Type::Config, true, Config::init_config().await);
|
||||
|
||||
// 启动时清理冗余的 Profile 文件
|
||||
logging!(info, Type::Setup, true, "清理冗余的Profile文件...");
|
||||
let profiles = Config::profiles();
|
||||
if let Err(e) = profiles.latest().auto_cleanup() {
|
||||
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
|
||||
} else {
|
||||
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
|
||||
}
|
||||
|
||||
logging!(trace, Type::Core, true, "启动核心管理器...");
|
||||
logging_error!(Type::Core, true, CoreManager::global().init().await);
|
||||
|
||||
@@ -208,24 +220,46 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
|
||||
// 创建窗口
|
||||
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if is_silent_start {
|
||||
use crate::AppHandleManager;
|
||||
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
}
|
||||
}
|
||||
create_window(!is_silent_start);
|
||||
|
||||
// 初始化定时器
|
||||
logging_error!(Type::System, true, timer::Timer::global().init());
|
||||
|
||||
// 自动进入轻量模式
|
||||
let enable_auto_light_weight_mode =
|
||||
{ Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
|
||||
if enable_auto_light_weight_mode && !is_silent_start {
|
||||
lightweight::enable_auto_light_weight_mode();
|
||||
}
|
||||
auto_lightweight_mode_init();
|
||||
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_part());
|
||||
|
||||
logging!(trace, Type::System, true, "初始化热键...");
|
||||
logging_error!(Type::System, true, hotkey::Hotkey::global().init());
|
||||
|
||||
logging!(info, Type::Setup, true, "异步设置任务完成");
|
||||
let elapsed = start_time.elapsed();
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"异步设置任务完成,耗时: {:?}",
|
||||
elapsed
|
||||
);
|
||||
|
||||
// 如果初始化时间过长,记录警告
|
||||
if elapsed.as_secs() > 10 {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"异步设置任务耗时较长({:?})",
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// reset system proxy (异步)
|
||||
@@ -258,12 +292,27 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
is_show
|
||||
);
|
||||
|
||||
if !is_show {
|
||||
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
|
||||
handle::Handle::notify_startup_completed();
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
|
||||
if is_show {
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -299,7 +348,72 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
.fullscreen(false)
|
||||
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
|
||||
.min_inner_size(520.0, 520.0)
|
||||
.visible(false)
|
||||
.visible(true) // 立即显示窗口,避免用户等待
|
||||
.initialization_script(
|
||||
r#"
|
||||
console.log('[Tauri] 窗口初始化脚本开始执行');
|
||||
|
||||
function createLoadingOverlay() {
|
||||
|
||||
if (document.getElementById('initial-loading-overlay')) {
|
||||
console.log('[Tauri] 加载指示器已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Tauri] 创建加载指示器');
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.id = 'initial-loading-overlay';
|
||||
loadingDiv.innerHTML = `
|
||||
<div style="
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; z-index: 9999;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transition: opacity 0.3s ease;
|
||||
">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<div style="
|
||||
width: 40px; height: 40px; border: 3px solid #e3e3e3;
|
||||
border-top: 3px solid #3498db; border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
"></div>
|
||||
</div>
|
||||
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --bg-color: #1a1a1a; --text-color: #ffffff; }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
if (document.body) {
|
||||
document.body.appendChild(loadingDiv);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.body && !document.getElementById('initial-loading-overlay')) {
|
||||
document.body.appendChild(loadingDiv);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createLoadingOverlay();
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', createLoadingOverlay);
|
||||
} else {
|
||||
createLoadingOverlay();
|
||||
}
|
||||
|
||||
console.log('[Tauri] 窗口初始化脚本执行完成');
|
||||
"#,
|
||||
)
|
||||
.build()
|
||||
{
|
||||
Ok(newly_created_window) => {
|
||||
@@ -318,70 +432,102 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
"异步窗口任务开始 (启动已标记完成)"
|
||||
);
|
||||
|
||||
// 先运行轻量模式检测
|
||||
lightweight::run_once_auto_lightweight();
|
||||
|
||||
// 发送启动完成事件,触发前端开始加载
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"发送 verge://startup-completed 事件"
|
||||
);
|
||||
handle::Handle::notify_startup_completed();
|
||||
|
||||
if is_show {
|
||||
let window_clone = newly_created_window.clone();
|
||||
|
||||
// Attempt to show and focus the window first.
|
||||
// 立即显示窗口
|
||||
let _ = window_clone.show();
|
||||
let _ = window_clone.set_focus();
|
||||
logging!(debug, Type::Window, true, "窗口已尝试显示和聚焦");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await; // Crucial delay
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"延时后,尝试发送 verge://startup-completed 事件"
|
||||
);
|
||||
handle::Handle::notify_startup_completed();
|
||||
logging!(info, Type::Window, true, "窗口已立即显示");
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
|
||||
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
2
|
||||
3
|
||||
} else {
|
||||
5
|
||||
8
|
||||
};
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"等待UI就绪 (最多{}秒)...",
|
||||
"开始监控UI加载状态 (最多{}秒)...",
|
||||
timeout_seconds
|
||||
);
|
||||
|
||||
let wait_result =
|
||||
tokio::time::timeout(Duration::from_secs(timeout_seconds), async {
|
||||
while !*get_ui_ready().read() {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
})
|
||||
.await;
|
||||
// 异步监控UI状态,不影响窗口显示
|
||||
tokio::spawn(async move {
|
||||
let wait_result =
|
||||
tokio::time::timeout(Duration::from_secs(timeout_seconds), async {
|
||||
let mut check_count = 0;
|
||||
while !*get_ui_ready().read() {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
check_count += 1;
|
||||
|
||||
match wait_result {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Window, true, "UI就绪");
|
||||
// 每2秒记录一次等待状态
|
||||
if check_count % 20 == 0 {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI加载状态检查... ({}秒)",
|
||||
check_count / 10
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match wait_result {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Window, true, "UI已完全加载就绪");
|
||||
// 移除初始加载指示器
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let _ = window.eval(r#"
|
||||
const overlay = document.getElementById('initial-loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.style.opacity = '0';
|
||||
setTimeout(() => overlay.remove(), 300);
|
||||
}
|
||||
"#);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI加载监控超时({}秒),但窗口已正常显示",
|
||||
timeout_seconds
|
||||
);
|
||||
*get_ui_ready().write() = true;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"等待UI就绪超时({}秒),强制标记就绪",
|
||||
timeout_seconds
|
||||
);
|
||||
*get_ui_ready().write() = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logging!(info, Type::Window, true, "窗口显示流程完成");
|
||||
} else {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"is_show为false,窗口保持隐藏。尝试发送启动事件。"
|
||||
"is_show为false,窗口保持隐藏状态"
|
||||
);
|
||||
handle::Handle::notify_startup_completed();
|
||||
}
|
||||
});
|
||||
true
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
use crate::{core::handle, logging, utils::logging::Type};
|
||||
use tauri::{Manager, WebviewWindow, Wry};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
|
||||
/// 窗口操作结果
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum WindowOperationResult {
|
||||
/// 窗口已显示并获得焦点
|
||||
Shown,
|
||||
/// 窗口已隐藏
|
||||
Hidden,
|
||||
/// 创建了新窗口
|
||||
Created,
|
||||
/// 操作失败
|
||||
Failed,
|
||||
/// 无需操作
|
||||
NoAction,
|
||||
}
|
||||
|
||||
/// 窗口状态
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum WindowState {
|
||||
/// 窗口可见且有焦点
|
||||
VisibleFocused,
|
||||
/// 窗口可见但无焦点
|
||||
VisibleUnfocused,
|
||||
/// 窗口最小化
|
||||
Minimized,
|
||||
/// 窗口隐藏
|
||||
Hidden,
|
||||
/// 窗口不存在
|
||||
NotExist,
|
||||
}
|
||||
|
||||
/// 统一的窗口管理器
|
||||
pub struct WindowManager;
|
||||
|
||||
impl WindowManager {
|
||||
pub fn get_main_window_state() -> WindowState {
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
WindowState::Minimized
|
||||
} else if window.is_visible().unwrap_or(false) {
|
||||
if window.is_focused().unwrap_or(false) {
|
||||
WindowState::VisibleFocused
|
||||
} else {
|
||||
WindowState::VisibleUnfocused
|
||||
}
|
||||
} else {
|
||||
WindowState::Hidden
|
||||
}
|
||||
} else {
|
||||
WindowState::NotExist
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取主窗口实例
|
||||
pub fn get_main_window() -> Option<WebviewWindow<Wry>> {
|
||||
handle::Handle::global()
|
||||
.app_handle()
|
||||
.and_then(|app| app.get_webview_window("main"))
|
||||
}
|
||||
|
||||
/// 智能显示主窗口
|
||||
pub fn show_main_window() -> WindowOperationResult {
|
||||
logging!(info, Type::Window, true, "开始智能显示主窗口");
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"{}",
|
||||
Self::get_window_status_info()
|
||||
);
|
||||
|
||||
let current_state = Self::get_main_window_state();
|
||||
|
||||
match current_state {
|
||||
WindowState::NotExist => {
|
||||
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
|
||||
if Self::create_new_window() {
|
||||
WindowOperationResult::Created
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
WindowState::VisibleFocused => {
|
||||
logging!(info, Type::Window, true, "窗口已经可见且有焦点,无需操作");
|
||||
WindowOperationResult::NoAction
|
||||
}
|
||||
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
Self::activate_window(&window)
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换主窗口显示状态(显示/隐藏)
|
||||
pub fn toggle_main_window() -> WindowOperationResult {
|
||||
logging!(info, Type::Window, true, "开始切换主窗口显示状态");
|
||||
|
||||
let current_state = Self::get_main_window_state();
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"当前窗口状态: {:?}",
|
||||
current_state
|
||||
);
|
||||
|
||||
match current_state {
|
||||
WindowState::NotExist => {
|
||||
// 窗口不存在,创建新窗口
|
||||
if Self::create_new_window() {
|
||||
WindowOperationResult::Created
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
WindowState::VisibleFocused => {
|
||||
// 窗口可见且有焦点,隐藏它
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
if window.hide().is_ok() {
|
||||
logging!(info, Type::Window, true, "窗口已隐藏");
|
||||
WindowOperationResult::Hidden
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
|
||||
// 窗口存在但不可见或无焦点,激活它
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
Self::activate_window(&window)
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 激活窗口(取消最小化、显示、设置焦点)
|
||||
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult {
|
||||
logging!(info, Type::Window, true, "开始激活窗口");
|
||||
|
||||
let mut operations_successful = true;
|
||||
|
||||
// 1. 如果窗口最小化,先取消最小化
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
|
||||
if let Err(e) = window.unminimize() {
|
||||
logging!(warn, Type::Window, true, "取消最小化失败: {}", e);
|
||||
operations_successful = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 显示窗口
|
||||
if let Err(e) = window.show() {
|
||||
logging!(warn, Type::Window, true, "显示窗口失败: {}", e);
|
||||
operations_successful = false;
|
||||
}
|
||||
|
||||
// 3. 设置焦点
|
||||
if let Err(e) = window.set_focus() {
|
||||
logging!(warn, Type::Window, true, "设置窗口焦点失败: {}", e);
|
||||
operations_successful = false;
|
||||
}
|
||||
|
||||
// 4. 平台特定的激活策略
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows 尝试额外的激活方法
|
||||
if let Err(e) = window.set_always_on_top(true) {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"设置置顶失败(非关键错误): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
// 立即取消置顶
|
||||
if let Err(e) = window.set_always_on_top(false) {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"取消置顶失败(非关键错误): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if operations_successful {
|
||||
logging!(info, Type::Window, true, "窗口激活成功");
|
||||
WindowOperationResult::Shown
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "窗口激活部分失败");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
|
||||
/// 隐藏主窗口
|
||||
pub fn hide_main_window() -> WindowOperationResult {
|
||||
logging!(info, Type::Window, true, "开始隐藏主窗口");
|
||||
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
if window.hide().is_ok() {
|
||||
logging!(info, Type::Window, true, "窗口已隐藏");
|
||||
WindowOperationResult::Hidden
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "隐藏窗口失败");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
} else {
|
||||
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
|
||||
WindowOperationResult::NoAction
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查窗口是否可见
|
||||
pub fn is_main_window_visible() -> bool {
|
||||
Self::get_main_window()
|
||||
.map(|window| window.is_visible().unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 检查窗口是否有焦点
|
||||
pub fn is_main_window_focused() -> bool {
|
||||
Self::get_main_window()
|
||||
.map(|window| window.is_focused().unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 检查窗口是否最小化
|
||||
pub fn is_main_window_minimized() -> bool {
|
||||
Self::get_main_window()
|
||||
.map(|window| window.is_minimized().unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 创建新窗口现有的实现
|
||||
fn create_new_window() -> bool {
|
||||
use crate::utils::resolve;
|
||||
resolve::create_window(true)
|
||||
}
|
||||
|
||||
/// 获取详细的窗口状态信息
|
||||
pub fn get_window_status_info() -> String {
|
||||
let state = Self::get_main_window_state();
|
||||
let is_visible = Self::is_main_window_visible();
|
||||
let is_focused = Self::is_main_window_focused();
|
||||
let is_minimized = Self::is_main_window_minimized();
|
||||
|
||||
format!(
|
||||
"窗口状态: {:?} | 可见: {} | 有焦点: {} | 最小化: {}",
|
||||
state, is_visible, is_focused, is_minimized
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,10 @@
|
||||
name = "mihomo_api"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
debug = []
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
reqwest = { version = "0.12.20", features = ["json"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
tokio = { version = "1.44.1", features = ["rt", "macros", "time"] }
|
||||
tokio = { version = "1.45.1", features = ["rt", "macros", "time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use reqwest::{Method, header::HeaderMap};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::time::Duration;
|
||||
pub mod model;
|
||||
pub use model::{MihomoData, MihomoManager};
|
||||
pub use model::MihomoManager;
|
||||
|
||||
impl MihomoManager {
|
||||
pub fn new(mihomo_server: String, headers: HeaderMap) -> Self {
|
||||
@@ -20,38 +17,10 @@ impl MihomoManager {
|
||||
|
||||
Self {
|
||||
mihomo_server,
|
||||
data: Arc::new(Mutex::new(MihomoData {
|
||||
proxies: serde_json::Value::Null,
|
||||
providers_proxies: serde_json::Value::Null,
|
||||
})),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_proxies(&self, proxies: serde_json::Value) {
|
||||
let mut data = self.data.lock().expect("Mutex poisoned");
|
||||
data.proxies = proxies;
|
||||
}
|
||||
|
||||
fn update_providers_proxies(&self, providers_proxies: serde_json::Value) {
|
||||
let mut data = self.data.lock().expect("Mutex poisoned");
|
||||
data.providers_proxies = providers_proxies;
|
||||
}
|
||||
|
||||
pub fn get_mihomo_server(&self) -> String {
|
||||
self.mihomo_server.clone()
|
||||
}
|
||||
|
||||
pub fn get_proxies(&self) -> serde_json::Value {
|
||||
let data = self.data.lock().expect("Mutex poisoned");
|
||||
data.proxies.clone()
|
||||
}
|
||||
|
||||
pub fn get_providers_proxies(&self) -> serde_json::Value {
|
||||
let data = self.data.lock().expect("Mutex poisoned");
|
||||
data.providers_proxies.clone()
|
||||
}
|
||||
|
||||
async fn send_request(
|
||||
&self,
|
||||
method: Method,
|
||||
@@ -87,18 +56,16 @@ impl MihomoManager {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn refresh_proxies(&self) -> Result<&Self, String> {
|
||||
pub async fn get_refresh_proxies(&self) -> Result<Value, String> {
|
||||
let url = format!("{}/proxies", self.mihomo_server);
|
||||
let proxies = self.send_request(Method::GET, url, None).await?;
|
||||
self.update_proxies(proxies);
|
||||
Ok(self)
|
||||
Ok(proxies)
|
||||
}
|
||||
|
||||
pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> {
|
||||
pub async fn get_providers_proxies(&self) -> Result<Value, String> {
|
||||
let url = format!("{}/providers/proxies", self.mihomo_server);
|
||||
let providers_proxies = self.send_request(Method::GET, url, None).await?;
|
||||
self.update_providers_proxies(providers_proxies);
|
||||
Ok(self)
|
||||
Ok(providers_proxies)
|
||||
}
|
||||
|
||||
pub async fn close_all_connections(&self) -> Result<(), String> {
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct MihomoData {
|
||||
pub(crate) proxies: serde_json::Value,
|
||||
pub(crate) providers_proxies: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MihomoManager {
|
||||
pub(crate) mihomo_server: String,
|
||||
pub(crate) data: Arc<Mutex<MihomoData>>,
|
||||
pub(crate) client: reqwest::Client,
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
impl Drop for MihomoData {
|
||||
fn drop(&mut self) {
|
||||
println!("Dropping MihomoData");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
impl Drop for MihomoManager {
|
||||
fn drop(&mut self) {
|
||||
println!("Dropping MihomoManager");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,7 @@
|
||||
use mihomo_api;
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
#[test]
|
||||
fn test_mihomo_manager_init() {
|
||||
let manager = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
|
||||
assert_eq!(manager.get_proxies(), serde_json::Value::Null);
|
||||
assert_eq!(manager.get_providers_proxies(), serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_refresh_proxies() {
|
||||
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
|
||||
let manager = manager.refresh_proxies().await.unwrap();
|
||||
let proxies = manager.get_proxies();
|
||||
let providers = manager.get_providers_proxies();
|
||||
assert_ne!(proxies, serde_json::Value::Null);
|
||||
assert_eq!(providers, serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_refresh_providers_proxies() {
|
||||
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
|
||||
let manager = manager.refresh_providers_proxies().await.unwrap();
|
||||
let proxies = manager.get_proxies();
|
||||
let providers = manager.get_providers_proxies();
|
||||
assert_eq!(proxies, serde_json::Value::Null);
|
||||
assert_ne!(providers, serde_json::Value::Null);
|
||||
let _ = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
|
||||
assert_eq!(true, true);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
@@ -11,15 +11,9 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [
|
||||
"resources",
|
||||
"resources/locales/*"
|
||||
],
|
||||
"resources": ["resources", "resources/locales/*"],
|
||||
"publisher": "Clash Verge Rev",
|
||||
"externalBin": [
|
||||
"sidecar/verge-mihomo",
|
||||
"sidecar/verge-mihomo-alpha"
|
||||
],
|
||||
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
|
||||
"copyright": "GNU General Public License v3.0",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "Clash Verge Rev",
|
||||
@@ -50,28 +44,18 @@
|
||||
},
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": [
|
||||
"clash",
|
||||
"clash-verge"
|
||||
]
|
||||
"schemes": ["clash", "clash-verge"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"capabilities": [
|
||||
"desktop-capability",
|
||||
"migrated"
|
||||
],
|
||||
"capabilities": ["desktop-capability", "migrated"],
|
||||
"assetProtocol": {
|
||||
"scope": [
|
||||
"$APPDATA/**",
|
||||
"$RESOURCE/../**",
|
||||
"**"
|
||||
],
|
||||
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
|
||||
"enable": true
|
||||
},
|
||||
"csp": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"windows": []
|
||||
"windows": [],
|
||||
"security": {
|
||||
"capabilities": [
|
||||
"desktop-capability",
|
||||
"desktop-windows-capability",
|
||||
"migrated"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
user-select: none;
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Snackbar, Alert, IconButton, Box } from '@mui/material';
|
||||
import { CloseRounded } from '@mui/icons-material';
|
||||
import { subscribeNotices, hideNotice, NoticeItem } from '@/services/noticeService';
|
||||
import React, { useSyncExternalStore } from "react";
|
||||
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
|
||||
import { CloseRounded } from "@mui/icons-material";
|
||||
import {
|
||||
subscribeNotices,
|
||||
hideNotice,
|
||||
getSnapshotNotices,
|
||||
} from "@/services/noticeService";
|
||||
|
||||
export const NoticeManager: React.FC = () => {
|
||||
const [currentNotices, setCurrentNotices] = useState<NoticeItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribeNotices((notices) => {
|
||||
setCurrentNotices(notices);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
const currentNotices = useSyncExternalStore(
|
||||
subscribeNotices,
|
||||
getSnapshotNotices,
|
||||
);
|
||||
|
||||
const handleClose = (id: number) => {
|
||||
hideNotice(id);
|
||||
@@ -23,49 +20,49 @@ export const NoticeManager: React.FC = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
position: "fixed",
|
||||
top: "20px",
|
||||
right: "20px",
|
||||
zIndex: 1500,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
maxWidth: '360px',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
maxWidth: "360px",
|
||||
}}
|
||||
>
|
||||
{currentNotices.map((notice) => (
|
||||
<Snackbar
|
||||
key={notice.id}
|
||||
open={true}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
transform: 'none',
|
||||
top: 'auto',
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
left: 'auto',
|
||||
width: '100%',
|
||||
position: "relative",
|
||||
transform: "none",
|
||||
top: "auto",
|
||||
right: "auto",
|
||||
bottom: "auto",
|
||||
left: "auto",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
severity={notice.type}
|
||||
variant="filled"
|
||||
sx={{ width: '100%' }}
|
||||
action={
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
onClick={() => handleClose(notice.id)}
|
||||
>
|
||||
<CloseRounded fontSize="inherit" />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{notice.message}
|
||||
</Alert>
|
||||
<Alert
|
||||
severity={notice.type}
|
||||
variant="filled"
|
||||
sx={{ width: "100%" }}
|
||||
action={
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
onClick={() => handleClose(notice.id)}
|
||||
>
|
||||
<CloseRounded fontSize="inherit" />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{notice.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ export const BaseSearchBox = (props: SearchProps) => {
|
||||
</Tooltip>
|
||||
</Box>
|
||||
),
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -107,7 +107,14 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
{information.map((each) => (
|
||||
<div key={each.label}>
|
||||
<b>{each.label}</b>
|
||||
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span>
|
||||
<span
|
||||
style={{
|
||||
wordBreak: "break-all",
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
: {each.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ClashInfoCard = () => {
|
||||
// 使用备忘录组件内容,减少重新渲染
|
||||
const cardContent = useMemo(() => {
|
||||
if (!clashConfig) return null;
|
||||
|
||||
|
||||
return (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
|
||||
@@ -23,12 +23,24 @@ export const ClashModeCard = () => {
|
||||
// 直接使用API返回的模式,不维护本地状态
|
||||
const currentMode = clashConfig?.mode?.toLowerCase();
|
||||
|
||||
const modeDescription = useMemo(() => {
|
||||
if (typeof currentMode === "string" && currentMode.length > 0) {
|
||||
return t(
|
||||
`${currentMode[0].toLocaleUpperCase()}${currentMode.slice(1)} Mode Description`,
|
||||
);
|
||||
}
|
||||
return t("Mode Description Not Available");
|
||||
}, [currentMode]);
|
||||
|
||||
// 模式图标映射
|
||||
const modeIcons = useMemo(() => ({
|
||||
rule: <MultipleStopRounded fontSize="small" />,
|
||||
global: <LanguageRounded fontSize="small" />,
|
||||
direct: <DirectionsRounded fontSize="small" />
|
||||
}), []);
|
||||
const modeIcons = useMemo(
|
||||
() => ({
|
||||
rule: <MultipleStopRounded fontSize="small" />,
|
||||
global: <LanguageRounded fontSize="small" />,
|
||||
direct: <DirectionsRounded fontSize="small" />,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// 切换模式的处理函数
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
@@ -68,18 +80,19 @@ export const ClashModeCard = () => {
|
||||
"&:active": {
|
||||
transform: "translateY(1px)",
|
||||
},
|
||||
"&::after": mode === currentMode
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -16,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 16,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
"&::after":
|
||||
mode === currentMode
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -16,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 16,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
});
|
||||
|
||||
// 描述样式
|
||||
@@ -143,12 +156,8 @@ export const ClashModeCard = () => {
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={descriptionStyles}
|
||||
>
|
||||
{t(`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`)}
|
||||
<Typography variant="caption" component="div" sx={descriptionStyles}>
|
||||
{modeDescription}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -105,7 +105,7 @@ export const CurrentProxyCard = () => {
|
||||
// 添加排序类型状态
|
||||
const [sortType, setSortType] = useState<ProxySortType>(() => {
|
||||
const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE);
|
||||
return savedSortType ? Number(savedSortType) as ProxySortType : 0;
|
||||
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0;
|
||||
});
|
||||
|
||||
// 定义状态类型
|
||||
@@ -156,7 +156,8 @@ export const CurrentProxyCard = () => {
|
||||
primaryKeywords.some((keyword) =>
|
||||
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||
),
|
||||
) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
|
||||
) ||
|
||||
proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
|
||||
|
||||
return primaryGroup?.name || "";
|
||||
};
|
||||
@@ -200,11 +201,13 @@ export const CurrentProxyCard = () => {
|
||||
// 只保留 Selector 类型的组用于选择
|
||||
const filteredGroups = proxies.groups
|
||||
.filter((g: { name: string; type?: string }) => g.type === "Selector")
|
||||
.map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({
|
||||
name: g.name,
|
||||
now: g.now || "",
|
||||
all: g.all.map((p: { name: string }) => p.name),
|
||||
}));
|
||||
.map(
|
||||
(g: { name: string; now: string; all: Array<{ name: string }> }) => ({
|
||||
name: g.name,
|
||||
now: g.now || "",
|
||||
all: g.all.map((p: { name: string }) => p.name),
|
||||
}),
|
||||
);
|
||||
|
||||
let newProxy = "";
|
||||
let newDisplayProxy = null;
|
||||
@@ -230,12 +233,12 @@ export const CurrentProxyCard = () => {
|
||||
if (selectorGroup) {
|
||||
newGroup = selectorGroup.name;
|
||||
newProxy = selectorGroup.now || selectorGroup.all[0] || "";
|
||||
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
if (newProxy) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
if (newProxy) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,7 +283,9 @@ export const CurrentProxyCard = () => {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
|
||||
setState((prev) => {
|
||||
const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup);
|
||||
const group = prev.proxyData.groups.find(
|
||||
(g: { name: string }) => g.name === newGroup,
|
||||
);
|
||||
if (group) {
|
||||
return {
|
||||
...prev,
|
||||
@@ -368,14 +373,16 @@ export const CurrentProxyCard = () => {
|
||||
}, [state.displayProxy]);
|
||||
|
||||
// 获取当前节点的延迟(增加非空校验)
|
||||
const currentDelay = currentProxy && state.selection.group
|
||||
? delayManager.getDelayFix(currentProxy, state.selection.group)
|
||||
: -1;
|
||||
const currentDelay =
|
||||
currentProxy && state.selection.group
|
||||
? delayManager.getDelayFix(currentProxy, state.selection.group)
|
||||
: -1;
|
||||
|
||||
// 信号图标(增加非空校验)
|
||||
const signalInfo = currentProxy && state.selection.group
|
||||
? getSignalIcon(currentDelay)
|
||||
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
|
||||
const signalInfo =
|
||||
currentProxy && state.selection.group
|
||||
? getSignalIcon(currentDelay)
|
||||
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
|
||||
|
||||
// 自定义渲染选择框中的值
|
||||
const renderProxyValue = useCallback(
|
||||
@@ -384,7 +391,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
state.proxyData.records[selected],
|
||||
state.selection.group
|
||||
state.selection.group,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -441,7 +448,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
return list;
|
||||
},
|
||||
[sortType, state.proxyData.records, state.selection.group]
|
||||
[sortType, state.proxyData.records, state.selection.group],
|
||||
);
|
||||
|
||||
// 计算要显示的代理选项(增加非空校验)
|
||||
@@ -452,11 +459,11 @@ export const CurrentProxyCard = () => {
|
||||
if (isGlobalMode && proxies?.global) {
|
||||
const options = proxies.global.all
|
||||
.filter((p: any) => {
|
||||
const name = typeof p === 'string' ? p : p.name;
|
||||
const name = typeof p === "string" ? p : p.name;
|
||||
return name !== "DIRECT" && name !== "REJECT";
|
||||
})
|
||||
.map((p: any) => ({
|
||||
name: typeof p === 'string' ? p : p.name
|
||||
name: typeof p === "string" ? p : p.name,
|
||||
}));
|
||||
|
||||
return sortProxies(options);
|
||||
@@ -464,7 +471,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
// 规则模式
|
||||
const group = state.selection.group
|
||||
? state.proxyData.groups.find(g => g.name === state.selection.group)
|
||||
? state.proxyData.groups.find((g) => g.name === state.selection.group)
|
||||
: null;
|
||||
|
||||
if (group) {
|
||||
@@ -473,7 +480,14 @@ export const CurrentProxyCard = () => {
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortProxies]);
|
||||
}, [
|
||||
isDirectMode,
|
||||
isGlobalMode,
|
||||
proxies,
|
||||
state.proxyData,
|
||||
state.selection.group,
|
||||
sortProxies,
|
||||
]);
|
||||
|
||||
// 获取排序图标
|
||||
const getSortIcon = () => {
|
||||
@@ -660,12 +674,14 @@ export const CurrentProxyCard = () => {
|
||||
{isDirectMode
|
||||
? null
|
||||
: proxyOptions.map((proxy, index) => {
|
||||
const delayValue = state.proxyData.records[proxy.name] && state.selection.group
|
||||
? delayManager.getDelayFix(
|
||||
state.proxyData.records[proxy.name],
|
||||
state.selection.group,
|
||||
)
|
||||
: -1;
|
||||
const delayValue =
|
||||
state.proxyData.records[proxy.name] &&
|
||||
state.selection.group
|
||||
? delayManager.getDelayFix(
|
||||
state.proxyData.records[proxy.name],
|
||||
state.selection.group,
|
||||
)
|
||||
: -1;
|
||||
return (
|
||||
<MenuItem
|
||||
key={`${proxy.name}-${index}`}
|
||||
@@ -706,4 +722,4 @@ export const CurrentProxyCard = () => {
|
||||
)}
|
||||
</EnhancedCard>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const EnhancedCard = ({
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
display: "block"
|
||||
display: "block",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -62,13 +62,15 @@ export const EnhancedCard = ({
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -87,9 +89,9 @@ export const EnhancedCard = ({
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
{typeof title === "string" ? (
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight="medium"
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight="medium"
|
||||
fontSize={18}
|
||||
sx={titleTruncateStyle}
|
||||
title={title}
|
||||
@@ -97,9 +99,7 @@ export const EnhancedCard = ({
|
||||
{title}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={titleTruncateStyle}>
|
||||
{title}
|
||||
</Box>
|
||||
<Box sx={titleTruncateStyle}>{title}</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -30,7 +30,7 @@ ChartJS.register(
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip,
|
||||
Filler
|
||||
Filler,
|
||||
);
|
||||
|
||||
// 流量数据项接口
|
||||
@@ -54,8 +54,8 @@ type DataPoint = ITrafficItem & { name: string; timestamp: number };
|
||||
/**
|
||||
* 增强型流量图表组件
|
||||
*/
|
||||
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
(props, ref) => {
|
||||
export const EnhancedTrafficGraph = memo(
|
||||
forwardRef<EnhancedTrafficGraphRef>((props, ref) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -63,20 +63,20 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>(10);
|
||||
const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
|
||||
const [displayData, setDisplayData] = useState<DataPoint[]>([]);
|
||||
|
||||
|
||||
// 数据缓冲区
|
||||
const dataBufferRef = useRef<DataPoint[]>([]);
|
||||
|
||||
// 根据时间范围计算保留的数据点数量
|
||||
const getMaxPointsByTimeRange = useCallback(
|
||||
(minutes: TimeRange): number => minutes * 60,
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
// 最大数据点数量
|
||||
const MAX_BUFFER_SIZE = useMemo(
|
||||
() => getMaxPointsByTimeRange(10),
|
||||
[getMaxPointsByTimeRange]
|
||||
[getMaxPointsByTimeRange],
|
||||
);
|
||||
|
||||
// 颜色配置
|
||||
@@ -89,23 +89,28 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
text: theme.palette.text.primary,
|
||||
tooltipBorder: theme.palette.divider,
|
||||
}),
|
||||
[theme]
|
||||
[theme],
|
||||
);
|
||||
|
||||
// 切换时间范围
|
||||
const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => {
|
||||
event.stopPropagation();
|
||||
setTimeRange((prevRange) => {
|
||||
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 点击图表主体或图例时切换样式
|
||||
const handleToggleStyleClick = useCallback((event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
|
||||
}, []);
|
||||
const handleTimeRangeClick = useCallback(
|
||||
(event: React.MouseEvent<SVGTextElement>) => {
|
||||
event.stopPropagation();
|
||||
setTimeRange((prevRange) => {
|
||||
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 点击图表主体或图例时切换样式
|
||||
const handleToggleStyleClick = useCallback(
|
||||
(event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 初始化数据缓冲区
|
||||
useEffect(() => {
|
||||
@@ -121,7 +126,9 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
let nameValue: string;
|
||||
try {
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn(`Initial data generation: Invalid date for timestamp ${pointTime}`);
|
||||
console.warn(
|
||||
`Initial data generation: Invalid date for timestamp ${pointTime}`,
|
||||
);
|
||||
nameValue = "??:??:??";
|
||||
} else {
|
||||
nameValue = date.toLocaleTimeString("en-US", {
|
||||
@@ -132,7 +139,14 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error in toLocaleTimeString during initial data gen:", e, "Date:", date, "Timestamp:", pointTime);
|
||||
console.error(
|
||||
"Error in toLocaleTimeString during initial data gen:",
|
||||
e,
|
||||
"Date:",
|
||||
date,
|
||||
"Timestamp:",
|
||||
pointTime,
|
||||
);
|
||||
nameValue = "Err:Time";
|
||||
}
|
||||
|
||||
@@ -142,55 +156,66 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
timestamp: pointTime,
|
||||
name: nameValue,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
dataBufferRef.current = initialBuffer;
|
||||
|
||||
|
||||
// 更新显示数据
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(initialBuffer.slice(-pointsToShow));
|
||||
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
|
||||
// 添加数据点方法
|
||||
const appendData = useCallback((data: ITrafficItem) => {
|
||||
const safeData = {
|
||||
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
|
||||
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
|
||||
};
|
||||
const appendData = useCallback(
|
||||
(data: ITrafficItem) => {
|
||||
const safeData = {
|
||||
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
|
||||
down:
|
||||
typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
|
||||
};
|
||||
|
||||
const timestamp = data.timestamp || Date.now();
|
||||
const date = new Date(timestamp);
|
||||
const timestamp = data.timestamp || Date.now();
|
||||
const date = new Date(timestamp);
|
||||
|
||||
let nameValue: string;
|
||||
try {
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
|
||||
nameValue = "??:??:??";
|
||||
} else {
|
||||
nameValue = date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
let nameValue: string;
|
||||
try {
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
|
||||
nameValue = "??:??:??";
|
||||
} else {
|
||||
nameValue = date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Error in toLocaleTimeString in appendData:",
|
||||
e,
|
||||
"Date:",
|
||||
date,
|
||||
"Timestamp:",
|
||||
timestamp,
|
||||
);
|
||||
nameValue = "Err:Time";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error in toLocaleTimeString in appendData:", e, "Date:", date, "Timestamp:", timestamp);
|
||||
nameValue = "Err:Time";
|
||||
}
|
||||
// 带时间标签的新数据点
|
||||
const newPoint: DataPoint = {
|
||||
...safeData,
|
||||
name: nameValue,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
// 带时间标签的新数据点
|
||||
const newPoint: DataPoint = {
|
||||
...safeData,
|
||||
name: nameValue,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
|
||||
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
||||
dataBufferRef.current = newBuffer;
|
||||
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
||||
dataBufferRef.current = newBuffer;
|
||||
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(newBuffer.slice(-pointsToShow));
|
||||
}, [timeRange, getMaxPointsByTimeRange]);
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(newBuffer.slice(-pointsToShow));
|
||||
},
|
||||
[timeRange, getMaxPointsByTimeRange],
|
||||
);
|
||||
|
||||
// 监听时间范围变化
|
||||
useEffect(() => {
|
||||
@@ -202,7 +227,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
|
||||
// 切换图表样式
|
||||
const toggleStyle = useCallback(() => {
|
||||
setChartStyle((prev) => prev === "line" ? "area" : "line");
|
||||
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
|
||||
}, []);
|
||||
|
||||
// 暴露方法给父组件
|
||||
@@ -212,30 +237,31 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
appendData,
|
||||
toggleStyle,
|
||||
}),
|
||||
[appendData, toggleStyle]
|
||||
[appendData, toggleStyle],
|
||||
);
|
||||
|
||||
|
||||
const formatYAxis = useCallback((value: number | string): string => {
|
||||
if (typeof value !== 'number') return String(value);
|
||||
if (typeof value !== "number") return String(value);
|
||||
const [num, unit] = parseTraffic(value);
|
||||
return `${num}${unit}`;
|
||||
}, []);
|
||||
|
||||
const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => {
|
||||
const dataPoint = displayData[index as number];
|
||||
if (dataPoint && dataPoint.name) {
|
||||
const parts = dataPoint.name.split(":");
|
||||
return `${parts[0]}:${parts[1]}`;
|
||||
}
|
||||
if(typeof tickValue === 'string') {
|
||||
const parts = tickValue.split(":");
|
||||
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
|
||||
return tickValue;
|
||||
}
|
||||
return '';
|
||||
}, [displayData]);
|
||||
|
||||
const formatXLabel = useCallback(
|
||||
(tickValue: string | number, index: number, ticks: any[]) => {
|
||||
const dataPoint = displayData[index as number];
|
||||
if (dataPoint && dataPoint.name) {
|
||||
const parts = dataPoint.name.split(":");
|
||||
return `${parts[0]}:${parts[1]}`;
|
||||
}
|
||||
if (typeof tickValue === "string") {
|
||||
const parts = tickValue.split(":");
|
||||
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
|
||||
return tickValue;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
[displayData],
|
||||
);
|
||||
|
||||
// 获取当前时间范围文本
|
||||
const getTimeRangeText = useCallback(() => {
|
||||
@@ -243,13 +269,13 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
}, [timeRange, t]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
const labels = displayData.map(d => d.name);
|
||||
const labels = displayData.map((d) => d.name);
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: t("Upload"),
|
||||
data: displayData.map(d => d.up),
|
||||
data: displayData.map((d) => d.up),
|
||||
borderColor: colors.up,
|
||||
backgroundColor: chartStyle === "area" ? colors.up : colors.up,
|
||||
fill: chartStyle === "area",
|
||||
@@ -260,7 +286,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
},
|
||||
{
|
||||
label: t("Download"),
|
||||
data: displayData.map(d => d.down),
|
||||
data: displayData.map((d) => d.down),
|
||||
borderColor: colors.down,
|
||||
backgroundColor: chartStyle === "area" ? colors.down : colors.down,
|
||||
fill: chartStyle === "area",
|
||||
@@ -268,113 +294,130 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
borderWidth: 2,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [displayData, colors.up, colors.down, t, chartStyle]);
|
||||
|
||||
const chartOptions = useMemo(() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false as false,
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
type: 'category' as const,
|
||||
labels: displayData.map(d => d.name),
|
||||
ticks: {
|
||||
const chartOptions = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false as false,
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
color: colors.text,
|
||||
font: { size: 10 },
|
||||
callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined {
|
||||
let labelToFormat: string | undefined = undefined;
|
||||
type: "category" as const,
|
||||
labels: displayData.map((d) => d.name),
|
||||
ticks: {
|
||||
display: true,
|
||||
color: colors.text,
|
||||
font: { size: 10 },
|
||||
callback: function (
|
||||
this: Scale,
|
||||
tickValue: string | number,
|
||||
index: number,
|
||||
ticks: Tick[],
|
||||
): string | undefined {
|
||||
let labelToFormat: string | undefined = undefined;
|
||||
|
||||
const currentDisplayTick = ticks[index];
|
||||
if (currentDisplayTick && typeof currentDisplayTick.label === 'string') {
|
||||
labelToFormat = currentDisplayTick.label;
|
||||
} else {
|
||||
const sourceLabels = displayData.map(d => d.name);
|
||||
if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) {
|
||||
labelToFormat = sourceLabels[tickValue];
|
||||
} else if (typeof tickValue === 'string') {
|
||||
labelToFormat = tickValue;
|
||||
const currentDisplayTick = ticks[index];
|
||||
if (
|
||||
currentDisplayTick &&
|
||||
typeof currentDisplayTick.label === "string"
|
||||
) {
|
||||
labelToFormat = currentDisplayTick.label;
|
||||
} else {
|
||||
const sourceLabels = displayData.map((d) => d.name);
|
||||
if (
|
||||
typeof tickValue === "number" &&
|
||||
tickValue >= 0 &&
|
||||
tickValue < sourceLabels.length
|
||||
) {
|
||||
labelToFormat = sourceLabels[tickValue];
|
||||
} else if (typeof tickValue === "string") {
|
||||
labelToFormat = tickValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof labelToFormat !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof labelToFormat !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parts: string[] = labelToFormat.split(':');
|
||||
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat;
|
||||
const parts: string[] = labelToFormat.split(":");
|
||||
return parts.length >= 2
|
||||
? `${parts[0]}:${parts[1]}`
|
||||
: labelToFormat;
|
||||
},
|
||||
autoSkip: true,
|
||||
maxTicksLimit: Math.max(
|
||||
5,
|
||||
Math.floor(displayData.length / (timeRange * 2)),
|
||||
),
|
||||
minRotation: 0,
|
||||
maxRotation: 0,
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
drawOnChartArea: false,
|
||||
drawTicks: true,
|
||||
tickLength: 2,
|
||||
color: colors.text,
|
||||
},
|
||||
autoSkip: true,
|
||||
maxTicksLimit: Math.max(5, Math.floor(displayData.length / (timeRange * 2))),
|
||||
minRotation: 0,
|
||||
maxRotation: 0,
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
drawOnChartArea: false,
|
||||
drawTicks: true,
|
||||
tickLength: 2,
|
||||
color: colors.text,
|
||||
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: colors.text,
|
||||
font: { size: 10 },
|
||||
callback: formatYAxis,
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
drawTicks: true,
|
||||
tickLength: 3,
|
||||
color: colors.grid,
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: colors.text,
|
||||
font: { size: 10 },
|
||||
callback: formatYAxis,
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
drawTicks: true,
|
||||
tickLength: 3,
|
||||
color: colors.grid,
|
||||
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
mode: 'index' as const,
|
||||
intersect: false,
|
||||
backgroundColor: colors.tooltipBg,
|
||||
titleColor: colors.text,
|
||||
bodyColor: colors.text,
|
||||
borderColor: colors.tooltipBorder,
|
||||
borderWidth: 1,
|
||||
cornerRadius: 4,
|
||||
padding: 8,
|
||||
callbacks: {
|
||||
title: (tooltipItems: any[]) => {
|
||||
return `${t("Time")}: ${tooltipItems[0].label}`;
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
mode: "index" as const,
|
||||
intersect: false,
|
||||
backgroundColor: colors.tooltipBg,
|
||||
titleColor: colors.text,
|
||||
bodyColor: colors.text,
|
||||
borderColor: colors.tooltipBorder,
|
||||
borderWidth: 1,
|
||||
cornerRadius: 4,
|
||||
padding: 8,
|
||||
callbacks: {
|
||||
title: (tooltipItems: any[]) => {
|
||||
return `${t("Time")}: ${tooltipItems[0].label}`;
|
||||
},
|
||||
label: (context: any): string => {
|
||||
const label = context.dataset.label || "";
|
||||
const value = context.parsed.y;
|
||||
const [num, unit] = parseTraffic(value);
|
||||
return `${label}: ${num} ${unit}/s`;
|
||||
},
|
||||
},
|
||||
label: (context: any): string => {
|
||||
const label = context.dataset.label || '';
|
||||
const value = context.parsed.y;
|
||||
const [num, unit] = parseTraffic(value);
|
||||
return `${label}: ${num} ${unit}/s`;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
right: 7,
|
||||
left: 3,
|
||||
}
|
||||
}
|
||||
}), [colors, t, formatYAxis, timeRange, displayData]);
|
||||
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
right: 7,
|
||||
left: 3,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[colors, t, formatYAxis, timeRange, displayData],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -392,8 +435,17 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
{displayData.length > 0 && (
|
||||
<ChartJsLine data={chartData} options={chartOptions} />
|
||||
)}
|
||||
|
||||
<svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}>
|
||||
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<text
|
||||
x="3.5%"
|
||||
y="10%"
|
||||
@@ -402,11 +454,11 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
fontSize={11}
|
||||
fontWeight="bold"
|
||||
onClick={handleTimeRangeClick}
|
||||
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||
style={{ cursor: "pointer", pointerEvents: "all" }}
|
||||
>
|
||||
{getTimeRangeText()}
|
||||
</text>
|
||||
|
||||
|
||||
<text
|
||||
x="99%"
|
||||
y="10%"
|
||||
@@ -415,7 +467,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
onClick={handleToggleStyleClick}
|
||||
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||
style={{ cursor: "pointer", pointerEvents: "all" }}
|
||||
>
|
||||
{t("Upload")}
|
||||
</text>
|
||||
@@ -428,7 +480,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
onClick={handleToggleStyleClick}
|
||||
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||
style={{ cursor: "pointer", pointerEvents: "all" }}
|
||||
>
|
||||
{t("Download")}
|
||||
</text>
|
||||
@@ -436,7 +488,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
));
|
||||
}),
|
||||
);
|
||||
|
||||
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";
|
||||
|
||||
@@ -66,85 +66,90 @@ const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
|
||||
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
|
||||
|
||||
// 统计卡片组件 - 使用memo优化
|
||||
const CompactStatCard = memo(({
|
||||
icon,
|
||||
title,
|
||||
value,
|
||||
unit,
|
||||
color,
|
||||
onClick,
|
||||
}: StatCardProps) => {
|
||||
const theme = useTheme();
|
||||
const CompactStatCard = memo(
|
||||
({ icon, title, value, unit, color, onClick }: StatCardProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// 获取调色板颜色 - 使用useMemo避免重复计算
|
||||
const colorValue = useMemo(() => {
|
||||
const palette = theme.palette;
|
||||
if (
|
||||
color in palette &&
|
||||
palette[color as keyof typeof palette] &&
|
||||
"main" in (palette[color as keyof typeof palette] as PaletteColor)
|
||||
) {
|
||||
return (palette[color as keyof typeof palette] as PaletteColor).main;
|
||||
}
|
||||
return palette.primary.main;
|
||||
}, [theme.palette, color]);
|
||||
// 获取调色板颜色 - 使用useMemo避免重复计算
|
||||
const colorValue = useMemo(() => {
|
||||
const palette = theme.palette;
|
||||
if (
|
||||
color in palette &&
|
||||
palette[color as keyof typeof palette] &&
|
||||
"main" in (palette[color as keyof typeof palette] as PaletteColor)
|
||||
) {
|
||||
return (palette[color as keyof typeof palette] as PaletteColor).main;
|
||||
}
|
||||
return palette.primary.main;
|
||||
}, [theme.palette, color]);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: 2,
|
||||
bgcolor: alpha(colorValue, 0.05),
|
||||
border: `1px solid ${alpha(colorValue, 0.15)}`,
|
||||
padding: "8px",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
cursor: onClick ? "pointer" : "default",
|
||||
"&:hover": onClick ? {
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
border: `1px solid ${alpha(colorValue, 0.3)}`,
|
||||
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
|
||||
} : {},
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* 图标容器 */}
|
||||
<Grid
|
||||
component="div"
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mr: 1,
|
||||
ml: "2px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "50%",
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
color: colorValue,
|
||||
borderRadius: 2,
|
||||
bgcolor: alpha(colorValue, 0.05),
|
||||
border: `1px solid ${alpha(colorValue, 0.15)}`,
|
||||
padding: "8px",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
cursor: onClick ? "pointer" : "default",
|
||||
"&:hover": onClick
|
||||
? {
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
border: `1px solid ${alpha(colorValue, 0.3)}`,
|
||||
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
</Grid>
|
||||
|
||||
{/* 文本内容 */}
|
||||
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
{title}
|
||||
</Typography>
|
||||
<Grid component="div" sx={{ display: "flex", alignItems: "baseline" }}>
|
||||
<Typography variant="body1" fontWeight="bold" noWrap sx={{ mr: 0.5 }}>
|
||||
{value}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{unit}
|
||||
</Typography>
|
||||
{/* 图标容器 */}
|
||||
<Grid
|
||||
component="div"
|
||||
sx={{
|
||||
mr: 1,
|
||||
ml: "2px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "50%",
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
color: colorValue,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
|
||||
{/* 文本内容 */}
|
||||
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
{title}
|
||||
</Typography>
|
||||
<Grid
|
||||
component="div"
|
||||
sx={{ display: "flex", alignItems: "baseline" }}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
noWrap
|
||||
sx={{ mr: 0.5 }}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{unit}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// 添加显示名称
|
||||
CompactStatCard.displayName = "CompactStatCard";
|
||||
@@ -205,25 +210,25 @@ export const EnhancedTrafficStats = () => {
|
||||
down: data.down,
|
||||
timestamp: now,
|
||||
});
|
||||
} catch { }
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
lastUpdateRef.current.traffic = now;
|
||||
const safeUp = isNaN(data.up) ? 0 : data.up;
|
||||
const safeDown = isNaN(data.down) ? 0 : data.down;
|
||||
try {
|
||||
setStats(prev => ({
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
traffic: { up: safeUp, down: safeDown }
|
||||
traffic: { up: safeUp, down: safeDown },
|
||||
}));
|
||||
} catch { }
|
||||
} catch {}
|
||||
try {
|
||||
trafficRef.current?.appendData({
|
||||
up: safeUp,
|
||||
down: safeDown,
|
||||
timestamp: now,
|
||||
});
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Traffic] 解析数据错误:", err, event.data);
|
||||
@@ -235,12 +240,12 @@ export const EnhancedTrafficStats = () => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as MemoryUsage;
|
||||
if (data && typeof data.inuse === "number") {
|
||||
setStats(prev => ({
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
memory: {
|
||||
inuse: isNaN(data.inuse) ? 0 : data.inuse,
|
||||
oslimit: data.oslimit,
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -257,7 +262,7 @@ export const EnhancedTrafficStats = () => {
|
||||
|
||||
// 清理现有连接的函数
|
||||
const cleanupSockets = () => {
|
||||
Object.values(socketRefs.current).forEach(socket => {
|
||||
Object.values(socketRefs.current).forEach((socket) => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
@@ -269,40 +274,78 @@ export const EnhancedTrafficStats = () => {
|
||||
cleanupSockets();
|
||||
|
||||
// 创建新连接
|
||||
console.log(`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`);
|
||||
socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, {
|
||||
onmessage: handleTrafficUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event);
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
|
||||
);
|
||||
socketRefs.current.traffic = createAuthSockette(
|
||||
`${server}/traffic`,
|
||||
secret,
|
||||
{
|
||||
onmessage: handleTrafficUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
|
||||
event,
|
||||
);
|
||||
},
|
||||
onerror: (event) => {
|
||||
console.error(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
|
||||
event,
|
||||
);
|
||||
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
},
|
||||
onclose: (event) => {
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
|
||||
event.code,
|
||||
event.reason,
|
||||
);
|
||||
if (event.code !== 1000 && event.code !== 1001) {
|
||||
console.warn(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
|
||||
);
|
||||
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
}
|
||||
},
|
||||
},
|
||||
onerror: (event) => {
|
||||
console.error(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
|
||||
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
},
|
||||
onclose: (event) => {
|
||||
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
|
||||
if (event.code !== 1000 && event.code !== 1001) {
|
||||
console.warn(`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
|
||||
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
}
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
console.log(`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`);
|
||||
console.log(
|
||||
`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`,
|
||||
);
|
||||
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
|
||||
onmessage: handleMemoryUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event);
|
||||
console.log(
|
||||
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
|
||||
event,
|
||||
);
|
||||
},
|
||||
onerror: (event) => {
|
||||
console.error(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
|
||||
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } }));
|
||||
console.error(
|
||||
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
|
||||
event,
|
||||
);
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
memory: { inuse: 0, oslimit: undefined },
|
||||
}));
|
||||
},
|
||||
onclose: (event) => {
|
||||
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
|
||||
console.log(
|
||||
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
|
||||
event.code,
|
||||
event.reason,
|
||||
);
|
||||
if (event.code !== 1000 && event.code !== 1001) {
|
||||
console.warn(`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
|
||||
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } }));
|
||||
console.warn(
|
||||
`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
|
||||
);
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
memory: { inuse: 0, oslimit: undefined },
|
||||
}));
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -314,11 +357,11 @@ export const EnhancedTrafficStats = () => {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
Object.values(socketRefs.current).forEach(socket => {
|
||||
Object.values(socketRefs.current).forEach((socket) => {
|
||||
if (socket) socket.close();
|
||||
});
|
||||
socketRefs.current = { traffic: null, memory: null };
|
||||
} catch { }
|
||||
} catch {}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -339,13 +382,25 @@ export const EnhancedTrafficStats = () => {
|
||||
const [up, upUnit] = parseTraffic(stats.traffic.up);
|
||||
const [down, downUnit] = parseTraffic(stats.traffic.down);
|
||||
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
|
||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal);
|
||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal);
|
||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(
|
||||
connections.uploadTotal,
|
||||
);
|
||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(
|
||||
connections.downloadTotal,
|
||||
);
|
||||
|
||||
return {
|
||||
up, upUnit, down, downUnit, inuse, inuseUnit,
|
||||
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit,
|
||||
connectionsCount: connections.count
|
||||
up,
|
||||
upUnit,
|
||||
down,
|
||||
downUnit,
|
||||
inuse,
|
||||
inuseUnit,
|
||||
uploadTotal,
|
||||
uploadTotalUnit,
|
||||
downloadTotal,
|
||||
downloadTotalUnit,
|
||||
connectionsCount: connections.count,
|
||||
};
|
||||
}, [stats, connections]);
|
||||
|
||||
@@ -392,51 +447,54 @@ export const EnhancedTrafficStats = () => {
|
||||
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
|
||||
|
||||
// 使用useMemo计算统计卡片配置
|
||||
const statCards = useMemo(() => [
|
||||
{
|
||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||
title: t("Upload Speed"),
|
||||
value: parsedData.up,
|
||||
unit: `${parsedData.upUnit}/s`,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <ArrowDownwardRounded fontSize="small" />,
|
||||
title: t("Download Speed"),
|
||||
value: parsedData.down,
|
||||
unit: `${parsedData.downUnit}/s`,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <LinkRounded fontSize="small" />,
|
||||
title: t("Active Connections"),
|
||||
value: parsedData.connectionsCount,
|
||||
unit: "",
|
||||
color: "success" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudUploadRounded fontSize="small" />,
|
||||
title: t("Uploaded"),
|
||||
value: parsedData.uploadTotal,
|
||||
unit: parsedData.uploadTotalUnit,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudDownloadRounded fontSize="small" />,
|
||||
title: t("Downloaded"),
|
||||
value: parsedData.downloadTotal,
|
||||
unit: parsedData.downloadTotalUnit,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <MemoryRounded fontSize="small" />,
|
||||
title: t("Memory Usage"),
|
||||
value: parsedData.inuse,
|
||||
unit: parsedData.inuseUnit,
|
||||
color: "error" as const,
|
||||
onClick: isDebug ? handleGarbageCollection : undefined,
|
||||
},
|
||||
], [t, parsedData, isDebug, handleGarbageCollection]);
|
||||
const statCards = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||
title: t("Upload Speed"),
|
||||
value: parsedData.up,
|
||||
unit: `${parsedData.upUnit}/s`,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <ArrowDownwardRounded fontSize="small" />,
|
||||
title: t("Download Speed"),
|
||||
value: parsedData.down,
|
||||
unit: `${parsedData.downUnit}/s`,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <LinkRounded fontSize="small" />,
|
||||
title: t("Active Connections"),
|
||||
value: parsedData.connectionsCount,
|
||||
unit: "",
|
||||
color: "success" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudUploadRounded fontSize="small" />,
|
||||
title: t("Uploaded"),
|
||||
value: parsedData.uploadTotal,
|
||||
unit: parsedData.uploadTotalUnit,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudDownloadRounded fontSize="small" />,
|
||||
title: t("Downloaded"),
|
||||
value: parsedData.downloadTotal,
|
||||
unit: parsedData.downloadTotalUnit,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <MemoryRounded fontSize="small" />,
|
||||
title: t("Memory Usage"),
|
||||
value: parsedData.inuse,
|
||||
unit: parsedData.inuseUnit,
|
||||
color: "error" as const,
|
||||
onClick: isDebug ? handleGarbageCollection : undefined,
|
||||
},
|
||||
],
|
||||
[t, parsedData, isDebug, handleGarbageCollection],
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||
|
||||
@@ -78,12 +78,16 @@ const truncateStyle = {
|
||||
maxWidth: "calc(100% - 28px)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
// 提取独立组件减少主组件复杂度
|
||||
const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
current: ProfileItem;
|
||||
const ProfileDetails = ({
|
||||
current,
|
||||
onUpdateProfile,
|
||||
updating,
|
||||
}: {
|
||||
current: ProfileItem;
|
||||
onUpdateProfile: () => void;
|
||||
updating: boolean;
|
||||
}) => {
|
||||
@@ -99,7 +103,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
if (!current.extra || !current.extra.total) return 1;
|
||||
return Math.min(
|
||||
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
|
||||
100
|
||||
100,
|
||||
);
|
||||
}, [current.extra, usedTraffic]);
|
||||
|
||||
@@ -109,19 +113,24 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
{current.url && (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<DnsOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary" noWrap sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
|
||||
{current.home ? (
|
||||
<Link
|
||||
component="button"
|
||||
fontWeight="medium"
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
sx={{
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
minWidth: 0,
|
||||
maxWidth: "calc(100% - 40px)",
|
||||
ml: 0.5
|
||||
ml: 0.5,
|
||||
}}
|
||||
title={parseUrl(current.url)}
|
||||
>
|
||||
@@ -132,14 +141,19 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
minWidth: 0,
|
||||
flex: 1
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{parseUrl(current.url)}
|
||||
</Typography>
|
||||
<LaunchOutlined
|
||||
fontSize="inherit"
|
||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }}
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
fontSize: "0.8rem",
|
||||
opacity: 0.7,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
@@ -152,7 +166,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
whiteSpace: "nowrap",
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
ml: 0.5
|
||||
ml: 0.5,
|
||||
}}
|
||||
title={parseUrl(current.url)}
|
||||
>
|
||||
@@ -195,7 +209,8 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Used / Total")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)}
|
||||
{parseTraffic(usedTraffic)} /{" "}
|
||||
{parseTraffic(current.extra.total)}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -240,7 +255,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
// 提取空配置组件
|
||||
const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -268,27 +283,30 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
|
||||
export const HomeProfileCard = ({
|
||||
current,
|
||||
onProfileUpdated,
|
||||
}: HomeProfileCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { refreshAll } = useAppData();
|
||||
|
||||
// 更新当前订阅
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
|
||||
const onUpdateProfile = useLockFn(async () => {
|
||||
if (!current?.uid) return;
|
||||
|
||||
setUpdating(true);
|
||||
try {
|
||||
await updateProfile(current.uid, current.option);
|
||||
showNotice('success', t("Update subscription successfully"), 1000);
|
||||
showNotice("success", t("Update subscription successfully"), 1000);
|
||||
onProfileUpdated?.();
|
||||
|
||||
|
||||
// 刷新首页数据
|
||||
refreshAll();
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString(), 3000);
|
||||
showNotice("error", err.message || err.toString(), 3000);
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
@@ -302,9 +320,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
||||
// 卡片标题
|
||||
const cardTitle = useMemo(() => {
|
||||
if (!current) return t("Profiles");
|
||||
|
||||
|
||||
if (!current.home) return current.name;
|
||||
|
||||
|
||||
return (
|
||||
<Link
|
||||
component="button"
|
||||
@@ -323,19 +341,19 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
flex: 1
|
||||
}
|
||||
flex: 1,
|
||||
},
|
||||
}}
|
||||
title={current.name}
|
||||
>
|
||||
<span>{current.name}</span>
|
||||
<LaunchOutlined
|
||||
fontSize="inherit"
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
fontSize: "0.8rem",
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
fontSize: "0.8rem",
|
||||
opacity: 0.7,
|
||||
flexShrink: 0
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
@@ -345,7 +363,7 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
||||
// 卡片操作按钮
|
||||
const cardAction = useMemo(() => {
|
||||
if (!current) return null;
|
||||
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -367,10 +385,10 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
||||
action={cardAction}
|
||||
>
|
||||
{current ? (
|
||||
<ProfileDetails
|
||||
current={current}
|
||||
onUpdateProfile={onUpdateProfile}
|
||||
updating={updating}
|
||||
<ProfileDetails
|
||||
current={current}
|
||||
onUpdateProfile={onUpdateProfile}
|
||||
updating={updating}
|
||||
/>
|
||||
) : (
|
||||
<EmptyProfile onClick={goToProfiles} />
|
||||
|
||||
@@ -83,28 +83,28 @@ export const IpInfoCard = () => {
|
||||
// 组件加载时获取IP信息
|
||||
useEffect(() => {
|
||||
fetchIpInfo();
|
||||
|
||||
|
||||
// 倒计时实现优化,减少不必要的重渲染
|
||||
let timer: number | null = null;
|
||||
let currentCount = IP_REFRESH_SECONDS;
|
||||
|
||||
|
||||
// 只在必要时更新状态,减少重渲染次数
|
||||
const startCountdown = () => {
|
||||
timer = window.setInterval(() => {
|
||||
currentCount -= 1;
|
||||
|
||||
|
||||
if (currentCount <= 0) {
|
||||
fetchIpInfo();
|
||||
currentCount = IP_REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
|
||||
// 每5秒或倒计时结束时才更新UI
|
||||
if (currentCount % 5 === 0 || currentCount <= 0) {
|
||||
setCountdown(currentCount);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
|
||||
startCountdown();
|
||||
return () => {
|
||||
if (timer) clearInterval(timer);
|
||||
@@ -112,7 +112,7 @@ export const IpInfoCard = () => {
|
||||
}, [fetchIpInfo]);
|
||||
|
||||
const toggleShowIp = useCallback(() => {
|
||||
setShowIp(prev => !prev);
|
||||
setShowIp((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// 渲染加载状态
|
||||
@@ -282,9 +282,7 @@ export const IpInfoCard = () => {
|
||||
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
|
||||
<InfoItem
|
||||
label={t("Location")}
|
||||
value={[ipInfo?.city, ipInfo?.region]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
|
||||
/>
|
||||
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
|
||||
</Box>
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography, Stack, Divider, Chip, IconButton, Tooltip } from "@mui/material";
|
||||
import {
|
||||
InfoOutlined,
|
||||
SettingsOutlined,
|
||||
WarningOutlined,
|
||||
import {
|
||||
Typography,
|
||||
Stack,
|
||||
Divider,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
InfoOutlined,
|
||||
SettingsOutlined,
|
||||
WarningOutlined,
|
||||
AdminPanelSettingsOutlined,
|
||||
DnsOutlined,
|
||||
ExtensionOutlined
|
||||
ExtensionOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
getSystemInfo,
|
||||
} from "@/services/cmds";
|
||||
import { getSystemInfo } from "@/services/cmds";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { version as appVersion } from "@root/package.json";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@@ -30,32 +35,35 @@ export const SystemInfoCard = () => {
|
||||
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
|
||||
const { installServiceAndRestartCore } = useServiceInstaller();
|
||||
|
||||
// 系统信息状态
|
||||
const [systemState, setSystemState] = useState({
|
||||
osInfo: "",
|
||||
lastCheckUpdate: "-",
|
||||
});
|
||||
// 系统信息状态
|
||||
const [systemState, setSystemState] = useState({
|
||||
osInfo: "",
|
||||
lastCheckUpdate: "-",
|
||||
});
|
||||
|
||||
// 初始化系统信息
|
||||
useEffect(() => {
|
||||
getSystemInfo()
|
||||
.then((info) => {
|
||||
const lines = info.split("\n");
|
||||
if (lines.length > 0) {
|
||||
const sysName = lines[0].split(": ")[1] || "";
|
||||
let sysVersion = lines[1].split(": ")[1] || "";
|
||||
// 初始化系统信息
|
||||
useEffect(() => {
|
||||
getSystemInfo()
|
||||
.then((info) => {
|
||||
const lines = info.split("\n");
|
||||
if (lines.length > 0) {
|
||||
const sysName = lines[0].split(": ")[1] || "";
|
||||
let sysVersion = lines[1].split(": ")[1] || "";
|
||||
|
||||
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) {
|
||||
sysVersion = sysVersion.substring(sysName.length).trim();
|
||||
if (
|
||||
sysName &&
|
||||
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
|
||||
) {
|
||||
sysVersion = sysVersion.substring(sysName.length).trim();
|
||||
}
|
||||
|
||||
setSystemState((prev) => ({
|
||||
...prev,
|
||||
osInfo: `${sysName} ${sysVersion}`,
|
||||
}));
|
||||
}
|
||||
|
||||
setSystemState((prev) => ({
|
||||
...prev,
|
||||
osInfo: `${sysName} ${sysVersion}`,
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
// 获取最后检查更新时间
|
||||
const lastCheck = localStorage.getItem("last_check_update");
|
||||
@@ -122,7 +130,6 @@ useEffect(() => {
|
||||
}
|
||||
}, [verge, patchVerge]);
|
||||
|
||||
|
||||
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
|
||||
const handleRunningModeClick = useCallback(() => {
|
||||
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
|
||||
@@ -135,13 +142,13 @@ useEffect(() => {
|
||||
try {
|
||||
const info = await checkUpdate();
|
||||
if (!info?.available) {
|
||||
showNotice('success', t("Currently on the Latest Version"));
|
||||
showNotice("success", t("Currently on the Latest Version"));
|
||||
} else {
|
||||
showNotice('info', t("Update Available"), 2000);
|
||||
showNotice("info", t("Update Available"), 2000);
|
||||
goToSettings();
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString());
|
||||
showNotice("error", err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -155,13 +162,15 @@ useEffect(() => {
|
||||
const runningModeStyle = useMemo(
|
||||
() => ({
|
||||
// Sidecar或纯管理员模式允许安装服务
|
||||
cursor: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "pointer" : "default",
|
||||
textDecoration: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "underline" : "none",
|
||||
cursor:
|
||||
isSidecarMode || (isAdminMode && isSidecarMode) ? "pointer" : "default",
|
||||
textDecoration:
|
||||
isSidecarMode || (isAdminMode && isSidecarMode) ? "underline" : "none",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
"&:hover": {
|
||||
opacity: (isSidecarMode || (isAdminMode && isSidecarMode)) ? 0.7 : 1,
|
||||
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
|
||||
},
|
||||
}),
|
||||
[isSidecarMode, isAdminMode],
|
||||
@@ -174,34 +183,34 @@ useEffect(() => {
|
||||
if (!isSidecarMode) {
|
||||
return (
|
||||
<>
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
titleAccess={t("Administrator Mode")}
|
||||
/>
|
||||
<DnsOutlined
|
||||
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
|
||||
<DnsOutlined
|
||||
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
|
||||
titleAccess={t("Service Mode")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
titleAccess={t("Administrator Mode")}
|
||||
/>
|
||||
);
|
||||
} else if (isSidecarMode) {
|
||||
return (
|
||||
<ExtensionOutlined
|
||||
sx={{ color: "info.main", fontSize: 16 }}
|
||||
<ExtensionOutlined
|
||||
sx={{ color: "info.main", fontSize: 16 }}
|
||||
titleAccess={t("Sidecar Mode")}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DnsOutlined
|
||||
sx={{ color: "success.main", fontSize: 16 }}
|
||||
<DnsOutlined
|
||||
sx={{ color: "success.main", fontSize: 16 }}
|
||||
titleAccess={t("Service Mode")}
|
||||
/>
|
||||
);
|
||||
@@ -247,13 +256,19 @@ useEffect(() => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Auto Launch")}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
{isAdminMode && (
|
||||
<Tooltip title={t("Administrator mode may not support auto launch")}>
|
||||
<Tooltip
|
||||
title={t("Administrator mode may not support auto launch")}
|
||||
>
|
||||
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -268,7 +283,11 @@ useEffect(() => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Running Mode")}
|
||||
</Typography>
|
||||
|
||||
@@ -87,12 +87,12 @@ export const TestCard = () => {
|
||||
}
|
||||
|
||||
const newList = testList.map((x) =>
|
||||
x.uid === uid ? { ...x, ...patch } : x
|
||||
x.uid === uid ? { ...x, ...patch } : x,
|
||||
);
|
||||
|
||||
mutateVerge({ ...verge, test_list: newList }, false);
|
||||
},
|
||||
[testList, verge, mutateVerge]
|
||||
[testList, verge, mutateVerge],
|
||||
);
|
||||
|
||||
const onDeleteTestListItem = useCallback(
|
||||
@@ -101,7 +101,7 @@ export const TestCard = () => {
|
||||
patchVerge({ test_list: newList });
|
||||
mutateVerge({ ...verge, test_list: newList }, false);
|
||||
},
|
||||
[testList, verge, patchVerge, mutateVerge]
|
||||
[testList, verge, patchVerge, mutateVerge],
|
||||
);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
@@ -122,7 +122,7 @@ export const TestCard = () => {
|
||||
const patchFn = () => {
|
||||
try {
|
||||
patchVerge({ test_list: newList });
|
||||
} catch { }
|
||||
} catch {}
|
||||
};
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(patchFn);
|
||||
@@ -131,7 +131,7 @@ export const TestCard = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[testList, verge, mutateVerge, patchVerge]
|
||||
[testList, verge, mutateVerge, patchVerge],
|
||||
);
|
||||
|
||||
// 仅在verge首次加载时初始化测试列表
|
||||
@@ -142,22 +142,25 @@ export const TestCard = () => {
|
||||
}, [verge, patchVerge]);
|
||||
|
||||
// 使用useMemo优化UI内容,减少渲染计算
|
||||
const renderTestItems = useMemo(() => (
|
||||
<Grid container spacing={1} columns={12}>
|
||||
<SortableContext items={testList.map((x) => x.uid)}>
|
||||
{testList.map((item) => (
|
||||
<Grid key={item.uid} size={3}>
|
||||
<TestItem
|
||||
id={item.uid}
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onDelete={onDeleteTestListItem}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Grid>
|
||||
), [testList, onDeleteTestListItem]);
|
||||
const renderTestItems = useMemo(
|
||||
() => (
|
||||
<Grid container spacing={1} columns={12}>
|
||||
<SortableContext items={testList.map((x) => x.uid)}>
|
||||
{testList.map((item) => (
|
||||
<Grid key={item.uid} size={3}>
|
||||
<TestItem
|
||||
id={item.uid}
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onDelete={onDeleteTestListItem}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Grid>
|
||||
),
|
||||
[testList, onDeleteTestListItem],
|
||||
);
|
||||
|
||||
const handleTestAll = useCallback(() => {
|
||||
emit("verge://test-all");
|
||||
|
||||
@@ -24,7 +24,7 @@ export const UpdateButton = (props: Props) => {
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!updateInfo?.available) return null;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material";
|
||||
import { getCurrentWebviewWindow, WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import {
|
||||
getCurrentWebviewWindow,
|
||||
WebviewWindow,
|
||||
} from "@tauri-apps/api/webviewWindow";
|
||||
import { useSetThemeMode, useThemeMode } from "@/services/states";
|
||||
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
@@ -49,16 +52,16 @@ export const useCustomTheme = () => {
|
||||
|
||||
let isMounted = true;
|
||||
|
||||
const timerId = setTimeout(() => {
|
||||
if (!isMounted) return;
|
||||
appWindow.theme().then((systemTheme) => {
|
||||
appWindow
|
||||
.theme()
|
||||
.then((systemTheme) => {
|
||||
if (isMounted && systemTheme) {
|
||||
setMode(systemTheme);
|
||||
}
|
||||
}).catch(err => {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to get initial system theme:", err);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
|
||||
if (isMounted) {
|
||||
@@ -68,14 +71,15 @@ export const useCustomTheme = () => {
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
clearTimeout(timerId);
|
||||
unlistenPromise.then((unlistenFn) => {
|
||||
if (typeof unlistenFn === 'function') {
|
||||
unlistenFn();
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error("Failed to unlisten from theme changes:", err);
|
||||
});
|
||||
unlistenPromise
|
||||
.then((unlistenFn) => {
|
||||
if (typeof unlistenFn === "function") {
|
||||
unlistenFn();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to unlisten from theme changes:", err);
|
||||
});
|
||||
};
|
||||
}, [theme_mode, appWindow, setMode]);
|
||||
|
||||
@@ -86,7 +90,10 @@ export const useCustomTheme = () => {
|
||||
|
||||
if (theme_mode === "system") {
|
||||
appWindow.setTheme(null).catch((err) => {
|
||||
console.error("Failed to set window theme to follow system (setTheme(null)):", err);
|
||||
console.error(
|
||||
"Failed to set window theme to follow system (setTheme(null)):",
|
||||
err,
|
||||
);
|
||||
});
|
||||
} else if (mode) {
|
||||
appWindow.setTheme(mode as TauriOsTheme).catch((err) => {
|
||||
@@ -120,6 +127,7 @@ export const useCustomTheme = () => {
|
||||
},
|
||||
background: {
|
||||
paper: dt.background_color,
|
||||
default: dt.background_color,
|
||||
},
|
||||
},
|
||||
shadows: Array(25).fill("none") as Shadows,
|
||||
@@ -146,6 +154,10 @@ export const useCustomTheme = () => {
|
||||
warning: { main: dt.warning_color },
|
||||
success: { main: dt.success_color },
|
||||
text: { primary: dt.primary_text, secondary: dt.secondary_text },
|
||||
background: {
|
||||
paper: dt.background_color,
|
||||
default: dt.background_color,
|
||||
},
|
||||
},
|
||||
typography: { fontFamily: dt.font_family },
|
||||
});
|
||||
@@ -153,31 +165,87 @@ export const useCustomTheme = () => {
|
||||
|
||||
const rootEle = document.documentElement;
|
||||
if (rootEle) {
|
||||
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
|
||||
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
|
||||
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
|
||||
const dividerColor =
|
||||
const backgroundColor =
|
||||
mode === "light" ? "#ECECEC" : dt.background_color;
|
||||
const selectColor = mode === "light" ? "#f5f5f5" : "#3E3E3E";
|
||||
const scrollColor = mode === "light" ? "#90939980" : "#555555";
|
||||
const dividerColor =
|
||||
mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";
|
||||
|
||||
rootEle.style.setProperty("--divider-color", dividerColor);
|
||||
rootEle.style.setProperty("--background-color", backgroundColor);
|
||||
rootEle.style.setProperty("--selection-color", selectColor);
|
||||
rootEle.style.setProperty("--scroller-color", scrollColor);
|
||||
rootEle.style.setProperty("--primary-main", muiTheme.palette.primary.main);
|
||||
rootEle.style.setProperty(
|
||||
rootEle.style.setProperty("--divider-color", dividerColor);
|
||||
rootEle.style.setProperty("--background-color", backgroundColor);
|
||||
rootEle.style.setProperty("--selection-color", selectColor);
|
||||
rootEle.style.setProperty("--scroller-color", scrollColor);
|
||||
rootEle.style.setProperty(
|
||||
"--primary-main",
|
||||
muiTheme.palette.primary.main,
|
||||
);
|
||||
rootEle.style.setProperty(
|
||||
"--background-color-alpha",
|
||||
alpha(muiTheme.palette.primary.main, 0.1),
|
||||
);
|
||||
);
|
||||
// 添加CSS变量
|
||||
rootEle.style.setProperty(
|
||||
"--window-border-color",
|
||||
mode === "light" ? "#cccccc" : "#1E1E1E",
|
||||
);
|
||||
rootEle.style.setProperty(
|
||||
"--scrollbar-bg",
|
||||
mode === "light" ? "#f1f1f1" : "#2E303D",
|
||||
);
|
||||
rootEle.style.setProperty(
|
||||
"--scrollbar-thumb",
|
||||
mode === "light" ? "#c1c1c1" : "#555555",
|
||||
);
|
||||
}
|
||||
// inject css
|
||||
|
||||
let styleElement = document.querySelector("style#verge-theme");
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement("style");
|
||||
styleElement.id = "verge-theme";
|
||||
document.head.appendChild(styleElement!);
|
||||
}
|
||||
|
||||
if (styleElement) {
|
||||
styleElement.innerHTML = setting.css_injection || "";
|
||||
// 添加全局样式,确保所有元素都使用暗色主题
|
||||
const globalStyles = `
|
||||
/* 修复滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--scrollbar-bg);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scrollbar-thumb);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: ${mode === "light" ? "#a1a1a1" : "#666666"};
|
||||
}
|
||||
|
||||
/* 确保所有元素都使用正确的背景色 */
|
||||
body, html {
|
||||
background-color: var(--background-color) !important;
|
||||
}
|
||||
|
||||
/* 修复可能的白色边框 */
|
||||
.MuiPaper-root {
|
||||
border-color: var(--window-border-color) !important;
|
||||
}
|
||||
|
||||
/* 确保模态框和对话框也使用暗色主题 */
|
||||
.MuiDialog-paper {
|
||||
background-color: ${mode === "light" ? "#ffffff" : "#2E303D"} !important;
|
||||
}
|
||||
|
||||
/* 移除可能的白色点或线条 */
|
||||
* {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
styleElement.innerHTML = (setting.css_injection || "") + globalStyles;
|
||||
}
|
||||
|
||||
const { palette } = muiTheme;
|
||||
|
||||
@@ -127,7 +127,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
currData.current = value;
|
||||
onChange?.(prevData.current, currData.current);
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString());
|
||||
showNotice("error", err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
!readOnly && onSave?.(prevData.current, currData.current);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString());
|
||||
showNotice("error", err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -144,7 +144,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
try {
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString());
|
||||
showNotice("error", err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ export const GroupItem = (props: Props) => {
|
||||
? alpha(palette.background.paper, 0.3)
|
||||
: alpha(palette.grey[400], 0.3)
|
||||
: type === "delete"
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
height: "100%",
|
||||
margin: "8px 0",
|
||||
borderRadius: "8px",
|
||||
|
||||
@@ -90,27 +90,27 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
|
||||
const filteredPrependSeq = useMemo(
|
||||
() => prependSeq.filter((group) => match(group.name)),
|
||||
[prependSeq, match]
|
||||
[prependSeq, match],
|
||||
);
|
||||
const filteredGroupList = useMemo(
|
||||
() => groupList.filter((group) => match(group.name)),
|
||||
[groupList, match]
|
||||
[groupList, match],
|
||||
);
|
||||
const filteredAppendSeq = useMemo(
|
||||
() => appendSeq.filter((group) => match(group.name)),
|
||||
[appendSeq, match]
|
||||
[appendSeq, match],
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
}),
|
||||
);
|
||||
const reorder = (
|
||||
list: IProxyGroupConfig[],
|
||||
startIndex: number,
|
||||
endIndex: number
|
||||
endIndex: number,
|
||||
) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
@@ -188,8 +188,8 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
setCurrData(
|
||||
yaml.dump(
|
||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||
{ forceQuotes: true }
|
||||
)
|
||||
{ forceQuotes: true },
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// 防止异常导致UI卡死
|
||||
@@ -226,7 +226,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
return !moreDeleteProxies.includes(proxy);
|
||||
}
|
||||
}),
|
||||
moreAppendProxies
|
||||
moreAppendProxies,
|
||||
);
|
||||
|
||||
setProxyPolicyList(
|
||||
@@ -236,8 +236,8 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
.map((group: IProxyGroupConfig) => group.name)
|
||||
.filter((name) => !deleteSeq.includes(name)) || [],
|
||||
appendSeq.map((group: IProxyGroupConfig) => group.name),
|
||||
proxies.map((proxy: any) => proxy.name)
|
||||
)
|
||||
proxies.map((proxy: any) => proxy.name),
|
||||
),
|
||||
);
|
||||
};
|
||||
const fetchProfile = async () => {
|
||||
@@ -266,7 +266,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
{},
|
||||
originProvider,
|
||||
moreProvider,
|
||||
globalProvider
|
||||
globalProvider,
|
||||
);
|
||||
|
||||
setProxyProviderList(Object.keys(provider));
|
||||
@@ -297,11 +297,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
await saveProfileFile(property, currData);
|
||||
showNotice('success', t("Saved Successfully"));
|
||||
showNotice("success", t("Saved Successfully"));
|
||||
onSave?.(prevData, currData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.toString());
|
||||
showNotice("error", err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -502,7 +502,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
{t("seconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
@@ -530,7 +530,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
{t("millis")}
|
||||
</InputAdornment>
|
||||
),
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
@@ -742,7 +742,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}
|
||||
setPrependSeq([formIns.getValues(), ...prependSeq]);
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString());
|
||||
showNotice("error", err.message || err.toString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -764,7 +764,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}
|
||||
setAppendSeq([...appendSeq, formIns.getValues()]);
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString());
|
||||
showNotice("error", err.message || err.toString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -811,8 +811,8 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
onDelete={() => {
|
||||
setPrependSeq(
|
||||
prependSeq.filter(
|
||||
(v) => v.name !== item.name
|
||||
)
|
||||
(v) => v.name !== item.name,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -838,8 +838,8 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredGroupList[newIndex].name
|
||||
)
|
||||
(v) => v !== filteredGroupList[newIndex].name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
@@ -871,8 +871,8 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
onDelete={() => {
|
||||
setAppendSeq(
|
||||
appendSeq.filter(
|
||||
(v) => v.name !== item.name
|
||||
)
|
||||
(v) => v.name !== item.name,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -906,8 +906,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
padding: {
|
||||
top: 33, // 顶部padding防止遮挡snippets
|
||||
},
|
||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||
}`,
|
||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
||||
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||
}`,
|
||||
fontLigatures: false, // 连字符
|
||||
smoothScrolling: true, // 平滑滚动
|
||||
}}
|
||||
|
||||
@@ -1,57 +1,58 @@
|
||||
import { alpha, Box, styled } from "@mui/material";
|
||||
|
||||
export const ProfileBox = styled(Box)(
|
||||
({ theme, "aria-selected": selected }) => {
|
||||
const { mode, primary, text } = theme.palette;
|
||||
const key = `${mode}-${!!selected}`;
|
||||
export const ProfileBox = styled(Box)(({
|
||||
theme,
|
||||
"aria-selected": selected,
|
||||
}) => {
|
||||
const { mode, primary, text } = theme.palette;
|
||||
const key = `${mode}-${!!selected}`;
|
||||
|
||||
const backgroundColor = mode === "light" ? "#ffffff" : "#282A36";
|
||||
const backgroundColor = mode === "light" ? "#ffffff" : "#282A36";
|
||||
|
||||
const color = {
|
||||
"light-true": text.secondary,
|
||||
"light-false": text.secondary,
|
||||
"dark-true": alpha(text.secondary, 0.65),
|
||||
"dark-false": alpha(text.secondary, 0.65),
|
||||
}[key]!;
|
||||
const color = {
|
||||
"light-true": text.secondary,
|
||||
"light-false": text.secondary,
|
||||
"dark-true": alpha(text.secondary, 0.65),
|
||||
"dark-false": alpha(text.secondary, 0.65),
|
||||
}[key]!;
|
||||
|
||||
const h2color = {
|
||||
"light-true": primary.main,
|
||||
"light-false": text.primary,
|
||||
"dark-true": primary.main,
|
||||
"dark-false": text.primary,
|
||||
}[key]!;
|
||||
const h2color = {
|
||||
"light-true": primary.main,
|
||||
"light-false": text.primary,
|
||||
"dark-true": primary.main,
|
||||
"dark-false": text.primary,
|
||||
}[key]!;
|
||||
|
||||
const borderSelect = {
|
||||
"light-true": {
|
||||
borderLeft: `3px solid ${primary.main}`,
|
||||
width: `calc(100% + 3px)`,
|
||||
marginLeft: `-3px`,
|
||||
},
|
||||
"light-false": {
|
||||
width: "100%",
|
||||
},
|
||||
"dark-true": {
|
||||
borderLeft: `3px solid ${primary.main}`,
|
||||
width: `calc(100% + 3px)`,
|
||||
marginLeft: `-3px`,
|
||||
},
|
||||
"dark-false": {
|
||||
width: "100%",
|
||||
},
|
||||
}[key];
|
||||
const borderSelect = {
|
||||
"light-true": {
|
||||
borderLeft: `3px solid ${primary.main}`,
|
||||
width: `calc(100% + 3px)`,
|
||||
marginLeft: `-3px`,
|
||||
},
|
||||
"light-false": {
|
||||
width: "100%",
|
||||
},
|
||||
"dark-true": {
|
||||
borderLeft: `3px solid ${primary.main}`,
|
||||
width: `calc(100% + 3px)`,
|
||||
marginLeft: `-3px`,
|
||||
},
|
||||
"dark-false": {
|
||||
width: "100%",
|
||||
},
|
||||
}[key];
|
||||
|
||||
return {
|
||||
position: "relative",
|
||||
display: "block",
|
||||
cursor: "pointer",
|
||||
textAlign: "left",
|
||||
padding: "8px 16px",
|
||||
boxSizing: "border-box",
|
||||
backgroundColor,
|
||||
...borderSelect,
|
||||
borderRadius: "8px",
|
||||
color,
|
||||
"& h2": { color: h2color },
|
||||
};
|
||||
}
|
||||
);
|
||||
return {
|
||||
position: "relative",
|
||||
display: "block",
|
||||
cursor: "pointer",
|
||||
textAlign: "left",
|
||||
padding: "8px 16px",
|
||||
boxSizing: "border-box",
|
||||
backgroundColor,
|
||||
...borderSelect,
|
||||
borderRadius: "8px",
|
||||
color,
|
||||
"& h2": { color: h2color },
|
||||
};
|
||||
});
|
||||
|
||||
@@ -75,7 +75,10 @@ export const ProfileItem = (props: Props) => {
|
||||
|
||||
// 获取下次更新时间的函数
|
||||
const fetchNextUpdateTime = useLockFn(async (forceRefresh = false) => {
|
||||
if (itemData.option?.update_interval && itemData.option.update_interval > 0) {
|
||||
if (
|
||||
itemData.option?.update_interval &&
|
||||
itemData.option.update_interval > 0
|
||||
) {
|
||||
try {
|
||||
console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`);
|
||||
|
||||
@@ -97,7 +100,7 @@ export const ProfileItem = (props: Props) => {
|
||||
setNextUpdateTime(t("Last Update failed"));
|
||||
} else {
|
||||
// 否则显示剩余时间
|
||||
const diffMinutes = nextUpdateDate.diff(now, 'minute');
|
||||
const diffMinutes = nextUpdateDate.diff(now, "minute");
|
||||
|
||||
if (diffMinutes < 60) {
|
||||
if (diffMinutes <= 0) {
|
||||
@@ -159,11 +162,17 @@ export const ProfileItem = (props: Props) => {
|
||||
};
|
||||
|
||||
// 只注册定时器更新事件监听
|
||||
window.addEventListener('verge://timer-updated', handleTimerUpdate as EventListener);
|
||||
window.addEventListener(
|
||||
"verge://timer-updated",
|
||||
handleTimerUpdate as EventListener,
|
||||
);
|
||||
|
||||
return () => {
|
||||
// 清理事件监听
|
||||
window.removeEventListener('verge://timer-updated', handleTimerUpdate as EventListener);
|
||||
window.removeEventListener(
|
||||
"verge://timer-updated",
|
||||
handleTimerUpdate as EventListener,
|
||||
);
|
||||
};
|
||||
}, [showNextUpdate, itemData.uid]);
|
||||
|
||||
@@ -271,7 +280,7 @@ export const ProfileItem = (props: Props) => {
|
||||
try {
|
||||
await viewProfile(itemData.uid);
|
||||
} catch (err: any) {
|
||||
showNotice('error', err?.message || err.toString());
|
||||
showNotice("error", err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -302,7 +311,7 @@ export const ProfileItem = (props: Props) => {
|
||||
await updateProfile(itemData.uid, option);
|
||||
|
||||
// 更新成功,刷新列表
|
||||
showNotice('success', t("Update subscription successfully"));
|
||||
showNotice("success", t("Update subscription successfully"));
|
||||
mutate("getProfiles");
|
||||
} catch (err: any) {
|
||||
// 更新完全失败(包括后端的回退尝试)
|
||||
@@ -421,13 +430,25 @@ export const ProfileItem = (props: Props) => {
|
||||
};
|
||||
|
||||
// 注册事件监听
|
||||
window.addEventListener('profile-update-started', handleUpdateStarted as EventListener);
|
||||
window.addEventListener('profile-update-completed', handleUpdateCompleted as EventListener);
|
||||
window.addEventListener(
|
||||
"profile-update-started",
|
||||
handleUpdateStarted as EventListener,
|
||||
);
|
||||
window.addEventListener(
|
||||
"profile-update-completed",
|
||||
handleUpdateCompleted as EventListener,
|
||||
);
|
||||
|
||||
return () => {
|
||||
// 清理事件监听
|
||||
window.removeEventListener('profile-update-started', handleUpdateStarted as EventListener);
|
||||
window.removeEventListener('profile-update-completed', handleUpdateCompleted as EventListener);
|
||||
window.removeEventListener(
|
||||
"profile-update-started",
|
||||
handleUpdateStarted as EventListener,
|
||||
);
|
||||
window.removeEventListener(
|
||||
"profile-update-completed",
|
||||
handleUpdateCompleted as EventListener,
|
||||
);
|
||||
};
|
||||
}, [itemData.uid, showNextUpdate]);
|
||||
|
||||
@@ -541,13 +562,23 @@ export const ProfileItem = (props: Props) => {
|
||||
)
|
||||
)}
|
||||
{hasUrl && (
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", ml: "auto" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
ml: "auto",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
noWrap
|
||||
component="span"
|
||||
fontSize={14}
|
||||
textAlign="right"
|
||||
title={showNextUpdate ? t("Click to show last update time") : `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`}
|
||||
title={
|
||||
showNextUpdate
|
||||
? t("Click to show last update time")
|
||||
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
|
||||
}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
display: "inline-block",
|
||||
@@ -556,13 +587,15 @@ export const ProfileItem = (props: Props) => {
|
||||
"&:hover": {
|
||||
borderBottomColor: "primary.main",
|
||||
color: "primary.main",
|
||||
}
|
||||
},
|
||||
}}
|
||||
onClick={toggleUpdateTimeDisplay}
|
||||
>
|
||||
{showNextUpdate
|
||||
? nextUpdateTime
|
||||
: (updated > 0 ? dayjs(updated * 1000).fromNow() : "")}
|
||||
: updated > 0
|
||||
? dayjs(updated * 1000).fromNow()
|
||||
: ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const ProfileMore = (props: Props) => {
|
||||
try {
|
||||
await viewProfile(id);
|
||||
} catch (err: any) {
|
||||
showNotice('error', err?.message || err.toString());
|
||||
showNotice("error", err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
||||
setOpen(false);
|
||||
fileDataRef.current = null;
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
} catch { }
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const text = {
|
||||
|
||||
@@ -66,27 +66,27 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
|
||||
const filteredPrependSeq = useMemo(
|
||||
() => prependSeq.filter((proxy) => match(proxy.name)),
|
||||
[prependSeq, match]
|
||||
[prependSeq, match],
|
||||
);
|
||||
const filteredProxyList = useMemo(
|
||||
() => proxyList.filter((proxy) => match(proxy.name)),
|
||||
[proxyList, match]
|
||||
[proxyList, match],
|
||||
);
|
||||
const filteredAppendSeq = useMemo(
|
||||
() => appendSeq.filter((proxy) => match(proxy.name)),
|
||||
[appendSeq, match]
|
||||
[appendSeq, match],
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
}),
|
||||
);
|
||||
const reorder = (
|
||||
list: IProxyConfig[],
|
||||
startIndex: number,
|
||||
endIndex: number
|
||||
endIndex: number,
|
||||
) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
@@ -208,8 +208,8 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
setCurrData(
|
||||
yaml.dump(
|
||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||
{ forceQuotes: true }
|
||||
)
|
||||
{ forceQuotes: true },
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// 防止异常导致UI卡死
|
||||
@@ -232,11 +232,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
await saveProfileFile(property, currData);
|
||||
showNotice('success', t("Saved Successfully"));
|
||||
showNotice("success", t("Saved Successfully"));
|
||||
onSave?.(prevData, currData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.toString());
|
||||
showNotice("error", err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -358,8 +358,8 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
onDelete={() => {
|
||||
setPrependSeq(
|
||||
prependSeq.filter(
|
||||
(v) => v.name !== item.name
|
||||
)
|
||||
(v) => v.name !== item.name,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -385,8 +385,8 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredProxyList[newIndex].name
|
||||
)
|
||||
(v) => v !== filteredProxyList[newIndex].name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
@@ -418,8 +418,8 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
onDelete={() => {
|
||||
setAppendSeq(
|
||||
appendSeq.filter(
|
||||
(v) => v.name !== item.name
|
||||
)
|
||||
(v) => v.name !== item.name,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -453,8 +453,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
padding: {
|
||||
top: 33, // 顶部padding防止遮挡snippets
|
||||
},
|
||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||
}`,
|
||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
||||
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||
}`,
|
||||
fontLigatures: false, // 连字符
|
||||
smoothScrolling: true, // 平滑滚动
|
||||
}}
|
||||
|
||||
@@ -49,8 +49,8 @@ export const ProxyItem = (props: Props) => {
|
||||
? alpha(palette.background.paper, 0.3)
|
||||
: alpha(palette.grey[400], 0.3)
|
||||
: type === "delete"
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
height: "100%",
|
||||
margin: "8px 0",
|
||||
borderRadius: "8px",
|
||||
|
||||
@@ -52,8 +52,8 @@ export const RuleItem = (props: Props) => {
|
||||
? alpha(palette.background.paper, 0.3)
|
||||
: alpha(palette.grey[400], 0.3)
|
||||
: type === "delete"
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
height: "100%",
|
||||
margin: "8px 0",
|
||||
borderRadius: "8px",
|
||||
|
||||
@@ -55,17 +55,17 @@ interface Props {
|
||||
|
||||
const portValidator = (value: string): boolean => {
|
||||
return new RegExp(
|
||||
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$"
|
||||
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$",
|
||||
).test(value);
|
||||
};
|
||||
const ipv4CIDRValidator = (value: string): boolean => {
|
||||
return new RegExp(
|
||||
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$"
|
||||
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$",
|
||||
).test(value);
|
||||
};
|
||||
const ipv6CIDRValidator = (value: string): boolean => {
|
||||
return new RegExp(
|
||||
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$"
|
||||
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$",
|
||||
).test(value);
|
||||
};
|
||||
|
||||
@@ -76,161 +76,161 @@ const rules: {
|
||||
noResolve?: boolean;
|
||||
validator?: (value: string) => boolean;
|
||||
}[] = [
|
||||
{
|
||||
name: "DOMAIN",
|
||||
example: "example.com",
|
||||
},
|
||||
{
|
||||
name: "DOMAIN-SUFFIX",
|
||||
example: "example.com",
|
||||
},
|
||||
{
|
||||
name: "DOMAIN-KEYWORD",
|
||||
example: "example",
|
||||
},
|
||||
{
|
||||
name: "DOMAIN-REGEX",
|
||||
example: "example.*",
|
||||
},
|
||||
{
|
||||
name: "GEOSITE",
|
||||
example: "youtube",
|
||||
},
|
||||
{
|
||||
name: "GEOIP",
|
||||
example: "CN",
|
||||
noResolve: true,
|
||||
},
|
||||
{
|
||||
name: "SRC-GEOIP",
|
||||
example: "CN",
|
||||
},
|
||||
{
|
||||
name: "IP-ASN",
|
||||
example: "13335",
|
||||
noResolve: true,
|
||||
validator: (value) => (+value ? true : false),
|
||||
},
|
||||
{
|
||||
name: "SRC-IP-ASN",
|
||||
example: "9808",
|
||||
validator: (value) => (+value ? true : false),
|
||||
},
|
||||
{
|
||||
name: "IP-CIDR",
|
||||
example: "127.0.0.0/8",
|
||||
noResolve: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "IP-CIDR6",
|
||||
example: "2620:0:2d0:200::7/32",
|
||||
noResolve: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "SRC-IP-CIDR",
|
||||
example: "192.168.1.201/32",
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "IP-SUFFIX",
|
||||
example: "8.8.8.8/24",
|
||||
noResolve: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "SRC-IP-SUFFIX",
|
||||
example: "192.168.1.201/8",
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "SRC-PORT",
|
||||
example: "7777",
|
||||
validator: (value) => portValidator(value),
|
||||
},
|
||||
{
|
||||
name: "DST-PORT",
|
||||
example: "80",
|
||||
validator: (value) => portValidator(value),
|
||||
},
|
||||
{
|
||||
name: "IN-PORT",
|
||||
example: "7890",
|
||||
validator: (value) => portValidator(value),
|
||||
},
|
||||
{
|
||||
name: "DSCP",
|
||||
example: "4",
|
||||
},
|
||||
{
|
||||
name: "PROCESS-NAME",
|
||||
example: getSystem() === "windows" ? "chrome.exe" : "curl",
|
||||
},
|
||||
{
|
||||
name: "PROCESS-PATH",
|
||||
example:
|
||||
getSystem() === "windows"
|
||||
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||
: "/usr/bin/wget",
|
||||
},
|
||||
{
|
||||
name: "PROCESS-NAME-REGEX",
|
||||
example: ".*telegram.*",
|
||||
},
|
||||
{
|
||||
name: "PROCESS-PATH-REGEX",
|
||||
example:
|
||||
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
|
||||
},
|
||||
{
|
||||
name: "NETWORK",
|
||||
example: "udp",
|
||||
validator: (value) => ["tcp", "udp"].includes(value),
|
||||
},
|
||||
{
|
||||
name: "UID",
|
||||
example: "1001",
|
||||
validator: (value) => (+value ? true : false),
|
||||
},
|
||||
{
|
||||
name: "IN-TYPE",
|
||||
example: "SOCKS/HTTP",
|
||||
},
|
||||
{
|
||||
name: "IN-USER",
|
||||
example: "mihomo",
|
||||
},
|
||||
{
|
||||
name: "IN-NAME",
|
||||
example: "ss",
|
||||
},
|
||||
{
|
||||
name: "SUB-RULE",
|
||||
example: "(NETWORK,tcp)",
|
||||
},
|
||||
{
|
||||
name: "RULE-SET",
|
||||
example: "providername",
|
||||
noResolve: true,
|
||||
},
|
||||
{
|
||||
name: "AND",
|
||||
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
|
||||
},
|
||||
{
|
||||
name: "OR",
|
||||
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
|
||||
},
|
||||
{
|
||||
name: "NOT",
|
||||
example: "((DOMAIN,baidu.com))",
|
||||
},
|
||||
{
|
||||
name: "MATCH",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
{
|
||||
name: "DOMAIN",
|
||||
example: "example.com",
|
||||
},
|
||||
{
|
||||
name: "DOMAIN-SUFFIX",
|
||||
example: "example.com",
|
||||
},
|
||||
{
|
||||
name: "DOMAIN-KEYWORD",
|
||||
example: "example",
|
||||
},
|
||||
{
|
||||
name: "DOMAIN-REGEX",
|
||||
example: "example.*",
|
||||
},
|
||||
{
|
||||
name: "GEOSITE",
|
||||
example: "youtube",
|
||||
},
|
||||
{
|
||||
name: "GEOIP",
|
||||
example: "CN",
|
||||
noResolve: true,
|
||||
},
|
||||
{
|
||||
name: "SRC-GEOIP",
|
||||
example: "CN",
|
||||
},
|
||||
{
|
||||
name: "IP-ASN",
|
||||
example: "13335",
|
||||
noResolve: true,
|
||||
validator: (value) => (+value ? true : false),
|
||||
},
|
||||
{
|
||||
name: "SRC-IP-ASN",
|
||||
example: "9808",
|
||||
validator: (value) => (+value ? true : false),
|
||||
},
|
||||
{
|
||||
name: "IP-CIDR",
|
||||
example: "127.0.0.0/8",
|
||||
noResolve: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "IP-CIDR6",
|
||||
example: "2620:0:2d0:200::7/32",
|
||||
noResolve: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "SRC-IP-CIDR",
|
||||
example: "192.168.1.201/32",
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "IP-SUFFIX",
|
||||
example: "8.8.8.8/24",
|
||||
noResolve: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "SRC-IP-SUFFIX",
|
||||
example: "192.168.1.201/8",
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
},
|
||||
{
|
||||
name: "SRC-PORT",
|
||||
example: "7777",
|
||||
validator: (value) => portValidator(value),
|
||||
},
|
||||
{
|
||||
name: "DST-PORT",
|
||||
example: "80",
|
||||
validator: (value) => portValidator(value),
|
||||
},
|
||||
{
|
||||
name: "IN-PORT",
|
||||
example: "7890",
|
||||
validator: (value) => portValidator(value),
|
||||
},
|
||||
{
|
||||
name: "DSCP",
|
||||
example: "4",
|
||||
},
|
||||
{
|
||||
name: "PROCESS-NAME",
|
||||
example: getSystem() === "windows" ? "chrome.exe" : "curl",
|
||||
},
|
||||
{
|
||||
name: "PROCESS-PATH",
|
||||
example:
|
||||
getSystem() === "windows"
|
||||
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||
: "/usr/bin/wget",
|
||||
},
|
||||
{
|
||||
name: "PROCESS-NAME-REGEX",
|
||||
example: ".*telegram.*",
|
||||
},
|
||||
{
|
||||
name: "PROCESS-PATH-REGEX",
|
||||
example:
|
||||
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
|
||||
},
|
||||
{
|
||||
name: "NETWORK",
|
||||
example: "udp",
|
||||
validator: (value) => ["tcp", "udp"].includes(value),
|
||||
},
|
||||
{
|
||||
name: "UID",
|
||||
example: "1001",
|
||||
validator: (value) => (+value ? true : false),
|
||||
},
|
||||
{
|
||||
name: "IN-TYPE",
|
||||
example: "SOCKS/HTTP",
|
||||
},
|
||||
{
|
||||
name: "IN-USER",
|
||||
example: "mihomo",
|
||||
},
|
||||
{
|
||||
name: "IN-NAME",
|
||||
example: "ss",
|
||||
},
|
||||
{
|
||||
name: "SUB-RULE",
|
||||
example: "(NETWORK,tcp)",
|
||||
},
|
||||
{
|
||||
name: "RULE-SET",
|
||||
example: "providername",
|
||||
noResolve: true,
|
||||
},
|
||||
{
|
||||
name: "AND",
|
||||
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
|
||||
},
|
||||
{
|
||||
name: "OR",
|
||||
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
|
||||
},
|
||||
{
|
||||
name: "NOT",
|
||||
example: "((DOMAIN,baidu.com))",
|
||||
},
|
||||
{
|
||||
name: "MATCH",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
||||
|
||||
@@ -260,22 +260,22 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
const filteredPrependSeq = useMemo(
|
||||
() => prependSeq.filter((rule) => match(rule)),
|
||||
[prependSeq, match]
|
||||
[prependSeq, match],
|
||||
);
|
||||
const filteredRuleList = useMemo(
|
||||
() => ruleList.filter((rule) => match(rule)),
|
||||
[ruleList, match]
|
||||
[ruleList, match],
|
||||
);
|
||||
const filteredAppendSeq = useMemo(
|
||||
() => appendSeq.filter((rule) => match(rule)),
|
||||
[appendSeq, match]
|
||||
[appendSeq, match],
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
}),
|
||||
);
|
||||
const reorder = (list: string[], startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(list);
|
||||
@@ -333,11 +333,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
setCurrData(
|
||||
yaml.dump(
|
||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||
{ forceQuotes: true }
|
||||
)
|
||||
{ forceQuotes: true },
|
||||
),
|
||||
);
|
||||
} catch (e: any) {
|
||||
showNotice('error', e?.message || e?.toString() || 'YAML dump error');
|
||||
showNotice("error", e?.message || e?.toString() || "YAML dump error");
|
||||
}
|
||||
};
|
||||
if (window.requestIdleCallback) {
|
||||
@@ -371,7 +371,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
return !moreDeleteGroups.includes(group);
|
||||
}
|
||||
}),
|
||||
moreAppendGroups
|
||||
moreAppendGroups,
|
||||
);
|
||||
|
||||
let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null;
|
||||
@@ -396,7 +396,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
let globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
|
||||
let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule);
|
||||
setProxyPolicyList(
|
||||
builtinProxyPolicies.concat(groups.map((group: any) => group.name))
|
||||
builtinProxyPolicies.concat(groups.map((group: any) => group.name)),
|
||||
);
|
||||
setRuleSetList(Object.keys(ruleSet));
|
||||
setSubRuleList(Object.keys(subRule));
|
||||
@@ -417,19 +417,20 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
throw new Error(t("Invalid Rule"));
|
||||
}
|
||||
|
||||
const condition = ruleType.required ?? true ? ruleContent : "";
|
||||
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : ""
|
||||
}`;
|
||||
const condition = (ruleType.required ?? true) ? ruleContent : "";
|
||||
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
|
||||
ruleType.noResolve && noResolve ? ",no-resolve" : ""
|
||||
}`;
|
||||
};
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
await saveProfileFile(property, currData);
|
||||
showNotice('success', t("Saved Successfully"));
|
||||
showNotice("success", t("Saved Successfully"));
|
||||
onSave?.(prevData, currData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.toString());
|
||||
showNotice("error", err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -557,7 +558,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
if (prependSeq.includes(raw)) return;
|
||||
setPrependSeq([raw, ...prependSeq]);
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString());
|
||||
showNotice("error", err.message || err.toString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -575,7 +576,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
if (appendSeq.includes(raw)) return;
|
||||
setAppendSeq([...appendSeq, raw]);
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString());
|
||||
showNotice("error", err.message || err.toString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -621,7 +622,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(
|
||||
prependSeq.filter((v) => v !== item)
|
||||
prependSeq.filter((v) => v !== item),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -645,8 +646,8 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredRuleList[newIndex]
|
||||
)
|
||||
(v) => v !== filteredRuleList[newIndex],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
@@ -677,7 +678,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(
|
||||
appendSeq.filter((v) => v !== item)
|
||||
appendSeq.filter((v) => v !== item),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -711,8 +712,9 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
padding: {
|
||||
top: 33, // 顶部padding防止遮挡snippets
|
||||
},
|
||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||
}`,
|
||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
||||
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||
}`,
|
||||
fontLigatures: false, // 连字符
|
||||
smoothScrolling: true, // 平滑滚动
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
import {
|
||||
Button,
|
||||
Box,
|
||||
Dialog,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
LinearProgress,
|
||||
alpha,
|
||||
styled,
|
||||
useTheme
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
@@ -65,77 +65,83 @@ export const ProviderButton = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
|
||||
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||
|
||||
|
||||
// 检查是否有提供者
|
||||
const hasProviders = Object.keys(proxyProviders || {}).length > 0;
|
||||
|
||||
|
||||
// 更新单个代理提供者
|
||||
const updateProvider = useLockFn(async (name: string) => {
|
||||
try {
|
||||
// 设置更新状态
|
||||
setUpdating(prev => ({ ...prev, [name]: true }));
|
||||
|
||||
setUpdating((prev) => ({ ...prev, [name]: true }));
|
||||
|
||||
await proxyProviderUpdate(name);
|
||||
|
||||
|
||||
// 刷新数据
|
||||
await refreshProxy();
|
||||
await refreshProxyProviders();
|
||||
|
||||
showNotice('success', `${name} 更新成功`);
|
||||
|
||||
showNotice("success", `${name} 更新成功`);
|
||||
} catch (err: any) {
|
||||
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`);
|
||||
showNotice(
|
||||
"error",
|
||||
`${name} 更新失败: ${err?.message || err.toString()}`,
|
||||
);
|
||||
} finally {
|
||||
// 清除更新状态
|
||||
setUpdating(prev => ({ ...prev, [name]: false }));
|
||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 更新所有代理提供者
|
||||
const updateAllProviders = useLockFn(async () => {
|
||||
try {
|
||||
// 获取所有provider的名称
|
||||
const allProviders = Object.keys(proxyProviders || {});
|
||||
if (allProviders.length === 0) {
|
||||
showNotice('info', "没有可更新的代理提供者");
|
||||
showNotice("info", "没有可更新的代理提供者");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 设置所有provider为更新中状态
|
||||
const newUpdating = allProviders.reduce((acc, key) => {
|
||||
acc[key] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
const newUpdating = allProviders.reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, boolean>,
|
||||
);
|
||||
setUpdating(newUpdating);
|
||||
|
||||
|
||||
// 改为串行逐个更新所有provider
|
||||
for (const name of allProviders) {
|
||||
try {
|
||||
await proxyProviderUpdate(name);
|
||||
// 每个更新完成后更新状态
|
||||
setUpdating(prev => ({ ...prev, [name]: false }));
|
||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||
} catch (err) {
|
||||
console.error(`更新 ${name} 失败`, err);
|
||||
// 继续执行下一个,不中断整体流程
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 刷新数据
|
||||
await refreshProxy();
|
||||
await refreshProxyProviders();
|
||||
|
||||
showNotice('success', "全部代理提供者更新成功");
|
||||
|
||||
showNotice("success", "全部代理提供者更新成功");
|
||||
} catch (err: any) {
|
||||
showNotice('error', `更新失败: ${err?.message || err.toString()}`);
|
||||
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
|
||||
} finally {
|
||||
// 清除所有更新状态
|
||||
setUpdating({});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
|
||||
if (!hasProviders) return null;
|
||||
|
||||
return (
|
||||
@@ -149,15 +155,14 @@ export const ProviderButton = () => {
|
||||
>
|
||||
{t("Proxy Provider")}
|
||||
</Button>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="h6">{t("Proxy Provider")}</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
@@ -170,14 +175,14 @@ export const ProviderButton = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
|
||||
<DialogContent>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(proxyProviders || {}).map(([key, item]) => {
|
||||
const provider = item as ProxyProviderItem;
|
||||
const time = dayjs(provider.updatedAt);
|
||||
const isUpdating = updating[key];
|
||||
|
||||
|
||||
// 订阅信息
|
||||
const sub = provider.subscriptionInfo;
|
||||
const hasSubInfo = !!sub;
|
||||
@@ -185,46 +190,53 @@ export const ProviderButton = () => {
|
||||
const download = sub?.Download || 0;
|
||||
const total = sub?.Total || 0;
|
||||
const expire = sub?.Expire || 0;
|
||||
|
||||
|
||||
// 流量使用进度
|
||||
const progress = total > 0
|
||||
? Math.min(Math.round(((download + upload) * 100) / total) + 1, 100)
|
||||
: 0;
|
||||
|
||||
const progress =
|
||||
total > 0
|
||||
? Math.min(
|
||||
Math.round(((download + upload) * 100) / total) + 1,
|
||||
100,
|
||||
)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
sx={[
|
||||
{
|
||||
{
|
||||
p: 0,
|
||||
mb: "8px",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s"
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
({ palette: { mode, primary } }) => {
|
||||
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
||||
const hoverColor = mode === "light"
|
||||
? alpha(primary.main, 0.1)
|
||||
: alpha(primary.main, 0.2);
|
||||
|
||||
const hoverColor =
|
||||
mode === "light"
|
||||
? alpha(primary.main, 0.1)
|
||||
: alpha(primary.main, 0.2);
|
||||
|
||||
return {
|
||||
backgroundColor: bgcolor,
|
||||
"&:hover": {
|
||||
backgroundColor: hoverColor,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ px: 2, py: 1 }}
|
||||
primary={
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
@@ -232,7 +244,7 @@ export const ProviderButton = () => {
|
||||
title={key}
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||
<TypeBox component="span">
|
||||
{provider.proxies.length}
|
||||
</TypeBox>
|
||||
@@ -240,9 +252,14 @@ export const ProviderButton = () => {
|
||||
{provider.vehicleType}
|
||||
</TypeBox>
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" noWrap>
|
||||
<small>{t("Update At")}: </small>{time.fromNow()}
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
>
|
||||
<small>{t("Update At")}: </small>
|
||||
{time.fromNow()}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
@@ -251,26 +268,29 @@ export const ProviderButton = () => {
|
||||
{/* 订阅信息 */}
|
||||
{hasSubInfo && (
|
||||
<>
|
||||
<Box sx={{
|
||||
mb: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span title={t("Used / Total") as string}>
|
||||
{parseTraffic(upload + download)} / {parseTraffic(total)}
|
||||
{parseTraffic(upload + download)} /{" "}
|
||||
{parseTraffic(total)}
|
||||
</span>
|
||||
<span title={t("Expire Time") as string}>
|
||||
{parseExpire(expire)}
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* 进度条 */}
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
height: 6,
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
opacity: total > 0 ? 1 : 0,
|
||||
}}
|
||||
@@ -281,12 +301,14 @@ export const ProviderButton = () => {
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box sx={{
|
||||
width: 40,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
@@ -295,11 +317,13 @@ export const ProviderButton = () => {
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
sx={{
|
||||
animation: isUpdating ? "spin 1s linear infinite" : "none",
|
||||
animation: isUpdating
|
||||
? "spin 1s linear infinite"
|
||||
: "none",
|
||||
"@keyframes spin": {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" }
|
||||
}
|
||||
"100%": { transform: "rotate(360deg)" },
|
||||
},
|
||||
}}
|
||||
title={t("Update Provider") as string}
|
||||
>
|
||||
@@ -311,7 +335,7 @@ export const ProviderButton = () => {
|
||||
})}
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="outlined">
|
||||
{t("Close")}
|
||||
|
||||
@@ -106,10 +106,12 @@ const LetterItem = memo(
|
||||
name,
|
||||
onClick,
|
||||
getFirstChar,
|
||||
enableAutoScroll = true,
|
||||
}: {
|
||||
name: string;
|
||||
onClick: (name: string) => void;
|
||||
getFirstChar: (str: string) => string;
|
||||
enableAutoScroll?: boolean;
|
||||
}) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const letterRef = useRef<HTMLDivElement>(null);
|
||||
@@ -136,11 +138,14 @@ const LetterItem = memo(
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setShowTooltip(true);
|
||||
// 添加 200ms 的延迟,避免鼠标快速划过时触发滚动
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
onClick(name);
|
||||
}, 100);
|
||||
}, [name, onClick]);
|
||||
// 只有在启用自动滚动时才触发滚动
|
||||
if (enableAutoScroll) {
|
||||
// 添加 100ms 的延迟,避免鼠标快速划过时触发滚动
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
onClick(name);
|
||||
}, 100);
|
||||
}
|
||||
}, [name, onClick, enableAutoScroll]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setShowTooltip(false);
|
||||
@@ -198,6 +203,9 @@ export const ProxyGroups = (props: Props) => {
|
||||
|
||||
const { verge } = useVerge();
|
||||
const { current, patchCurrent } = useProfiles();
|
||||
|
||||
// 获取自动滚动开关状态,默认为 true
|
||||
const enableAutoScroll = verge?.enable_hover_jump_navigator ?? true;
|
||||
const timeout = verge?.default_latency_timeout || 10000;
|
||||
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
@@ -541,6 +549,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
name={name}
|
||||
onClick={handleLetterClick}
|
||||
getFirstChar={getFirstChar}
|
||||
enableAutoScroll={enableAutoScroll}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,10 @@ interface RenderProps {
|
||||
onLocation: (group: IRenderItem["group"]) => void;
|
||||
onCheckAll: (groupName: string) => void;
|
||||
onHeadState: (groupName: string, patch: Partial<HeadState>) => void;
|
||||
onChangeProxy: (group: IRenderItem["group"], proxy: IRenderItem["proxy"] & { name: string }) => void;
|
||||
onChangeProxy: (
|
||||
group: IRenderItem["group"],
|
||||
proxy: IRenderItem["proxy"] & { name: string },
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const ProxyRender = (props: RenderProps) => {
|
||||
@@ -129,14 +132,15 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
/>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Tooltip title={t("Proxy Count")} arrow>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`${group.all.length}`}
|
||||
sx={{
|
||||
mr: 1,
|
||||
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.1),
|
||||
<Chip
|
||||
size="small"
|
||||
label={`${group.all.length}`}
|
||||
sx={{
|
||||
mr: 1,
|
||||
backgroundColor: (theme) =>
|
||||
alpha(theme.palette.primary.main, 0.1),
|
||||
color: (theme) => theme.palette.primary.main,
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function useFilterSort(
|
||||
proxies: IProxyItem[],
|
||||
groupName: string,
|
||||
filterText: string,
|
||||
sortType: ProxySortType
|
||||
sortType: ProxySortType,
|
||||
) {
|
||||
const [refresh, setRefresh] = useState({});
|
||||
|
||||
@@ -40,7 +40,7 @@ export function filterSort(
|
||||
proxies: IProxyItem[],
|
||||
groupName: string,
|
||||
filterText: string,
|
||||
sortType: ProxySortType
|
||||
sortType: ProxySortType,
|
||||
) {
|
||||
const fp = filterProxies(proxies, groupName, filterText);
|
||||
const sp = sortProxies(fp, groupName, sortType);
|
||||
@@ -60,7 +60,7 @@ const regex2 = /type=(.*)/i;
|
||||
function filterProxies(
|
||||
proxies: IProxyItem[],
|
||||
groupName: string,
|
||||
filterText: string
|
||||
filterText: string,
|
||||
) {
|
||||
if (!filterText) return proxies;
|
||||
|
||||
@@ -100,7 +100,7 @@ function filterProxies(
|
||||
function sortProxies(
|
||||
proxies: IProxyItem[],
|
||||
groupName: string,
|
||||
sortType: ProxySortType
|
||||
sortType: ProxySortType,
|
||||
) {
|
||||
if (!proxies) return [];
|
||||
if (sortType === 0) return proxies;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user