Compare commits

...
90 Commits
Author SHA1 Message Date
wonfen c423fe90e2 Release 2.3.1 2025-06-18 20:42:13 +08:00
623461b649 about tray and silent startup + automatic startup light repair and optimization (#3814)
* fixed the issue that the tray does not display in light mode

* add show main window after exiting light mode

* fix: the issue where other selections could not respond in real time

* Refactored the original silent + self-starting lightweight #3708

* update logs

* fix formatting issues

* fix

* fix nesting

* repair previous formatting issues

* fix: resolve issue with tray not restoring from lightweight mode

* remove the tray display that should not exist

* fix translation field issue

---------

Co-authored-by: Ahao <108321411+xuanyuan0408@users.noreply.github.com>
Co-authored-by: Tunglies <selenvow+github@gmail.com>
2025-06-18 20:00:33 +08:00
Tunglies 1f9ba4de5c feat: add translation for 'Already Using Latest Core' in English and Chinese locales 2025-06-18 19:10:08 +08:00
Tunglies 10f792b8bc fix: streamline pnpm installation steps in autobuild workflow (#3816)
* fix: streamline pnpm installation steps in autobuild workflow

* fix: format code for better readability in useCustomTheme and _layout components
2025-06-18 18:57:05 +08:00
Tunglies 357b64e9a0 feat: enhance pre-push hook to include remote URL check and improve format check messaging 2025-06-18 18:56:03 +08:00
Tunglies f3ad6cee41 feat: add code format check in pre-push hook 2025-06-18 18:55:53 +08:00
972310ea4e fix:Linux displays white boxes, white dots and white bars (#3785)
Co-authored-by: Ahao <108321411+xuanyuan0408@users.noreply.github.com>
Co-authored-by: Tunglies <selenvow+github@gmail.com>
2025-06-18 08:28:54 +08:00
时雨andTunglies f5315fd737 fix: 修正Linux系统窗口下页面白边和顶部圆角白底 (#3778)
Co-authored-by: Tunglies <selenvow+github@gmail.com>
2025-06-18 08:28:40 +08:00
Tunglies c40c1960ed feat: add window-state management and recovery functionality (#3792)
* feat: add window-state management and recovery functionality

* fix: correct file name reference for window-state.json in uninstall section

* fix: remove unused tauri_plugin_window_state import
2025-06-18 02:21:00 +08:00
Tunglies c8ae37811c chore: add pnpm cache configuration to Node setup steps 2025-06-18 01:42:06 +08:00
Tunglies 15f05748a5 fix: update tauri-plugin-autostart to version 2.4.0 and change source to crates.io 2025-06-18 01:34:40 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 0a0a25913b chore(deps): update npm dependencies (#3718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 01:31:39 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 945999889c chore(deps): update cargo dependencies (#3731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 01:31:24 +08:00
Tunglies ce7e1d6456 chore: remove pnpm cache configuration from workflow 2025-06-18 01:30:38 +08:00
Tunglies 72c4b9de8a doc: clarify macOS Intel X86 architecture build error message 2025-06-18 01:30:05 +08:00
Tunglies a8257e8cb2 fix: unexpected behavior while pulling resources (#3789)
fix: unexpected behavior while pulling resources

在 commit 25cfd162f6 中引入新的 pnpm prepare 执行脚本指令,而 prepare 关键字与包管理器的生命周期脚本冲突。导致在运行 Workflow 的时候被执行两次资源拉取,并且被当做生命周期脚本的时候没有携带预期的 \`${{ matrix.target }}\`。这一行为进一步影响 macOS Intel X86 平台上的构建。

所影响的 issues: #3753, #3771
2025-06-18 01:11:33 +08:00
Tunglies 15d3c765ac feat: add debug logging for matrix target and update prepare script invocation 2025-06-17 23:35:23 +08:00
wonfen e7d3d7e0ae chore: update changelog 2025-06-17 22:05:03 +08:00
wonfen c0f3d35e13 fix: resolve namespace issue 2025-06-17 19:47:24 +08:00
wonfen fc30fab9cd feat: unify window manager and optimize window show/hide logic 2025-06-17 13:02:47 +08:00
wonfen a67e8388a9 fix: restore window from tray when status bar icon is present 2025-06-17 12:59:05 +08:00
wonfen ac7307b1f7 feat: enhance profile management and proxy refresh with improved event listening and state updates 2025-06-17 11:38:53 +08:00
Tunglies 4068e5ec9c fix: clippy error in https://github.com/clash-verge-rev/clash-verge-rev/commit/26acce94a40e9203774107187809b0309918d08a#diff-3a1c371e65c9809233eadec0aed9dfc09813f06b4d622ffd5a613ae6020048c8R298 2025-06-17 08:24:10 +08:00
Just want to protect youandAhao ea71181692 fix : js script escape error issue introduced #3751 (#3759)
* refactoring fix js script is too strict

* fix:format

---------

Co-authored-by: Ahao <108321411+xuanyuan0408@users.noreply.github.com>
2025-06-16 21:04:40 +08:00
wonfen 9b46c1a991 refactor: unify log output format 2025-06-16 21:03:45 +08:00
wonfen 26acce94a4 feat: add clash_core config validation & auto-fix to default; fixed "No such file or directory (os error 2)" 2025-06-16 20:27:02 +08:00
wonfen 2a0e1e206e Release 2.3.0 2025-06-16 12:01:14 +08:00
Tunglies f83d1d1582 fix: unexpected workflow changes #3684 2025-06-16 00:57:53 +08:00
wonfen 77a090d5eb i18n: update en 2025-06-15 20:23:32 +08:00
陈生杂物房 a9561e0ded feat: 更新简体中文语言文件 (#3684)
* feat: 更新简体中文语言文件

* 报错照样上传文件
2025-06-15 19:38:55 +08:00
wonfen dd505e4d58 perf: optimize layout initialization and enhance backend notification mechanism 2025-06-15 19:35:12 +08:00
wonfen dd0e9d4e1b fix: enhance hotkey handling in lightweight mode and improve window management 2025-06-15 17:40:59 +08:00
希亚的西红柿andTunglies 8bc451ff08 fix: proxy mode description displays incorrectly (#3719)
* fix: proxy mode description display incorrectly

* fix: update language json

---------

Co-authored-by: Tunglies <selenvow+github@gmail.com>
2025-06-15 05:19:30 +08:00
Tunglies 032e5bf32e fix: add macOS specific activation policy handling for silent start (#3740)
* fix: add macOS specific activation policy handling for silent start

* fix: resolve abnormal Dock icon launch during macOS silent startup
2025-06-15 05:10:15 +08:00
Tunglies 41ba8f6c7a fix: streamline pre-push hook to run clippy only for src-tauri changes 2025-06-14 02:47:15 +08:00
Tunglies 799783d3ef fix: ensure app exits gracefully when an instance is already running 2025-06-14 02:43:58 +08:00
Just want to protect youandAhao 77fb40506f fix the JS script error when escaping special characters to prevent special airport names from causing the script to fail to work (#3717)
* fix the JS script error

* update logs

* remove unused references

* update logs

* fix special escape test

* optimize:unescape of special characters in JS strings

* fix unaligned annotations

---------

Co-authored-by: Ahao <108321411+xuanyuan0408@users.noreply.github.com>
2025-06-13 22:59:48 +08:00
wonfen 5858f05c13 perf: async app startup loading to prevent UI freeze 2025-06-13 22:59:16 +08:00
Tunglies 0432cad112 fix: disable node and pnpm auto update to prevent breaks build 2025-06-13 14:22:21 +08:00
rebecca554owen 47ec8a348c Revert "fix: update packageManager to pnpm@10.12.1" (#3728)
This reverts commit 9bd5950f60.
2025-06-13 14:21:07 +08:00
Tunglies eef348b8dc fix: add conditional compilation for localhost URL in dev mode 2025-06-13 03:11:35 +08:00
Tunglies e78a02d0ba fix: add function to retrieve short git commit hash and update timestamp format 2025-06-13 00:17:31 +08:00
希亚的西红柿 4e54b61380 fix: silent start does not take effect (#3708)
* fix: light mode error

* feat: optimize the logic

* fix: clippy issues
2025-06-12 23:39:03 +08:00
Tunglies cc39b2734e fix: enhance system proxy toggle logic to account for autoproxy and sysproxy states 2025-06-12 22:42:33 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 16eaccb10e chore(deps): update npm dependencies (#3714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 21:55:54 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> dbc6f54a77 chore(deps): update rust crate reqwest to 0.12.20 (#3715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 21:32:03 +08:00
Tunglies 9bd5950f60 fix: update packageManager to pnpm@10.12.1 2025-06-12 21:27:23 +08:00
Tunglies 2c7c416f60 fix: simplify mutable reference handling in with_lightweight_status function 2025-06-12 21:26:22 +08:00
Tunglies 3eb0ab4197 fix: enable node/pnpm version updates in Renovate configuration 2025-06-12 21:23:31 +08:00
TianHua Liu b7cb511032 perf(Notice): replace useState by useSyncExternalStore (#3682)
* fix: the first function call creates multiple axios instances

* perf(Notice): replace useState by useSyncExternalStore

* chore: prettier --check .

* fix: prettier format
2025-06-12 20:49:39 +08:00
wonfen 18850bb51a Update Crowdin configuration file 2025-06-12 10:57:33 +08:00
wonfen 4d718e0cea Update Crowdin configuration file 2025-06-12 10:51:21 +08:00
wonfen 3a2f567055 Update Crowdin configuration file 2025-06-12 10:48:58 +08:00
rebecca554owen 4f474e1098 feat: add proxy auto scroll toggle setting (#3674)
* feat: add proxy auto scroll toggle setting

* refactor: rename enable_proxy_auto_scroll to enable_hover_jump_navigator

* 参考 Unified Delay 调整式样
2025-06-11 13:21:24 +08:00
Tunglies 22eac1a832 refactor: comment out Rust cache step in Clippy workflow 2025-06-11 07:53:59 +08:00
Tunglies 07738dd82d feat: implement lightweight mode state management and initialization #3344, d05952e945 2025-06-11 04:20:50 +08:00
Tunglies f07d508018 refactor: standardize casing for "HTTP Port" to "Http Port" in localization files 2025-06-11 03:40:16 +08:00
Tunglies b591a90100 feat: add Linux-specific tray icon builder configuration 2025-06-11 01:04:56 +08:00
Tunglies db3cfdf66f refactor: update icon variable naming for clarity and adjust macOS logging import 2025-06-11 00:51:34 +08:00
Tunglies 5cf3e1a817 refactor: streamline macOS-specific code by consolidating conditional imports and logic in sysopt, tray, lightweight, and mihomo modules 2025-06-11 00:19:06 +08:00
Tunglies 25cfd162f6 refactor: replace 'pnpm check' with 'pnpm prepare' in workflow files and update CONTRIBUTING.md 2025-06-10 23:43:04 +08:00
Tunglies 7001ef2030 feat: add Clippy linting workflow for pull requests
feat: add check step to Clippy workflow

feat: add build step for web assets in Clippy workflow

fix: set NODE_OPTIONS for web asset build to prevent memory issues
2025-06-10 23:37:10 +08:00
Tunglies cbcab72a7c refactor: simplify version check logic in run_core_by_service and update tray menu function call 2025-06-10 23:37:09 +08:00
wonfen d05b1c3130 feat: clean up redundant profiles files 2025-06-09 13:48:38 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> fbdcda9a7f chore(deps): update npm dependencies (#3660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 20:37:49 +08:00
Tunglies 0ebc482698 refactor: update UPDATELOG.md for clarity and organization, enhance security recommendations, and improve known issues section 2025-06-07 10:42:43 +08:00
Tunglies 09969d95de feat: add rustfmt configuration and CI workflow for code formatting
refactor: streamline formatting workflow by removing unused taplo steps and clarifying directory change

refactor: remove unnecessary directory change step in formatting workflow
2025-06-06 22:13:11 +08:00
Tunglies 689042df60 refactor: use Box to store large config objects and add memory usage tests
- Refactored config-related structs to use Box for storing large objects (e.g., IRuntime, IProfiles, PrfItem) to reduce stack memory usage and improve performance.
- Updated related methods and assignments to handle Boxed types correctly.
- Added and improved unit tests to compare memory usage between Boxed and non-Boxed config objects, demonstrating the memory efficiency of Box.
- Test output now shows the size difference between stack-allocated and heap-allocated (Box) config objects.
2025-06-06 14:54:48 +08:00
Tunglies 564fe15df2 refactor(tests): update format_bytes_speed test cases for consistent output 2025-06-06 14:13:48 +08:00
Tunglies 30015dfd67 refactor(state/proxy): use Box<Value> for providers_proxies to reduce struct size
- Changed providers_proxies from Value to Box<Value> in CmdProxyState
- Reduces struct size from 32 bytes to 8 bytes, optimizing memory usage
2025-06-06 12:46:58 +08:00
Tunglies 698c47da69 Refactor workflow triggers in alpha.yml to clarify usage of git tags and remove commented-out sections 2025-06-06 11:58:58 +08:00
Tunglies 46b7a520cd Add I18N issue template for translation and internationalization requests
Fix I18N issue template description and update labels and type
2025-06-06 11:58:58 +08:00
Tunglies cadd6c3497 Add Node.js and pnpm installation steps to cross-check workflow 2025-06-06 11:58:58 +08:00
wonfen c4682ab6e9 revert translation to 8aa7b34197 2025-06-06 11:27:15 +08:00
Tunglies 59594855b8 chore: add cross-platform cargo check workflow and remove debug features 2025-06-06 11:27:15 +08:00
Tunglies 44b7af0c6e chore: publish tag alpha support
chore: update alpha and release workflows to include version consistency checks
2025-06-06 11:27:13 +08:00
Tunglies 80f550d67e refactor(proxy): enhance proxy state management and refresh logic 2025-06-05 20:10:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 44f085604a chore(config): migrate config renovate.json (#3659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 1144f8017a chore(deps): update cargo dependencies (#3653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> d8fa17a3e7 chore(deps): update npm dependencies (#3654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:28 +08:00
Tunglies bf868e0ae2 fix: update serde_yaml to deprecated version and adjust range strategy in renovate configuration 2025-06-05 19:34:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> e1dac63f69 chore(deps): update rust crate zip to v4 (#3652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2657234761 chore(deps): update npm dependencies (#3651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:27 +08:00
Tunglies 4a345fdd41 feat: add initial renovate configuration for dependency management
chore: update renovate configuration for improved dependency management

Revert "chore: update renovate configuration for improved dependency management"

This reverts commit dcfd5f95a5.

feat: add support for grouping cargo and npm dependencies into single PRs
2025-06-05 19:34:26 +08:00
Tunglies fee6a586d7 feat: implement CmdProxyState for managing proxy refresh state and add state management module
refactor: remove unnecessary macOS conditional compilation directive
2025-06-05 19:34:24 +08:00
Tunglies 7c738483b7 feat: add desktop-windows capabilities configuration and permissions
feat: add identifier, description, and windows properties to desktop-windows capabilities

fix: correct desktop-windows capability name in configuration
2025-06-05 19:34:23 +08:00
Tunglies c3d8ed28a2 fix: update fallback language to English in i18n configuration 2025-06-05 19:34:22 +08:00
Tunglies f47b4b961b fix: update alpha and release workflows to trigger builds via git tags only
feat: update release workflow to trigger builds via git tags and add publish version script
2025-06-05 19:34:20 +08:00
Tunglies 7763abf6c2 Revert TCP concurrency
Revert "Added controller translation for TCP concurrency"

This reverts commit c9589fc75d.

Revert "add:tcp concurrency"

This reverts commit 6440f856a2.
2025-06-02 21:44:43 +08:00
Tunglies 9d9d078346 feat: enhance workflow configurations for alpha and autobuild processes 2025-06-02 18:51:58 +08:00
157 changed files with 9696 additions and 7284 deletions
+58
View File
@@ -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
+178 -141
View File
@@ -1,123 +1,176 @@
name: Alpha Build name: Alpha Build
on: on:
# 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境
# 所以不再使用 workflow_dispatch 触发
# 应当通过 git tag 来触发构建
# TODO 手动控制版本号
workflow_dispatch: workflow_dispatch:
# schedule: # inputs:
# UTC+8 0,6,12,18 # tag_name:
# - cron: "0 16,22,4,10 * * *" # 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 permissions: write-all
env: env:
TAG_NAME: alpha
TAG_CHANNEL: Alpha
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short RUST_BACKTRACE: short
concurrency: concurrency:
# only allow per workflow per commit (and not pr) to run at a time
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}" group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs: jobs:
check_commit: check_alpha_tag:
name: Check Alpha Tag package.json Version Consistency
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check if version changed or src changed - name: Check tag and package.json version
id: check id: check_tag
run: | run: |
# For manual workflow_dispatch, always run TAG_REF="${GITHUB_REF##*/}"
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then echo "Current tag: $TAG_REF"
echo "should_run=true" >> $GITHUB_OUTPUT if [[ ! "$TAG_REF" =~ -alpha ]]; then
exit 0 echo "Current tag is not an alpha tag."
exit 1
fi fi
PKG_VERSION=$(jq -r .version package.json)
# Store current version from package.json echo "package.json version: $PKG_VERSION"
CURRENT_VERSION=$(cat package.json | jq -r '.version') if [[ "$PKG_VERSION" != *alpha* ]]; then
echo "Current version: $CURRENT_VERSION" echo "package.json version is not an alpha version."
exit 1
# Get the previous commit's package.json version
git checkout HEAD~1 package.json
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
echo "Previous version: $PREVIOUS_VERSION"
# Reset back to current commit
git checkout HEAD package.json
# Check if version changed
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi fi
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
# Check if src or src-tauri directories changed echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
CURRENT_SRC_HASH=$(git rev-parse HEAD:src) exit 1
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
echo "Current src hash: $CURRENT_SRC_HASH"
echo "Previous src hash: $PREVIOUS_SRC_HASH"
echo "Current tauri hash: $CURRENT_TAURI_HASH"
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
echo "Source directories changed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "Version and source directories unchanged"
echo "should_run=false" >> $GITHUB_OUTPUT
fi fi
echo "Alpha tag and package.json version are consistent."
delete_old_assets: delete_old_assets:
needs: check_commit name: Delete Old Alpha Release Assets and Tags
if: ${{ needs.check_commit.outputs.should_run == 'true' }} needs: check_alpha_tag
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Delete Old Alpha Release Assets - name: Delete Old Alpha Tags Except Latest
uses: actions/github-script@v7 uses: actions/github-script@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
const releaseTag = 'alpha'; const tagPattern = /-alpha.*/; // 匹配带有 -alpha 的 tag
const owner = context.repo.owner;
const repo = context.repo.repo;
try { try {
// Get the release by tag name // 获取所有 tag
const { data: release } = await github.rest.repos.getReleaseByTag({ const { data: tags } = await github.rest.repos.listTags({
owner: context.repo.owner, owner,
repo: context.repo.repo, repo,
tag: releaseTag per_page: 100 // 调整 per_page 以获取更多 tag
}); });
console.log(`Found release with ID: ${release.id}`); // 过滤出包含 -alpha 的 tag
const alphaTags = (await Promise.all(
tags
.filter(tag => tagPattern.test(tag.name))
.map(async tag => {
// 获取每个 tag 的 commit 信息以获得日期
const { data: commit } = await github.rest.repos.getCommit({
owner,
repo,
ref: tag.commit.sha
});
return {
...tag,
commitDate: commit.committer && commit.committer.date ? commit.committer.date : commit.commit.author.date
};
})
)).sort((a, b) => {
// 按 commit 日期降序排序(最新的在前面)
return new Date(b.commitDate) - new Date(a.commitDate);
});
// Delete each asset console.log(`Found ${alphaTags.length} alpha tags`);
if (release.assets && release.assets.length > 0) {
console.log(`Deleting ${release.assets.length} assets`);
for (const asset of release.assets) { if (alphaTags.length === 0) {
console.log(`Deleting asset: ${asset.name} (${asset.id})`); console.log('No alpha tags found');
await github.rest.repos.deleteReleaseAsset({ return;
owner: context.repo.owner, }
repo: context.repo.repo,
asset_id: asset.id // 保留最新的 tag
const latestTag = alphaTags[0];
console.log(`Keeping latest alpha tag: ${latestTag.name}`);
// 处理其他旧的 alpha tag
for (const tag of alphaTags.slice(1)) {
console.log(`Processing tag: ${tag.name}`);
// 获取与 tag 关联的 release
try {
const { data: release } = await github.rest.repos.getReleaseByTag({
owner,
repo,
tag: tag.name
}); });
}
console.log('All assets deleted successfully'); // 删除 release 下的所有资产
} else { if (release.assets && release.assets.length > 0) {
console.log('No assets found to delete'); console.log(`Deleting ${release.assets.length} assets for release ${tag.name}`);
for (const asset of release.assets) {
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
await github.rest.repos.deleteReleaseAsset({
owner,
repo,
asset_id: asset.id
});
}
}
// 删除 release
console.log(`Deleting release for tag: ${tag.name}`);
await github.rest.repos.deleteRelease({
owner,
repo,
release_id: release.id
});
// 删除 tag
console.log(`Deleting tag: ${tag.name}`);
await github.rest.git.deleteRef({
owner,
repo,
ref: `tags/${tag.name}`
});
} catch (error) {
if (error.status === 404) {
console.log(`No release found for tag ${tag.name}, deleting tag directly`);
await github.rest.git.deleteRef({
owner,
repo,
ref: `tags/${tag.name}`
});
} else {
console.error(`Error processing tag ${tag.name}:`, error);
throw error;
}
}
} }
console.log('Old alpha tags and releases deleted successfully');
} catch (error) { } catch (error) {
if (error.status === 404) { console.error('Error:', error);
console.log('Release not found, nothing to delete'); throw error;
} else {
console.error('Error:', error);
throw error;
}
} }
update_tag: update_tag:
@@ -128,22 +181,18 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Fetch Alpha update logs - name: Fetch UPDATE logs
id: fetch_alpha_logs id: fetch_update_logs
run: | run: |
# Check if UPDATELOG.md exists
if [ -f "UPDATELOG.md" ]; then if [ -f "UPDATELOG.md" ]; then
# Extract the section starting with ## and containing -alpha until the next ## or end of file UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
# ALPHA_LOGS=$(awk '/^## .*-alpha/{flag=1; print; next} /^## /{flag=0} flag' UPDATELOG.md) if [ -n "$UPDATE_LOGS" ]; then
ALPHA_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
if [ -n "$ALPHA_LOGS" ]; then echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "Found alpha update logs"
echo "ALPHA_LOGS<<EOF" >> $GITHUB_ENV
echo "$ALPHA_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
else else
echo "No alpha sections found in UPDATELOG.md" echo "No update sections found in UPDATELOG.md"
fi fi
else else
echo "UPDATELOG.md file not found" echo "UPDATELOG.md file not found"
@@ -156,17 +205,15 @@ jobs:
shell: bash shell: bash
- run: | - run: |
# 检查 ALPHA_LOGS 是否存在,如果不存在则使用默认消息 if [ -z "$UPDATE_LOGS" ]; then
if [ -z "$ALPHA_LOGS" ]; then echo "No update logs found, using default message"
echo "No alpha logs found, using default message" UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
ALPHA_LOGS="More new features are now supported. Check for detailed changelog soon."
else else
echo "Using found alpha logs" echo "Using found update logs"
fi fi
# 生成 release.txt 文件
cat > release.txt << EOF cat > release.txt << EOF
$ALPHA_LOGS $UPDATE_LOGS
## 我应该下载哪个版本? ## 我应该下载哪个版本?
@@ -189,7 +236,6 @@ jobs:
- arm64架构: arm64_fixed_webview2-setup.exe - arm64架构: arm64_fixed_webview2-setup.exe
### FAQ ### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html) - [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐 ### 稳定机场VPN推荐
@@ -201,14 +247,15 @@ jobs:
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: alpha tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev Alpha" name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
body_path: release.txt body_path: release.txt
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true generate_release_notes: true
alpha: alpha-x86-windows-macos-linux:
name: Alpha x86 Windows, MacOS and Linux
needs: update_tag needs: update_tag
strategy: strategy:
fail-fast: false fail-fast: false
@@ -224,7 +271,6 @@ jobs:
target: x86_64-apple-darwin target: x86_64-apple-darwin
- os: ubuntu-22.04 - os: ubuntu-22.04
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
@@ -240,8 +286,7 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true save-if: false
cache-on-failure: true
- name: Install dependencies (ubuntu only) - name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04' if: matrix.os == 'ubuntu-22.04'
@@ -262,13 +307,10 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release Alpha Version # - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-alpha-version # run: pnpm release-version ${{ env.TAG_NAME }}
- name: Patch Alpha Hash
run: pnpm fix-alpha-version
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@@ -284,15 +326,16 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with: with:
tagName: alpha tagName: ${{ env.TAG_NAME }}
releaseName: "Clash Verge Rev Alpha" releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
releaseBody: "More new features are now supported." releaseBody: "More new features are now supported."
releaseDraft: false releaseDraft: false
prerelease: true prerelease: true
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
alpha-for-linux-arm: alpha-arm-linux:
name: Alpha ARM Linux
needs: update_tag needs: update_tag
strategy: strategy:
fail-fast: false fail-fast: false
@@ -319,7 +362,7 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true save-if: false
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -334,16 +377,13 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release Alpha Version # - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-alpha-version # run: pnpm release-version ${{ env.TAG_NAME }}
- name: Patch Alpha Hash - name: Setup for linux
run: pnpm fix-alpha-version run: |
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/ sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF cat > /tmp/sources.list << EOF
@@ -377,14 +417,14 @@ jobs:
patchelf:${{ matrix.arch }} \ patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }} librsvg2-dev:${{ matrix.arch }}
- name: "Install aarch64 tools" - name: Install aarch64 tools
if: matrix.target == 'aarch64-unknown-linux-gnu' if: matrix.target == 'aarch64-unknown-linux-gnu'
run: | run: |
sudo apt install -y \ sudo apt install -y \
gcc-aarch64-linux-gnu \ gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu g++-aarch64-linux-gnu
- name: "Install armv7 tools" - name: Install armv7 tools
if: matrix.target == 'armv7-unknown-linux-gnueabihf' if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: | run: |
sudo apt install -y \ sudo apt install -y \
@@ -417,15 +457,16 @@ jobs:
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: alpha tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev Alpha" name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: | files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
alpha-for-fixed-webview2: alpha-x86-arm-windows_webview2:
name: Alpha x86 and ARM Windows with WebView2
needs: update_tag needs: update_tag
strategy: strategy:
fail-fast: false fail-fast: false
@@ -449,8 +490,7 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true save-if: false
cache-on-failure: true
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -465,13 +505,10 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release Alpha Version # - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-alpha-version # run: pnpm release-version ${{ env.TAG_NAME }}
- name: Patch Alpha Hash
run: pnpm fix-alpha-version
- name: Download WebView2 Runtime - name: Download WebView2 Runtime
run: | run: |
@@ -515,13 +552,13 @@ jobs:
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: alpha tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev Alpha" name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle - name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+36 -31
View File
@@ -8,6 +8,7 @@ on:
permissions: write-all permissions: write-all
env: env:
TAG_NAME: autobuild TAG_NAME: autobuild
TAG_CHANNEL: AutoBuild
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short RUST_BACKTRACE: short
concurrency: concurrency:
@@ -143,7 +144,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev AutoBuild" name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
body_path: release.txt body_path: release.txt
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@@ -182,7 +183,7 @@ jobs:
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true cache-all-crates: true
cache-on-failure: true save-if: ${{ github.ref == 'refs/heads/dev' }}
- name: Install dependencies (ubuntu only) - name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04' if: matrix.os == 'ubuntu-22.04'
@@ -190,23 +191,24 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf 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 - uses: pnpm/action-setup@v4
name: Install pnpm name: Install pnpm
with: with:
run_install: false run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release Autobuild Version - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild run: pnpm release-version ${{ env.TAG_NAME }}
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@@ -223,7 +225,7 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with: with:
tagName: ${{ env.TAG_NAME }} tagName: ${{ env.TAG_NAME }}
releaseName: "Clash Verge Rev AutoBuild" releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
releaseBody: "More new features are now supported." releaseBody: "More new features are now supported."
releaseDraft: false releaseDraft: false
prerelease: true prerelease: true
@@ -259,24 +261,26 @@ jobs:
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
run_install: false run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release Autobuild Version - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild run: pnpm release-version ${{ env.TAG_NAME }}
- name: Setup for linux - name: Setup for linux
run: | run: |
@@ -354,7 +358,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev AutoBuild" name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: | files: |
@@ -387,25 +391,26 @@ jobs:
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true 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 - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "22" node-version: "22"
cache: "pnpm"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release Autobuild Version - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild run: pnpm release-version ${{ env.TAG_NAME }}
- name: Download WebView2 Runtime - name: Download WebView2 Runtime
run: | run: |
@@ -450,12 +455,12 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev AutoBuild" name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle - name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --autobuild run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+63
View File
@@ -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
+64
View File
@@ -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
+2 -3
View File
@@ -42,8 +42,7 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true save-if: false
cache-on-failure: true
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -58,7 +57,7 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
+50
View File
@@ -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
+41 -9
View File
@@ -1,7 +1,16 @@
name: Release Build name: Release Build
on: on:
workflow_dispatch: # ! 为了避免重复发布版本,应当通过独特 git tag 触发。
# ! 不再使用 workflow_dispatch 触发。
# workflow_dispatch:
push:
# 应当限制在 main 分支上触发发布。
branches:
- main
# 应当限制 v*.*.* 的 tag 触发发布。
tags:
- "v*.*.*"
permissions: write-all permissions: write-all
env: env:
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
@@ -12,7 +21,28 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs: 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: release:
name: Release Build
needs: check_tag_version
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -43,8 +73,7 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true save-if: false
cache-on-failure: true
- name: Install dependencies (ubuntu only) - name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04' if: matrix.os == 'ubuntu-22.04'
@@ -65,7 +94,7 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@@ -88,6 +117,7 @@ jobs:
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
release-for-linux-arm: release-for-linux-arm:
name: Release Build for Linux ARM
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -113,7 +143,7 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true save-if: false
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -128,7 +158,7 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: "Setup for linux" - name: "Setup for linux"
run: |- run: |-
@@ -209,6 +239,7 @@ jobs:
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
release-for-fixed-webview2: release-for-fixed-webview2:
name: Release Build for Fixed WebView2
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -231,8 +262,7 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: src-tauri workspaces: src-tauri
cache-all-crates: true save-if: false
cache-on-failure: true
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -247,7 +277,7 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Download WebView2 Runtime - name: Download WebView2 Runtime
run: | run: |
@@ -303,6 +333,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update: release-update:
name: Release Update
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [release, release-for-linux-arm] needs: [release, release-for-linux-arm]
steps: steps:
@@ -353,6 +384,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget: submit-to-winget:
name: Submit to Winget
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [release-update] needs: [release-update]
steps: steps:
+17 -9
View File
@@ -2,17 +2,25 @@
#pnpm pretty-quick --staged #pnpm pretty-quick --staged
# 运行 clippy fmt if git diff --cached --name-only | grep -q '^src/'; then
cd src-tauri pnpm format:check
cargo fmt if [ $? -ne 0 ]; then
echo "Code format check failed in src/. Please fix formatting issues."
if [ $? -ne 0 ]; then exit 1
echo "rustfmt failed to format the code. Please fix the issues and try again." fi
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 exit 0
+22 -8
View File
@@ -1,13 +1,27 @@
#!/bin/bash #!/bin/bash
# 运行 clippy if git diff --cached --name-only | grep -q '^src-tauri/'; then
# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix 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 remote_name="$1"
# if [ $? -ne 0 ]; then remote_url=$(git remote get-url "$remote_name")
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
# exit 1 if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
# fi 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 exit 0
+8
View File
@@ -0,0 +1,8 @@
# README.md
# UPDATELOG.md
# CONTRIBUTING.md
pnpm-lock.yaml
src-tauri/target/
src-tauri/gen/
+6
View File
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"experimentalOperatorPosition": "start"
}
+18 -10
View File
@@ -33,12 +33,15 @@ npm install pnpm -g
``` ```
### Install Dependencies ### Install Dependencies
Install node packages Install node packages
```shell ```shell
pnpm install pnpm install
``` ```
Install apt packages ONLY for Ubuntu Install apt packages ONLY for Ubuntu
```shell ```shell
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf 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: - Automatically download it via the provided script:
```shell ```shell
pnpm run check pnpm run prebuild
# Use '--force' to force update to the latest version # 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). - 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. 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 ```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: cd src-tauri
$ clash-verge-rev/src-tauri: cargo fmt $ 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: Once you have made your changes:
1. Fork the repository. 1. Fork the repository.
+12 -3
View File
@@ -18,10 +18,18 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
## Install ## Install
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br> 请到发布页面下载对应的安装包:[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). 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 - 解锁流媒体及 ChatGPT
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6) - 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持, #### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器! 感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
🧩 YXVM 独立服务器优势: 🧩 YXVM 独立服务器优势:
- 🌎 优质网络,回程优化,下载快到飞起 - 🌎 优质网络,回程优化,下载快到飞起
- 🔧 物理机独享资源,非VPS可比,性能拉满 - 🔧 物理机独享资源,非VPS可比,性能拉满
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用 - 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
+196 -129
View File
@@ -1,143 +1,192 @@
## v2.3.1
### 🐞 修复问题
- 增加配置文件校验,修复从古老版本升级上来的"No such file or directory (os error 2)"错误
- 修复扩展脚本转义错误
- 修复 macOS Intel X86 架构构建错误导致无法运行
- 修复 Linux 下界面边框白边问题
- 修复 托盘 无响应问题
- 修复 托盘 无法从轻量模式退出并恢复窗口
- 修复 快速切换订阅可能导致的卡死问题
### ✨ 新增功能
- 新增 window-state 窗口状态管理和恢复
### 🚀 优化改进
- 优化 托盘 统一响应
- 优化 静默启动+自启动轻量模式 运行方式
- 升级依赖
## v2.3.0 ## v2.3.0
尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。 **发行代号:御**
代号释义: 「御」,象征掌控与守护,寓意本次版本对系统稳定性、安全性与用户体验的全面驾驭与提升。
#### ⚠️ 已知问题 尽管 `external-controller` 密钥现已自动补全默认值且不允许为空,**仍建议手动修改密钥以提高安全性**。
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆。
- MacOS 下卸载服务后需手动重启软件才能与内核通信。
### 2.3.0 相对于 2.2.3 ### ⚠️ 已知问题
#### 🐞 修复问题
- 首页"代理模式"快速切换导致的卡死问题
- 解锁测试报错信息
- Macos 快捷键关闭窗口无法启用自动轻量模式
- 静默启动异常窗口创建和关闭流程
- Windows 错误的全局快捷键 `Ctrl+Q` 注册
- Vless URL 解码时网络类型错误
- 切换自定义代理地址导致系统代理状态异常
- Macos TUN 默认无效网卡名称
- 托盘更改订阅后 UI 不同步的问题
- 修复提权漏洞,改用带认证的 IPC 通信
- 编辑器中连字符问题
- 安装服务模式后无法立即开启 TUN 模式
- 同步更新多语言翻译
- 修复 .window-state.json 无法删除的问题
- 无法修改配置更新 HTTP 请求超时
- 修复 getDelayFix 钩子问题
- 使用外部扩展脚本覆写代理组时首页无法显示代理组
- 导出诊断 Verge 版本与设置页面不同步
#### ✨ 新增功能 - 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
- Mihomo(Meta)内核升级至 1.19.10 - macOS:
- 允许代理主机地址设置为非 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并发)
#### 🚀 优化改进 - MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
- 系统代理 Bypass 设置 - 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题 - 彩色 Tray 图标颜色偏淡;
- 切换到规则页面时自动刷新规则数据
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 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
#### 🗑️ 移除内容 - 已确认窗口状态管理器存在上游缺陷,已暂时移除窗口大小与位置记忆功能。
- 窗口状态管理器
- 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 ## v2.2.3
#### 已知问题 #### 已知问题
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙 - 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡 - MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS - MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
### 2.2.3 相对于 2.2.2 ### 2.2.3 相对于 2.2.2
#### 修复了: #### 修复了:
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- “开机自启”和“DNS覆写”开关跳动问题 - 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- 自定义托盘图标未能应用更改 - “开机自启”和“DNS覆写”开关跳动问题
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大 - 自定义托盘图标未能应用更改
- MacOS 托盘速率显示不全 - MacOS 自定义托盘图标显示速率时图标和文本间隙过大
- Linux 在系统服务模式下无法拉起 Mihomo 内核 - MacOS 托盘速率显示不全
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃 - Linux 在系统服务模式下无法拉起 Mihomo 内核
- 相同节点名称可能导致的页面渲染出错 - 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
- URL Schemes被截断的问题 - 相同节点名称可能导致的页面渲染出错
- 首页流量统计卡更好的时间戳范围 - URL Schemes被截断的问题
- 静默启动无法触发自动轻量化计时器 - 首页流量统计卡更好的时间戳范围
- 静默启动无法触发自动轻量化计时器
#### 新增了: #### 新增了:
- Mihomo(Meta)内核升级至 1.19.4
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限 - Mihomo(Meta)内核升级至 1.19.4
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务 - Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置 - 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启 - 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
- 代理组显示节点数量 - 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
- 统一运行模式检测,支持管理员模式下开启TUN模式 - 代理组显示节点数量
- 托盘切换代理模式会根据设置自动断开之前连接 - 统一运行模式检测,支持管理员模式下开启TUN模式
- 如订阅获取失败回退使用Clash内核代理再次尝试 - 托盘切换代理模式会根据设置自动断开之前连接
- 如订阅获取失败回退使用Clash内核代理再次尝试
#### 移除了: #### 移除了:
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
#### 优化了: #### 优化了:
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 前端统一刷新应用数据,优化数据获取和刷新逻辑 - 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 优化首页流量图表代码,调整图表文字边距 - 前端统一刷新应用数据,优化数据获取和刷新逻辑
- MacOS 托盘速率更好的显示样式和更新逻辑 - 优化首页流量图表代码,调整图表文字边距
- 首页仅在有流量图表时显示流量图表区域 - MacOS 托盘速率更好的显示样式和更新逻辑
- 更新DNS默认覆写配置 - 首页仅在有流量图表时显示流量图表区域
- 移除测试目录,简化资源初始化逻辑 - 更新DNS默认覆写配置
- 移除测试目录,简化资源初始化逻辑
## v2.2.2 ## v2.2.2
@@ -148,23 +197,29 @@
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。 代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,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(已下架不再提供) ### 2.2.2 相对于 2.2.1(已下架不再提供)
#### 修复了: #### 修复了:
- 弹黑框的问题(原因是服务崩溃触发重装机制)
- MacOS进入轻量模式以后隐藏Dock图标 - 弹黑框的问题(原因是服务崩溃触发重装机制)
- 增加轻量模式缺失的tray翻译 - MacOS进入轻量模式以后隐藏Dock图标
- Linux下的窗口边框被削掉的问题 - 增加轻量模式缺失的tray翻译
- Linux下的窗口边框被削掉的问题
#### 新增了: #### 新增了:
- 加强服务检测和重装逻辑
- 增强内核与服务保活机制 - 加强服务检测和重装逻辑
- 增加服务模式下的僵尸进程清理机制 - 增强内核与服务保活机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式 - 增加服务模式下的僵尸进程清理机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
### 2.2.1 相对于 2.2.0(已下架不再提供) ### 2.2.1 相对于 2.2.0(已下架不再提供)
#### 修复了: #### 修复了:
1. **首页** 1. **首页**
- 修复 Direct 模式首页无法渲染 - 修复 Direct 模式首页无法渲染
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出 - 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
@@ -181,6 +236,7 @@
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。 - 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
#### 新增了: #### 新增了:
1. **首页** 1. **首页**
- 首页文本过长自动截断 - 首页文本过长自动截断
2. **轻量模式** 2. **轻量模式**
@@ -197,7 +253,9 @@
## 2.2.0(已下架不再提供) ## 2.2.0(已下架不再提供)
#### 新增功能 #### 新增功能
1. **首页** 1. **首页**
- 新增首页功能,默认启动页面改为首页。 - 新增首页功能,默认启动页面改为首页。
- 首页流量图卡片显示上传/下载名称。 - 首页流量图卡片显示上传/下载名称。
- 首页支持轻量模式切换。 - 首页支持轻量模式切换。
@@ -205,17 +263,21 @@
- 限制首页配置文件卡片URL长度。 - 限制首页配置文件卡片URL长度。
2. **DNS 设置与覆写** 2. **DNS 设置与覆写**
- 新增 DNS 覆写功能。 - 新增 DNS 覆写功能。
- 默认启用 DNS 覆写。 - 默认启用 DNS 覆写。
3. **解锁测试** 3. **解锁测试**
- 新增解锁测试页面。 - 新增解锁测试页面。
4. **轻量模式** 4. **轻量模式**
- 新增轻量模式及设置。 - 新增轻量模式及设置。
- 添加自动轻量模式定时器。 - 添加自动轻量模式定时器。
5. **系统支持** 5. **系统支持**
- Mihomo(meta)内核升级 1.19.3 - Mihomo(meta)内核升级 1.19.3
- macOS 支持 CMD+W 关闭窗口。 - macOS 支持 CMD+W 关闭窗口。
- 新增 macOS 应用菜单。 - 新增 macOS 应用菜单。
@@ -228,7 +290,9 @@
- 新增代理命令。 - 新增代理命令。
#### 修复 #### 修复
1. **系统** 1. **系统**
- 修复 Windows 热键崩溃。 - 修复 Windows 热键崩溃。
- 修复 macOS 无框标题。 - 修复 macOS 无框标题。
- 修复 macOS 静默启动崩溃。 - 修复 macOS 静默启动崩溃。
@@ -241,7 +305,9 @@
- 修复构建失败问题。 - 修复构建失败问题。
#### 优化 #### 优化
1. **性能** 1. **性能**
- 重构后端,巨幅性能优化。 - 重构后端,巨幅性能优化。
- 优化首页组件性能。 - 优化首页组件性能。
- 优化流量图表资源使用。 - 优化流量图表资源使用。
@@ -254,6 +320,7 @@
- 优化修改verge配置性能。 - 优化修改verge配置性能。
2. **重构** 2. **重构**
- 重构后端,巨幅性能优化。 - 重构后端,巨幅性能优化。
- 优化定时器管理。 - 优化定时器管理。
- 重构 MihomoManager 处理流量。 - 重构 MihomoManager 处理流量。
+4
View File
@@ -0,0 +1,4 @@
files:
- source: /src/locales/en.json
translation: /src/locales
multilingual: 1
+45 -44
View File
@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "2.3.0", "version": "2.3.1",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
@@ -11,17 +11,18 @@
"web:dev": "vite", "web:dev": "vite",
"web:build": "tsc --noEmit && vite build", "web:build": "tsc --noEmit && vite build",
"web:serve": "vite preview", "web:serve": "vite preview",
"check": "node scripts/check.mjs", "prebuild": "node scripts/prebuild.mjs",
"updater": "node scripts/updater.mjs", "updater": "node scripts/updater.mjs",
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs", "updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
"portable": "node scripts/portable.mjs", "portable": "node scripts/portable.mjs",
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs", "portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"fix-alpha-version": "node scripts/fix-alpha_version.mjs", "fix-alpha-version": "node scripts/fix-alpha_version.mjs",
"release-version": "node scripts/release-version.mjs", "release-version": "node scripts/release-version.mjs",
"release-alpha-version": "node scripts/release-alpha_version.mjs", "publish-version": "node scripts/publish-version.mjs",
"prepare": "husky",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml", "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": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
@@ -30,77 +31,77 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.0.2", "@mui/icons-material": "^7.1.1",
"@mui/lab": "7.0.0-beta.12", "@mui/lab": "7.0.0-beta.13",
"@mui/material": "^7.0.2", "@mui/material": "^7.1.1",
"@mui/x-data-grid": "^8.2.0", "@mui/x-data-grid": "^8.5.2",
"@tauri-apps/api": "2.5.0", "@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2", "@tauri-apps/plugin-clipboard-manager": "^2.2.3",
"@tauri-apps/plugin-dialog": "^2.2.1", "@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.2.1", "@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.0", "@tauri-apps/plugin-global-shortcut": "^2.2.1",
"@tauri-apps/plugin-notification": "^2.2.2", "@tauri-apps/plugin-notification": "^2.2.3",
"@tauri-apps/plugin-process": "^2.2.1", "@tauri-apps/plugin-process": "^2.2.2",
"@tauri-apps/plugin-shell": "2.2.1", "@tauri-apps/plugin-shell": "2.2.2",
"@tauri-apps/plugin-updater": "2.7.1", "@tauri-apps/plugin-updater": "2.8.1",
"@tauri-apps/plugin-window-state": "^2.2.2", "@tauri-apps/plugin-window-state": "^2.2.3",
"@types/d3-shape": "^3.1.7", "@types/d3-shape": "^3.1.7",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.8.4", "ahooks": "^3.8.5",
"axios": "^1.8.3", "axios": "^1.10.0",
"chart.js": "^4.4.9", "chart.js": "^4.5.0",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"foxact": "^0.2.44", "foxact": "^0.2.49",
"glob": "^11.0.1", "glob": "^11.0.3",
"i18next": "^25.0.2", "i18next": "^25.2.1",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"monaco-yaml": "^5.3.1", "monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"peggy": "^5.0.0", "peggy": "^5.0.3",
"react": "19.1.0", "react": "19.1.0",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "6.0.0", "react-error-boundary": "6.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.58.1",
"react-i18next": "15.5.1", "react-i18next": "15.5.3",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-monaco-editor": "0.58.0", "react-monaco-editor": "0.58.0",
"react-router-dom": "7.6.0", "react-router-dom": "7.6.2",
"react-virtuoso": "^4.12.7", "react-virtuoso": "^4.13.0",
"sockette": "^2.0.6", "sockette": "^2.0.6",
"swr": "^2.3.3", "swr": "^2.3.3",
"tar": "^7.4.3", "tar": "^7.4.3",
"types-pac": "^1.0.3", "types-pac": "^1.0.3",
"zustand": "^5.0.3" "zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^6.0.0", "@actions/github": "^6.0.1",
"@tauri-apps/cli": "2.2.7", "@tauri-apps/cli": "2.5.0",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/react": "19.1.4", "@types/react": "19.1.8",
"@types/react-dom": "19.1.5", "@types/react-dom": "19.1.6",
"@vitejs/plugin-legacy": "^6.0.2", "@vitejs/plugin-legacy": "^6.1.1",
"@vitejs/plugin-react": "4.4.1", "@vitejs/plugin-react": "4.5.2",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"commander": "^14.0.0", "commander": "^14.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"husky": "^9.1.7", "husky": "^9.1.7",
"meta-json-schema": "^1.19.3", "meta-json-schema": "^1.19.10",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"pretty-quick": "^4.1.1", "pretty-quick": "^4.2.2",
"sass": "^1.86.0", "sass": "^1.89.2",
"terser": "^5.39.0", "terser": "^5.43.0",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"vite": "^6.2.2", "vite": "^6.3.5",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.0" "vite-plugin-svgr": "^4.3.0"
}, },
@@ -112,4 +113,4 @@
}, },
"type": "module", "type": "module",
"packageManager": "pnpm@9.13.2" "packageManager": "pnpm@9.13.2"
} }
+719 -744
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -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"]
}
+31 -27
View File
@@ -1,21 +1,21 @@
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.resolve(__dirname, '../src/locales'); const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const SRC_DIRS = [ const SRC_DIRS = [
path.resolve(__dirname, '../src'), path.resolve(__dirname, "../src"),
path.resolve(__dirname, '../src-tauri') 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) { function getAllFiles(dir, exts) {
let files = []; let files = [];
fs.readdirSync(dir).forEach(file => { fs.readdirSync(dir).forEach((file) => {
const full = path.join(dir, file); const full = path.join(dir, file);
if (fs.statSync(full).isDirectory()) { if (fs.statSync(full).isDirectory()) {
files = files.concat(getAllFiles(full, exts)); files = files.concat(getAllFiles(full, exts));
@@ -28,21 +28,21 @@ function getAllFiles(dir, exts) {
// 读取所有源码内容为一个大字符串 // 读取所有源码内容为一个大字符串
function getAllSourceContent() { function getAllSourceContent() {
const files = SRC_DIRS.flatMap(dir => getAllFiles(dir, exts)); const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
return files.map(f => fs.readFileSync(f, 'utf8')).join('\n'); return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
} }
// 白名单 key,不检查这些 key 是否被使用 // 白名单 key,不检查这些 key 是否被使用
const WHITELIST_KEYS = [ const WHITELIST_KEYS = [
'theme.light', "theme.light",
'theme.dark', "theme.dark",
'theme.system', "theme.system",
"Already Using Latest Core Version" "Already Using Latest Core Version",
]; ];
// 主流程 // 主流程
function processI18nFile(i18nPath, lang, allSource) { 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 keys = Object.keys(i18n);
const used = {}; const used = {};
@@ -50,7 +50,7 @@ function processI18nFile(i18nPath, lang, allSource) {
let checked = 0; let checked = 0;
const total = keys.length; const total = keys.length;
keys.forEach(key => { keys.forEach((key) => {
if (WHITELIST_KEYS.includes(key)) { if (WHITELIST_KEYS.includes(key)) {
used[key] = i18n[key]; used[key] = i18n[key];
} else { } else {
@@ -65,8 +65,10 @@ function processI18nFile(i18nPath, lang, allSource) {
checked++; checked++;
if (checked % 20 === 0 || checked === total) { if (checked % 20 === 0 || checked === total) {
const percent = ((checked / total) * 100).toFixed(1); const percent = ((checked / total) * 100).toFixed(1);
process.stdout.write(`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`); process.stdout.write(
if (checked === total) process.stdout.write('\n'); `\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); console.log(`\n[${lang}] Unused keys:`, unused);
// 备份原文件 // 备份原文件
const oldPath = i18nPath + '.old'; const oldPath = i18nPath + ".old";
fs.renameSync(i18nPath, oldPath); fs.renameSync(i18nPath, oldPath);
// 写入精简后的 i18n 文件(保留原文件名) // 写入精简后的 i18n 文件(保留原文件名)
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), 'utf8'); 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}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
);
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`); console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
} }
function main() { function main() {
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等 // 支持 zhtw.json、zh-tw.json、zh_CN.json 等
const files = fs.readdirSync(LOCALES_DIR).filter(f => const files = fs
/^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith('.old') .readdirSync(LOCALES_DIR)
); .filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
const allSource = getAllSourceContent(); const allSource = getAllSourceContent();
files.forEach(file => { files.forEach((file) => {
const lang = path.basename(file, '.json'); const lang = path.basename(file, ".json");
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource); processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
}); });
} }
main(); main();
+66
View File
@@ -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
View File
@@ -1,5 +1,3 @@
/** /**
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json. * 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 fs from "fs/promises";
import path from "path"; import path from "path";
import { program } from "commander"; import { program } from "commander";
import { execSync } from "child_process";
/** /**
* 生成短时间戳(格式:YYMMDDHHMM) * 获取当前 git 短 commit hash
* @returns {string} * @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 now = new Date();
const year = String(now.getFullYear()).slice(-2);
const month = String(now.getMonth() + 1).padStart(2, "0"); const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0"); const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0"); if (withCommit) {
const minutes = String(now.getMinutes()).padStart(2, "0"); const gitShort = getGitShortCommit();
return `${year}${month}${day}${hours}${minutes}`; return `${month}${day}.${gitShort}`;
}
return `${month}${day}`;
} }
/** /**
@@ -51,7 +65,9 @@ function generateShortTimestamp() {
* @returns {boolean} * @returns {boolean}
*/ */
function isValidVersion(version) { 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} * @returns {string}
*/ */
function getBaseVersion(version) { function getBaseVersion(version) {
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, ''); let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, ''); base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
return base; return base;
} }
@@ -85,10 +101,21 @@ async function updatePackageVersion(newVersion) {
const data = await fs.readFile(packageJsonPath, "utf8"); const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data); const packageJson = JSON.parse(data);
console.log("[INFO]: Current package.json version is: ", packageJson.version); console.log(
packageJson.version = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; "[INFO]: Current package.json version is: ",
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf8"); packageJson.version,
console.log(`[INFO]: package.json version updated to: ${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) { } catch (error) {
console.error("Error updating package.json version:", error); console.error("Error updating package.json version:", error);
throw error; throw error;
@@ -105,12 +132,17 @@ async function updateCargoVersion(newVersion) {
try { try {
const data = await fs.readFile(cargoTomlPath, "utf8"); const data = await fs.readFile(cargoTomlPath, "utf8");
const lines = data.split("\n"); 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 baseVersion = getBaseVersion(versionWithoutV);
const updatedLines = lines.map((line) => { const updatedLines = lines.map((line) => {
if (line.trim().startsWith("version =")) { if (line.trim().startsWith("version =")) {
return line.replace(/version\s*=\s*"[^"]+"/, `version = "${baseVersion}"`); return line.replace(
/version\s*=\s*"[^"]+"/,
`version = "${baseVersion}"`,
);
} }
return line; return line;
}); });
@@ -133,12 +165,21 @@ async function updateTauriConfigVersion(newVersion) {
try { try {
const data = await fs.readFile(tauriConfigPath, "utf8"); const data = await fs.readFile(tauriConfigPath, "utf8");
const tauriConfig = JSON.parse(data); 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); 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; 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}`); console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
} catch (error) { } catch (error) {
console.error("Error updating tauri.conf.json version:", error); console.error("Error updating tauri.conf.json version:", error);
@@ -180,8 +221,8 @@ async function main(versionArg) {
const baseVersion = getBaseVersion(currentVersion); const baseVersion = getBaseVersion(currentVersion);
if (versionArg.toLowerCase() === "autobuild") { if (versionArg.toLowerCase() === "autobuild") {
const timestamp = generateShortTimestamp(); // 格式: 2.3.0+autobuild.250613.cc39b27
newVersion = `${baseVersion}+autobuild.${timestamp}`; newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
} else { } else {
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`; newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
} }
@@ -210,4 +251,3 @@ program
.argument("<version>", "version tag or full version") .argument("<version>", "version tag or full version")
.action(main) .action(main)
.parse(process.argv); .parse(process.argv);
+175 -159
View File
@@ -1042,7 +1042,7 @@ dependencies = [
[[package]] [[package]]
name = "clash-verge" name = "clash-verge"
version = "2.3.0" version = "2.3.1"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"aes-gcm", "aes-gcm",
@@ -1057,7 +1057,7 @@ dependencies = [
"dunce", "dunce",
"futures", "futures",
"gethostname 1.0.2", "gethostname 1.0.2",
"getrandom 0.3.2", "getrandom 0.3.3",
"hex", "hex",
"hmac", "hmac",
"image", "image",
@@ -1100,8 +1100,8 @@ dependencies = [
"tauri-plugin-window-state", "tauri-plugin-window-state",
"tempfile", "tempfile",
"tokio", "tokio",
"tokio-tungstenite 0.26.2", "tokio-tungstenite 0.27.0",
"tungstenite 0.26.2", "tungstenite 0.27.0",
"users", "users",
"warp", "warp",
"winapi", "winapi",
@@ -1337,21 +1337,6 @@ dependencies = [
"libc", "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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@@ -1666,7 +1651,7 @@ dependencies = [
"tonic-health", "tonic-health",
"tonic-web", "tonic-web",
"tower 0.4.13", "tower 0.4.13",
"tower-http", "tower-http 0.4.4",
"tower-layer", "tower-layer",
"tracing", "tracing",
"tracing-core", "tracing-core",
@@ -2100,11 +2085,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.0" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys",
"miniz_oxide", "miniz_oxide",
] ]
@@ -2462,9 +2448,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@@ -2587,9 +2573,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]] [[package]]
name = "global-hotkey" name = "global-hotkey"
version = "0.6.4" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fbb3a4e56c901ee66c190fdb3fa08344e6d09593cc6c61f8eb9add7144b271" checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"keyboard-types", "keyboard-types",
@@ -2599,7 +2585,8 @@ dependencies = [
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.12",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"x11-dl", "x11rb",
"xkeysym",
] ]
[[package]] [[package]]
@@ -2939,7 +2926,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa 1.0.15", "itoa 1.0.15",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.8", "socket2 0.5.10",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -2981,7 +2968,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
"webpki-roots", "webpki-roots 0.26.8",
] ]
[[package]] [[package]]
@@ -3014,21 +3001,28 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.10" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [ dependencies = [
"base64 0.22.1",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core",
"futures-util", "futures-util",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
"hyper 1.6.0", "hyper 1.6.0",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.8", "socket2 0.5.10",
"system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
"windows-registry",
] ]
[[package]] [[package]]
@@ -3380,6 +3374,16 @@ dependencies = [
"regex", "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]] [[package]]
name = "is-docker" name = "is-docker"
version = "0.2.0" version = "0.2.0"
@@ -3583,9 +3587,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.173"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
@@ -3607,6 +3611,26 @@ dependencies = [
"winapi", "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]] [[package]]
name = "libm" name = "libm"
version = "0.2.11" version = "0.2.11"
@@ -3624,6 +3648,15 @@ dependencies = [
"redox_syscall", "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]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.3.8" version = "0.3.8"
@@ -3668,9 +3701,9 @@ dependencies = [
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"scopeguard", "scopeguard",
@@ -3743,27 +3776,6 @@ dependencies = [
"hashbrown 0.15.2", "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]] [[package]]
name = "mac" name = "mac"
version = "0.1.1" version = "0.1.1"
@@ -4777,9 +4789,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.3" version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [ dependencies = [
"lock_api", "lock_api",
"parking_lot_core", "parking_lot_core",
@@ -4787,9 +4799,9 @@ dependencies = [
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.9.10" version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@@ -5381,7 +5393,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2 0.5.8", "socket2 0.5.10",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tracing", "tracing",
@@ -5395,7 +5407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
dependencies = [ dependencies = [
"bytes", "bytes",
"getrandom 0.3.2", "getrandom 0.3.3",
"rand 0.9.0", "rand 0.9.0",
"ring", "ring",
"rustc-hash", "rustc-hash",
@@ -5417,7 +5429,7 @@ dependencies = [
"cfg_aliases 0.2.1", "cfg_aliases 0.2.1",
"libc", "libc",
"once_cell", "once_cell",
"socket2 0.5.8", "socket2 0.5.10",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -5527,7 +5539,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.3",
] ]
[[package]] [[package]]
@@ -5727,9 +5739,9 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.15" version = "0.12.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -5746,36 +5758,32 @@ dependencies = [
"hyper-rustls", "hyper-rustls",
"hyper-tls", "hyper-tls",
"hyper-util", "hyper-util",
"ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"native-tls", "native-tls",
"once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
"rustls", "rustls",
"rustls-pemfile",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls", "tokio-rustls",
"tokio-util", "tokio-util",
"tower 0.5.2", "tower 0.5.2",
"tower-http 0.6.6",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots 1.0.0",
"windows-registry 0.4.0",
] ]
[[package]] [[package]]
@@ -5967,15 +5975,6 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.11.0" version = "1.11.0"
@@ -6521,9 +6520,9 @@ dependencies = [
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.8" version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -6708,9 +6707,9 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.35.0" version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422" checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
dependencies = [ dependencies = [
"libc", "libc",
"memchr", "memchr",
@@ -6978,8 +6977,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-autostart" name = "tauri-plugin-autostart"
version = "2.3.0" version = "2.4.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#9bc4b2230ebb32bd30a4c0c2a21077829a729193" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9641831518c56775a364a8750e0eed8852adee87e0f11006d043b9ebba0bf5"
dependencies = [ dependencies = [
"auto-launch", "auto-launch",
"serde", "serde",
@@ -6991,9 +6991,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-clipboard-manager" name = "tauri-plugin-clipboard-manager"
version = "2.2.2" version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab4cb42fdf745229b768802e9180920a4be63122cf87ed1c879103f7609d98e" checksum = "11fa4f17a6d380490597f7632aca40b65d379cb374cb92bd9d80f333309b7fd7"
dependencies = [ dependencies = [
"arboard", "arboard",
"log", "log",
@@ -7006,9 +7006,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-deep-link" name = "tauri-plugin-deep-link"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba4412f30eaff6f5d210e20383c2d6835593977402092e95b72497a4f8632fa" checksum = "e4976ac728ebc0487515aa956cfdf200abcc52b784e441493fc544bc6ce369c8"
dependencies = [ dependencies = [
"dunce", "dunce",
"rust-ini", "rust-ini",
@@ -7020,7 +7020,7 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
"tracing", "tracing",
"url", "url",
"windows-registry 0.5.1", "windows-registry",
"windows-result 0.3.2", "windows-result 0.3.2",
] ]
@@ -7053,9 +7053,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.2.1" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcaf6e5d6062423a0f711a23c2a573ccba222b6a16a9322d8499928f27e41376" checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@@ -7071,9 +7071,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682" checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce", "dunce",
@@ -7089,14 +7089,13 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
"toml", "toml",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
name = "tauri-plugin-global-shortcut" name = "tauri-plugin-global-shortcut"
version = "2.2.0" version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f646a09511e8d283267dcdaa08c2ef27c4116bf271d9114849d9ca215606c3" checksum = "31919f3c07bcb585afef217c0c33cde80da9ebccf5b8e2c90e0e0a535b14ab47"
dependencies = [ dependencies = [
"global-hotkey", "global-hotkey",
"log", "log",
@@ -7109,9 +7108,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-process" name = "tauri-plugin-process"
version = "2.2.1" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57da5888533e802b6206b9685091f8714aa1f5266dc80051a82388449558b773" checksum = "4d870adae9408be585abd56eade2b5def2660339512b7c8de5ddf21238b67a34"
dependencies = [ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
@@ -7119,9 +7118,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-shell"
version = "2.2.1" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d5eb3368b959937ad2aeaf6ef9a8f5d11e01ffe03629d3530707bbcb27ff5d" checksum = "d34e525a448b80ad5d906fcbd93838ac3ba37985b29ac699a045b5da9b0a1a22"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"log", "log",
@@ -7140,9 +7139,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-updater" name = "tauri-plugin-updater"
version = "2.7.1" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f05c38afd77a4b8fd98e8fb6f1cdbb5fbb8a46ba181eb2758b05321e3c6209" checksum = "b068673e9037376ca9906f99b00ae5f9e6eb62f456f900b4435c38d57cfa73e4"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"dirs 6.0.0", "dirs 6.0.0",
@@ -7166,15 +7165,15 @@ dependencies = [
"time", "time",
"tokio", "tokio",
"url", "url",
"windows-sys 0.59.0", "windows-sys 0.60.2",
"zip", "zip",
] ]
[[package]] [[package]]
name = "tauri-plugin-window-state" name = "tauri-plugin-window-state"
version = "2.2.2" version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27a3fe49de72adbe0d84aee33c89a0b059722cd0b42aaeab29eaaee7f7535cd" checksum = "136e5ce5e61edc8eeeaca70080811bbdcdd890cac9c4070cb4db9cc3de1da449"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"log", "log",
@@ -7285,12 +7284,12 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.19.1" version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [ dependencies = [
"fastrand 2.3.0", "fastrand 2.3.0",
"getrandom 0.3.2", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix 1.0.3", "rustix 1.0.3",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@@ -7514,9 +7513,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.44.2" version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -7525,7 +7524,7 @@ dependencies = [
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2 0.5.8", "socket2 0.5.10",
"tokio-macros", "tokio-macros",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -7597,14 +7596,14 @@ dependencies = [
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.26.2" version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
"tokio", "tokio",
"tungstenite 0.26.2", "tungstenite 0.27.0",
] ]
[[package]] [[package]]
@@ -7730,7 +7729,7 @@ dependencies = [
"pin-project", "pin-project",
"tokio-stream", "tokio-stream",
"tonic", "tonic",
"tower-http", "tower-http 0.4.4",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -7789,6 +7788,24 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.3" version = "0.3.3"
@@ -7937,9 +7954,9 @@ dependencies = [
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.26.2" version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
dependencies = [ dependencies = [
"bytes", "bytes",
"data-encoding", "data-encoding",
@@ -8144,7 +8161,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.3",
"serde", "serde",
] ]
@@ -8514,6 +8531,15 @@ dependencies = [
"rustls-pki-types", "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]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.37.0" version = "0.37.0"
@@ -8761,17 +8787,6 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "windows-registry" name = "windows-registry"
version = "0.5.1" version = "0.5.1"
@@ -8811,15 +8826,6 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.4.0" version = "0.4.0"
@@ -8865,6 +8871,15 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.42.2" version = "0.42.2"
@@ -8913,9 +8928,9 @@ dependencies = [
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.53.0" version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm 0.53.0", "windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0", "windows_aarch64_msvc 0.53.0",
@@ -9313,21 +9328,18 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.25" version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" 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]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.5" version = "0.7.5"
@@ -9514,32 +9526,36 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "2.6.1" version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0"
dependencies = [ dependencies = [
"aes", "aes",
"arbitrary", "arbitrary",
"bzip2", "bzip2",
"constant_time_eq", "constant_time_eq",
"crc32fast", "crc32fast",
"crossbeam-utils",
"deflate64", "deflate64",
"flate2", "flate2",
"getrandom 0.3.2", "getrandom 0.3.3",
"hmac", "hmac",
"indexmap 2.8.0", "indexmap 2.8.0",
"lzma-rs", "liblzma",
"memchr", "memchr",
"pbkdf2", "pbkdf2",
"sha1", "sha1",
"time", "time",
"xz2",
"zeroize", "zeroize",
"zopfli", "zopfli",
"zstd", "zstd",
] ]
[[package]]
name = "zlib-rs"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
[[package]] [[package]]
name = "zopfli" name = "zopfli"
version = "0.8.1" version = "0.8.1"
+28 -28
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "2.3.0" version = "2.3.1"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"] authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" 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 = [] } tauri-build = { version = "2.2.0", features = [] }
[dependencies] [dependencies]
warp = "0.3" warp = "0.3.7"
anyhow = "1.0.98" anyhow = "1.0.98"
dirs = "6.0" dirs = "6.0"
open = "5.3" open = "5.3.2"
log = "0.4" log = "0.4.27"
dunce = "1.0" dunce = "1.0.5"
log4rs = "1" log4rs = "1.3.0"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4.41" chrono = "0.4.41"
sysinfo = "0.35.0" sysinfo = "0.35.2"
boa_engine = "0.20.0" boa_engine = "0.20.0"
serde_json = "1.0.140" serde_json = "1.0.140"
serde_yaml = "0.9.34" serde_yaml = "0.9.34-deprecated"
once_cell = "1.21.3" once_cell = "1.21.3"
lazy_static = "1.5.0" lazy_static = "1.5.0"
port_scanner = "0.1.5" port_scanner = "0.1.5"
delay_timer = "0.11.6" delay_timer = "0.11.6"
parking_lot = "0.12.3" parking_lot = "0.12.4"
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
tokio = { version = "1.44.2", features = [ tokio = { version = "1.45.1", features = [
"rt-multi-thread", "rt-multi-thread",
"macros", "macros",
"time", "time",
"sync", "sync",
] } ] }
serde = { version = "1.0.219", features = ["derive"] } 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" regex = "1.11.1"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
image = "0.25.6" image = "0.25.6"
@@ -55,27 +55,27 @@ tauri = { version = "2.5.1", features = [
"image-png", "image-png",
] } ] }
network-interface = { version = "2.0.1", features = ["serde"] } network-interface = { version = "2.0.1", features = ["serde"] }
tauri-plugin-shell = "2.2.1" tauri-plugin-shell = "2.2.2"
tauri-plugin-dialog = "2.2.1" tauri-plugin-dialog = "2.2.2"
tauri-plugin-fs = "2.2.1" tauri-plugin-fs = "2.3.0"
tauri-plugin-process = "2.2.1" tauri-plugin-process = "2.2.2"
tauri-plugin-clipboard-manager = "2.2.2" tauri-plugin-clipboard-manager = "2.2.3"
tauri-plugin-deep-link = "2.2.1" tauri-plugin-deep-link = "2.3.0"
tauri-plugin-devtools = "2.0.0" tauri-plugin-devtools = "2.0.0"
tauri-plugin-window-state = "2.2.2" tauri-plugin-window-state = "2.2.3"
zip = "2.6.1" zip = "4.1.0"
reqwest_dav = "0.2.1" reqwest_dav = "0.2.1"
aes-gcm = { version = "0.10.3", features = ["std"] } aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1" base64 = "0.22.1"
getrandom = "0.3.2" getrandom = "0.3.3"
tokio-tungstenite = "0.26.2" tokio-tungstenite = "0.27.0"
futures = "0.3.31" futures = "0.3.31"
sys-locale = "0.3.2" sys-locale = "0.3.2"
async-trait = "0.1.88" async-trait = "0.1.88"
mihomo_api = { path = "src_crates/crate_mihomo_api" } mihomo_api = { path = "src_crates/crate_mihomo_api" }
ab_glyph = "0.2.29" ab_glyph = "0.2.29"
tungstenite = "0.26.2" tungstenite = "0.27.0"
libc = "0.2.172" libc = "0.2.173"
gethostname = "1.0.2" gethostname = "1.0.2"
hmac = "0.12.1" hmac = "0.12.1"
sha2 = "0.10.9" sha2 = "0.10.9"
@@ -85,7 +85,7 @@ hex = "0.4.3"
runas = "=1.2.0" runas = "=1.2.0"
deelevate = "0.2.0" deelevate = "0.2.0"
winreg = "0.55.0" winreg = "0.55.0"
winapi = { version = "0.3", features = [ winapi = { version = "0.3.9", features = [
"winbase", "winbase",
"fileapi", "fileapi",
"winnt", "winnt",
@@ -99,9 +99,9 @@ winapi = { version = "0.3", features = [
users = "0.11.0" users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [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-autostart = "2.4.0"
tauri-plugin-global-shortcut = "2.2.0" tauri-plugin-global-shortcut = "2.2.1"
tauri-plugin-updater = "2.7.1" tauri-plugin-updater = "2.8.1"
[features] [features]
default = ["custom-protocol"] default = ["custom-protocol"]
@@ -136,7 +136,7 @@ name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[dev-dependencies] [dev-dependencies]
tempfile = "3.19.1" tempfile = "3.20.0"
[workspace] [workspace]
members = ["src_crates/crate_mihomo_api"] 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"
]
}
+4 -4
View File
@@ -916,9 +916,9 @@ FunctionEnd
!macroend !macroend
Section Uninstall Section Uninstall
;删除 .window-state.json 文件 ;删除 window-state.json 文件
SetShellVarContext current 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 CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses !insertmacro CheckAllVergeProcesses
@@ -1015,9 +1015,9 @@ Section Uninstall
RmDir /r "$LOCALAPPDATA\${BUNDLEID}" RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
${EndIf} ${EndIf}
;删除 .window-state.json 文件 ;删除 window-state.json 文件
SetShellVarContext current 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 ${GetOptions} $CMDLINE "/P" $R0
IfErrors +2 0 IfErrors +2 0
-1
View File
@@ -11,4 +11,3 @@ merge_derives = true
use_try_shorthand = false use_try_shorthand = false
use_field_init_shorthand = false use_field_init_shorthand = false
force_explicit_abi = true force_explicit_abi = true
imports_granularity = "Crate"
+81 -26
View File
@@ -12,45 +12,89 @@ use tokio::sync::Mutex;
// 添加全局互斥锁防止并发配置更新 // 添加全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(()); static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
/// 获取配置文件列表 /// 获取配置文件避免锁竞争
#[tauri::command] #[tauri::command]
pub async fn get_profiles() -> CmdResult<IProfiles> { pub async fn get_profiles() -> CmdResult<IProfiles> {
let profiles_result = tokio::time::timeout( // 策略1: 尝试快速获取latest数据
Duration::from_secs(3), // 3秒超时 let latest_result = tokio::time::timeout(
tokio::task::spawn_blocking(move || Config::profiles().data().clone()), 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; .await;
match profiles_result { match latest_result {
Ok(Ok(profiles)) => Ok(profiles), Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => { Ok(Err(join_err)) => {
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err); logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
Ok(IProfiles {
current: None,
items: Some(vec![]),
})
} }
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!( logging!(
error, error,
Type::Cmd, Type::Cmd,
true, true,
"获取配置列表超时(3秒),可能存在锁竞争" "获取draft配置任务失败: {}",
join_err
); );
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await { }
Ok(profiles) => { Err(_) => {
logging!(info, Type::Cmd, true, "使用latest()成功获取配置"); logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
Ok(profiles) }
} }
Err(_) => {
logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置"); // 策略3: fallback,尝试重新创建配置
Ok(IProfiles { logging!(
current: None, warn,
items: Some(vec![]), 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] #[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult { pub async fn delete_profile(index: String) -> CmdResult {
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?; let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
// 删除后自动清理冗余文件
let _ = Config::profiles().latest().auto_cleanup();
if should_update { if should_update {
wrap_err!(CoreManager::global().update_config().await)?; wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
@@ -242,6 +290,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
Config::profiles().apply(); Config::profiles().apply();
handle::Handle::refresh_clash(); 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 { crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Tray::global().update_tooltip() { if let Err(e) = Tray::global().update_tooltip() {
log::warn!(target: "app", "异步更新托盘提示失败: {}", e); log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
+77 -49
View File
@@ -1,71 +1,99 @@
use super::CmdResult; use super::CmdResult;
use crate::module::mihomo::MihomoManager; use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::{ use std::{
sync::atomic::{AtomicBool, Ordering}, sync::Mutex,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tauri::Manager;
static LAST_REFRESH_TIME: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None)); const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3);
static IS_REFRESHING: AtomicBool = AtomicBool::new(false); const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
const REFRESH_INTERVAL: Duration = Duration::from_secs(5);
#[tauri::command] #[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> { pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global(); let manager = MihomoManager::global();
manager let app_handle = handle::Handle::global().app_handle().unwrap();
.refresh_proxies() let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
.await
.map(|_| manager.get_proxies()) let should_refresh = {
.or_else(|_| Ok(manager.get_proxies())) 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] #[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> { pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global(); let app_handle = handle::Handle::global().app_handle().unwrap();
let cached_data = manager.get_providers_proxies(); 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 should_refresh = {
let last_refresh = LAST_REFRESH_TIME.lock(); let mut state = cmd_proxy_state.lock().unwrap();
match *last_refresh { let now = Instant::now();
Some(last_time) => last_time.elapsed() > REFRESH_INTERVAL, if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL {
None => true, state.need_refresh = true;
state.last_refresh_time = now;
} }
state.need_refresh
}; };
if should_refresh && !IS_REFRESHING.load(Ordering::Acquire) { if should_refresh {
IS_REFRESHING.store(true, Ordering::Release); let manager = MihomoManager::global();
let providers = manager.get_providers_proxies().await?;
crate::process::AsyncHandler::spawn(|| async move { {
let manager = MihomoManager::global(); let mut state = cmd_proxy_state.lock().unwrap();
match manager.refresh_providers_proxies().await { state.providers_proxies = Box::new(providers);
Ok(_) => { state.need_refresh = false;
log::debug!(target: "app", "providers_proxies静默后台刷新成功"); }
} 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);
});
} }
Ok(safe_data) let providers_proxies = {
let state = cmd_proxy_state.lock().unwrap();
state.providers_proxies.clone()
};
Ok(*providers_proxies)
} }
+64 -15
View File
@@ -1,5 +1,11 @@
use super::CmdResult; 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; use std::fs;
/// 保存profiles的配置 /// 保存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()))?; wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
let file_path_str = file_path.to_string_lossy().to_string(); let file_path_str = file_path.to_string_lossy().to_string();
println!( logging!(
info,
Type::Config,
true,
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}", "[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
file_path_str, is_merge_file file_path_str,
is_merge_file
); );
// 对于 merge 文件,只进行语法验证,不进行后续内核验证 // 对于 merge 文件,只进行语法验证,不进行后续内核验证
if is_merge_file { if is_merge_file {
println!("[cmd配置save] 检测到merge文件,只进行语法验证"); logging!(
info,
Type::Config,
true,
"[cmd配置save] 检测到merge文件,只进行语法验证"
);
match CoreManager::global() match CoreManager::global()
.validate_config_file(&file_path_str, Some(true)) .validate_config_file(&file_path_str, Some(true))
.await .await
{ {
Ok((true, _)) => { Ok((true, _)) => {
println!("[cmd配置save] merge文件语法验证通过"); logging!(
info,
Type::Config,
true,
"[cmd配置save] merge文件语法验证通过"
);
// 成功后尝试更新整体配置 // 成功后尝试更新整体配置
if let Err(e) = CoreManager::global().update_config().await { if let Err(e) = CoreManager::global().update_config().await {
println!("[cmd配置save] 更新整体配置时发生错误: {}", e); logging!(
log::warn!(target: "app", "更新整体配置时发生错误: {}", e); warn,
Type::Config,
true,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
} }
return Ok(()); return Ok(());
} }
Ok((false, error_msg)) => { 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))?; 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(()); return Ok(());
} }
Err(e) => { Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e); logging!(
error,
Type::Config,
true,
"[cmd配置save] 验证过程发生错误: {}",
e
);
// 恢复原始配置文件 // 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?; wrap_err!(fs::write(&file_path, original_content))?;
return Err(e.to_string()); return Err(e.to_string());
@@ -71,11 +108,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
.await .await
{ {
Ok((true, _)) => { Ok((true, _)) => {
println!("[cmd配置save] 验证成功"); logging!(info, Type::Config, true, "[cmd配置save] 验证成功");
Ok(()) Ok(())
} }
Ok((false, error_msg)) => { 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))?; 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) || (!file_path_str.ends_with(".js") && !is_script_error)
{ {
// 普通YAML错误使用YAML通知处理 // 普通YAML错误使用YAML通知处理
println!("[cmd配置save] YAML配置文件验证失败,发送通知"); log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
let result = (false, error_msg.clone()); let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件"); crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error { } else if is_script_error {
// 脚本错误使用专门的通知处理 // 脚本错误使用专门的通知处理
println!("[cmd配置save] 脚本文件验证失败,发送通知"); log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
let result = (false, error_msg.clone()); let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件"); crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else { } else {
// 普通配置错误使用一般通知 // 普通配置错误使用一般通知
println!("[cmd配置save] 其他类型验证失败,发送一般通知"); log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
handle::Handle::notice_message("config_validate::error", &error_msg); handle::Handle::notice_message("config_validate::error", &error_msg);
} }
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e); logging!(
error,
Type::Config,
true,
"[cmd配置save] 验证过程发生错误: {}",
e
);
// 恢复原始配置文件 // 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?; wrap_err!(fs::write(&file_path, original_content))?;
Err(e.to_string()) Err(e.to_string())
+41 -7
View File
@@ -1,5 +1,5 @@
use super::CmdResult; use super::CmdResult;
use crate::core::*; use crate::{core::*, logging, utils::logging::Type};
/// 发送脚本验证通知消息 /// 发送脚本验证通知消息
#[tauri::command] #[tauri::command]
@@ -28,7 +28,14 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
"config_validate::script_error" "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); 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] #[tauri::command]
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> { 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() match CoreManager::global()
.validate_config_file(&file_path, None) .validate_config_file(&file_path, None)
@@ -48,7 +55,13 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
} }
Err(e) => { Err(e) => {
let error_msg = e.to_string(); 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); handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
Ok(false) 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) { pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
if !result.0 { if !result.0 {
let error_msg = &result.1; let error_msg = &result.1;
println!("[通知] 处理{}验证错误: {}", file_type, error_msg); logging!(
info,
Type::Config,
true,
"[通知] 处理{}验证错误: {}",
file_type,
error_msg
);
// 检查是否为merge文件 // 检查是否为merge文件
let is_merge_file = file_type.contains("合并"); 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); logging!(
println!("[通知] 发送通知: status={}, msg={}", status, error_msg); 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); handle::Handle::notice_message(status, error_msg);
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@ use crate::{config::*, feat, wrap_err};
pub fn get_verge_config() -> CmdResult<IVergeResponse> { pub fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge(); let verge = Config::verge();
let verge_data = verge.data().clone(); let verge_data = verge.data().clone();
Ok(IVergeResponse::from(verge_data)) Ok(IVergeResponse::from(*verge_data))
} }
/// 修改Verge配置 /// 修改Verge配置
+4
View File
@@ -63,6 +63,8 @@ impl IClashTemp {
vec![ vec![
"tauri://localhost", "tauri://localhost",
"http://tauri.localhost", "http://tauri.localhost",
// Only enable this in dev mode
#[cfg(feature = "verge-dev")]
"http://localhost:3000", "http://localhost:3000",
"https://yacd.metacubex.one", "https://yacd.metacubex.one",
"https://metacubex.github.io", "https://metacubex.github.io",
@@ -103,6 +105,8 @@ impl IClashTemp {
vec![ vec![
"tauri://localhost", "tauri://localhost",
"http://tauri.localhost", "http://tauri.localhost",
// Only enable this in dev mode
#[cfg(feature = "verge-dev")]
"http://localhost:3000", "http://localhost:3000",
"https://yacd.metacubex.one", "https://yacd.metacubex.one",
"https://metacubex.github.io", "https://metacubex.github.io",
+53 -14
View File
@@ -15,10 +15,10 @@ pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
pub struct Config { pub struct Config {
clash_config: Draft<IClashTemp>, clash_config: Draft<Box<IClashTemp>>,
verge_config: Draft<IVerge>, verge_config: Draft<Box<IVerge>>,
profiles_config: Draft<IProfiles>, profiles_config: Draft<Box<IProfiles>>,
runtime_config: Draft<IRuntime>, runtime_config: Draft<Box<IRuntime>>,
} }
impl Config { impl Config {
@@ -26,26 +26,26 @@ impl Config {
static CONFIG: OnceCell<Config> = OnceCell::new(); static CONFIG: OnceCell<Config> = OnceCell::new();
CONFIG.get_or_init(|| Config { CONFIG.get_or_init(|| Config {
clash_config: Draft::from(IClashTemp::new()), clash_config: Draft::from(Box::new(IClashTemp::new())),
verge_config: Draft::from(IVerge::new()), verge_config: Draft::from(Box::new(IVerge::new())),
profiles_config: Draft::from(IProfiles::new()), profiles_config: Draft::from(Box::new(IProfiles::new())),
runtime_config: Draft::from(IRuntime::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() Self::global().clash_config.clone()
} }
pub fn verge() -> Draft<IVerge> { pub fn verge() -> Draft<Box<IVerge>> {
Self::global().verge_config.clone() Self::global().verge_config.clone()
} }
pub fn profiles() -> Draft<IProfiles> { pub fn profiles() -> Draft<Box<IProfiles>> {
Self::global().profiles_config.clone() Self::global().profiles_config.clone()
} }
pub fn runtime() -> Draft<IRuntime> { pub fn runtime() -> Draft<Box<IRuntime>> {
Self::global().runtime_config.clone() Self::global().runtime_config.clone()
} }
@@ -149,11 +149,11 @@ impl Config {
pub async fn generate() -> Result<()> { pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await; let (config, exists_keys, logs) = enhance::enhance().await;
*Config::runtime().draft() = IRuntime { *Config::runtime().draft() = Box::new(IRuntime {
config: Some(config), config: Some(config),
exists_keys, exists_keys,
chain_logs: logs, chain_logs: logs,
}; });
Ok(()) Ok(())
} }
@@ -164,3 +164,42 @@ pub enum ConfigType {
Run, Run,
Check, 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>>>()
);
}
}
+28 -20
View File
@@ -9,13 +9,21 @@ pub struct Draft<T: Clone + ToOwned> {
macro_rules! draft_define { macro_rules! draft_define {
($id: ident) => { ($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)] #[allow(unused)]
pub fn data(&self) -> MappedMutexGuard<$id> { pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0) 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| { MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() { if inner.1.is_none() {
&mut inner.0 &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| { MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() { if inner.1.is_none() {
inner.1 = Some(inner.0.clone()); 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(); let mut inner = self.inner.lock();
match inner.1.take() { 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(); let mut inner = self.inner.lock();
inner.1.take() inner.1.take()
} }
} }
impl From<$id> for Draft<$id> { impl From<Box<$id>> for Draft<Box<$id>> {
fn from(data: $id) -> Self { fn from(data: Box<$id>) -> Self {
Draft { Draft {
inner: Arc::new(Mutex::new((data, None))), inner: Arc::new(Mutex::new((data, None))),
} }
@@ -71,12 +79,12 @@ draft_define!(IRuntime);
draft_define!(IVerge); draft_define!(IVerge);
#[test] #[test]
fn test_draft() { fn test_draft_box() {
let verge = IVerge { let verge = Box::new(IVerge {
enable_auto_launch: Some(true), enable_auto_launch: Some(true),
enable_tun_mode: Some(false), enable_tun_mode: Some(false),
..IVerge::default() ..IVerge::default()
}; });
let draft = Draft::from(verge); 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_auto_launch, Some(true));
assert_eq!(draft.draft().enable_tun_mode, Some(false)); assert_eq!(draft.draft().enable_tun_mode, Some(false));
let mut d = draft.draft(); {
d.enable_auto_launch = Some(false); let mut d = draft.draft();
d.enable_tun_mode = Some(true); d.enable_auto_launch = Some(false);
drop(d); d.enable_tun_mode = Some(true);
}
assert_eq!(draft.data().enable_auto_launch, Some(true)); assert_eq!(draft.data().enable_auto_launch, Some(true));
assert_eq!(draft.data().enable_tun_mode, Some(false)); 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_auto_launch, Some(false));
assert_eq!(draft.draft().enable_tun_mode, Some(true)); assert_eq!(draft.draft().enable_tun_mode, Some(true));
let mut d = draft.draft(); {
d.enable_auto_launch = Some(true); let mut d = draft.draft();
drop(d); d.enable_auto_launch = Some(true);
}
assert_eq!(draft.data().enable_auto_launch, Some(false)); assert_eq!(draft.data().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(true)); assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert!(draft.discard().is_some()); assert!(draft.discard().is_some());
assert_eq!(draft.data().enable_auto_launch, Some(false)); assert_eq!(draft.data().enable_auto_launch, Some(false));
assert!(draft.discard().is_none()); assert!(draft.discard().is_none());
assert_eq!(draft.draft().enable_auto_launch, Some(false)); assert_eq!(draft.draft().enable_auto_launch, Some(false));
+202 -1
View File
@@ -3,7 +3,7 @@ use crate::utils::{dirs, help};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{fs, io::Write}; use std::{collections::HashSet, fs, io::Write};
/// Define the `profiles.yaml` schema /// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -15,6 +15,14 @@ pub struct IProfiles {
pub items: Option<Vec<PrfItem>>, 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 { macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => { ($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() { if ($rv.$key).is_some() {
@@ -485,4 +493,197 @@ impl IProfiles {
.collect() .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(())
}
}
}
} }
+99 -2
View File
@@ -1,6 +1,7 @@
use crate::{ use crate::{
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC}, config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
utils::{dirs, help, i18n}, logging,
utils::{dirs, help, i18n, logging::Type},
}; };
use anyhow::Result; use anyhow::Result;
use log::LevelFilter; use log::LevelFilter;
@@ -200,6 +201,9 @@ pub struct IVerge {
/// 自动进入轻量模式的延迟(分钟) /// 自动进入轻量模式的延迟(分钟)
pub auto_light_weight_minutes: Option<u64>, pub auto_light_weight_minutes: Option<u64>,
/// 启用代理页面自动滚动
pub enable_hover_jump_navigator: Option<bool>,
/// 服务状态跟踪 /// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>, pub service_state: Option<crate::core::service::ServiceState>,
} }
@@ -229,6 +233,93 @@ pub struct IVergeTheme {
} }
impl IVerge { 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 { fn get_system_language() -> String {
let sys_lang = sys_locale::get_locale() let sys_lang = sys_locale::get_locale()
.unwrap_or_else(|| String::from("en")) .unwrap_or_else(|| String::from("en"))
@@ -275,6 +366,7 @@ impl IVerge {
tun_tray_icon: Some(false), tun_tray_icon: Some(false),
enable_auto_launch: Some(false), enable_auto_launch: Some(false),
enable_silent_start: Some(false), enable_silent_start: Some(false),
enable_hover_jump_navigator: Some(true),
enable_system_proxy: Some(false), enable_system_proxy: Some(false),
proxy_auto_config: Some(false), proxy_auto_config: Some(false),
pac_file_content: Some(DEFAULT_PAC.into()), pac_file_content: Some(DEFAULT_PAC.into()),
@@ -351,6 +443,7 @@ impl IVerge {
patch!(enable_tun_mode); patch!(enable_tun_mode);
patch!(enable_auto_launch); patch!(enable_auto_launch);
patch!(enable_silent_start); patch!(enable_silent_start);
patch!(enable_hover_jump_navigator);
patch!(enable_random_port); patch!(enable_random_port);
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
patch!(verge_redir_port); patch!(verge_redir_port);
@@ -492,11 +585,14 @@ pub struct IVergeResponse {
pub auto_light_weight_minutes: Option<u64>, pub auto_light_weight_minutes: Option<u64>,
pub enable_dns_settings: Option<bool>, pub enable_dns_settings: Option<bool>,
pub home_cards: Option<serde_json::Value>, pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>, pub service_state: Option<crate::core::service::ServiceState>,
} }
impl From<IVerge> for IVergeResponse { impl From<IVerge> for IVergeResponse {
fn from(verge: IVerge) -> Self { fn from(verge: IVerge) -> Self {
// 先获取验证后的clash_core值,避免后续借用冲突
let valid_clash_core = verge.get_valid_clash_core();
Self { Self {
app_log_level: verge.app_log_level, app_log_level: verge.app_log_level,
language: verge.language, language: verge.language,
@@ -528,7 +624,7 @@ impl From<IVerge> for IVergeResponse {
proxy_host: verge.proxy_host, proxy_host: verge.proxy_host,
theme_setting: verge.theme_setting, theme_setting: verge.theme_setting,
web_ui_list: verge.web_ui_list, web_ui_list: verge.web_ui_list,
clash_core: verge.clash_core, clash_core: Some(valid_clash_core),
hotkeys: verge.hotkeys, hotkeys: verge.hotkeys,
auto_close_connection: verge.auto_close_connection, auto_close_connection: verge.auto_close_connection,
auto_check_update: verge.auto_check_update, 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, auto_light_weight_minutes: verge.auto_light_weight_minutes,
enable_dns_settings: verge.enable_dns_settings, enable_dns_settings: verge.enable_dns_settings,
home_cards: verge.home_cards, home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
service_state: verge.service_state, service_state: verge.service_state,
} }
} }
+6 -11
View File
@@ -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 { impl CoreManager {
/// 检查文件是否为脚本文件 /// 检查文件是否为脚本文件
@@ -140,11 +140,11 @@ impl CoreManager {
/// 使用默认配置 /// 使用默认配置
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> { pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); 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()), config: Some(Config::clash().latest().0.clone()),
exists_keys: vec![], exists_keys: vec![],
chain_logs: Default::default(), chain_logs: Default::default(),
}; });
help::save_yaml( help::save_yaml(
&runtime_path, &runtime_path,
&Config::clash().latest().0, &Config::clash().latest().0,
@@ -249,8 +249,7 @@ impl CoreManager {
config_path config_path
); );
let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = Config::verge().latest().get_valid_clash_core();
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
logging!(info, Type::Config, true, "使用内核: {}", clash_core); logging!(info, Type::Config, true, "使用内核: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
@@ -442,11 +441,7 @@ impl CoreManager {
let app_handle = handle::Handle::global() let app_handle = handle::Handle::global()
.app_handle() .app_handle()
.ok_or(anyhow::anyhow!("failed to get app handle"))?; .ok_or(anyhow::anyhow!("failed to get app handle"))?;
let clash_core = Config::verge() let clash_core = Config::verge().latest().get_valid_clash_core();
.latest()
.clash_core
.clone()
.unwrap_or("verge-mihomo".to_string());
let config_dir = dirs::app_home_dir()?; let config_dir = dirs::app_home_dir()?;
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service"); let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
@@ -804,7 +799,7 @@ impl CoreManager {
return Err(error_message.to_string()); return Err(error_message.to_string());
} }
let core: &str = &clash_core.clone().unwrap(); 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); let error_message = format!("Clash core invalid name: {}", core);
logging!(error, Type::Core, true, "{}", error_message); logging!(error, Type::Core, true, "{}", error_message);
return Err(error_message); return Err(error_message);
+22 -3
View File
@@ -151,6 +151,10 @@ impl NotificationSystem {
match window.emit(event_name_str, payload) { match window.emit(event_name_str, payload) {
Ok(_) => { Ok(_) => {
system.stats.total_sent.fetch_add(1, Ordering::SeqCst); 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) => { Err(e) => {
log::warn!("Failed to emit event {}: {}", event_name_str, e); log::warn!("Failed to emit event {}: {}", event_name_str, e);
@@ -224,12 +228,27 @@ impl NotificationSystem {
} }
fn shutdown(&mut self) { fn shutdown(&mut self) {
log::info!("NotificationSystem shutdown initiated");
self.is_running = false; 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");
} }
} }
+75 -33
View File
@@ -153,35 +153,76 @@ impl Hotkey {
"=== Hotkey Dashboard Window Operation Start ===" "=== Hotkey Dashboard Window Operation Start ==="
); );
// 使用 spawn_blocking 来确保在正确的线程上执行 // 检查是否在轻量模式下,如果是,需要同步处理
AsyncHandler::spawn_blocking(|| { if crate::module::lightweight::is_in_lightweight_mode() {
logging!(debug, Type::Hotkey, "Toggle dashboard window visibility"); logging!(
info,
// 检查窗口是否存在 Type::Hotkey,
if let Some(window) = handle::Handle::global().get_window() { true,
// 如果窗口可见,则隐藏它 "In lightweight mode, calling open_or_close_dashboard directly"
if window.is_visible().unwrap_or(false) { );
logging!(info, Type::Window, "Window is visible, hiding it"); crate::feat::open_or_close_dashboard();
let _ = window.hide(); } else {
} else { AsyncHandler::spawn(move || async move {
// 如果窗口不可见,则显示它
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 {
// 如果窗口不存在,创建一个新窗口
logging!( logging!(
info, debug,
Type::Window, Type::Hotkey,
"Window does not exist, creating a new one" 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!( logging!(
debug, debug,
@@ -231,10 +272,11 @@ impl Hotkey {
if is_enable_global_hotkey { if is_enable_global_hotkey {
f(); 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_visible = WindowManager::is_main_window_visible();
let is_focused = window.is_focused().unwrap_or(false); let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible { if is_focused && is_visible {
f(); f();
@@ -289,9 +331,9 @@ impl Hotkey {
let func = iter.next(); let func = iter.next();
let key = iter.next(); let key = iter.next();
if func.is_some() && key.is_some() { if let (Some(func), Some(key)) = (func, key) {
let func = func.unwrap().trim(); let func = func.trim();
let key = key.unwrap().trim(); let key = key.trim();
map.insert(key, func); map.insert(key, func);
} }
}); });
+4 -7
View File
@@ -742,8 +742,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)"); log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心"); // logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = Config::verge().latest().get_valid_clash_core();
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
let bin_ext = if cfg!(windows) { ".exe" } else { "" }; let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}"); 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 version_check && is_service_available().await.is_ok() {
if is_service_available().await.is_ok() { log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用"); return start_with_existing_service(config_file).await;
return start_with_existing_service(config_file).await;
}
} }
if !version_check { if !version_check {
+4 -6
View File
@@ -235,13 +235,11 @@ impl Sysopt {
} else { } else {
return Ok(()); return Ok(());
} }
} else if let Err(e) = startup_shortcut::remove_shortcut() {
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
self.try_original_autostart_method(is_enable);
} else { } else {
if let Err(e) = startup_shortcut::remove_shortcut() { return Ok(());
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
}
} }
} }
+157 -67
View File
@@ -6,11 +6,7 @@ use crate::{
cmd, cmd,
config::Config, config::Config,
feat, logging, feat, logging,
module::{ module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
lightweight::{entry_lightweight_mode, is_in_lightweight_mode},
mihomo::Rate,
},
resolve,
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION}, utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
Type, Type,
}; };
@@ -205,33 +201,38 @@ impl Tray {
Ok(()) Ok(())
} }
/// 更新托盘菜单(带频率限制) /// 更新托盘菜单
pub fn update_menu(&self) -> Result<()> { 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) { if self.menu_updating.load(Ordering::Acquire) {
log::debug!(target: "app", "托盘菜单正在更新中,跳过本次更新");
return Ok(()); return Ok(());
} }
// 检查更新频率 // 检查更新频率,但允许重要事件跳过频率限制
{ let should_force_update = match std::thread::current().name() {
let last_update = self.last_menu_update.lock(); Some("main") => true,
if let Some(last_time) = *last_update { _ => {
if last_time.elapsed() < MIN_UPDATE_INTERVAL { let last_update = self.last_menu_update.lock();
log::debug!(target: "app", "托盘菜单更新频率过高,跳过本次更新"); if let Some(last_time) = *last_update {
return Ok(()); last_time.elapsed() >= MIN_UPDATE_INTERVAL
} else {
true
} }
} }
};
if !should_force_update {
return Ok(());
} }
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
None => { None => {
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在"); log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
return Ok(()); // 早期返回,避免panic return Ok(());
} }
}; };
@@ -248,6 +249,7 @@ impl Tray {
result result
} }
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> { fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
let verge = Config::verge().latest().clone(); let verge = Config::verge().latest().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); 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") { match app_handle.tray_by_id("main") {
Some(tray) => { Some(tray) => {
let _ = tray.set_menu(Some(create_tray_menu( let _ = tray.set_menu(Some(create_tray_menu(
&app_handle, app_handle,
Some(mode.as_str()), Some(mode.as_str()),
*system_proxy, *system_proxy,
*tun_mode, *tun_mode,
@@ -288,6 +290,7 @@ impl Tray {
} }
/// 更新托盘图标 /// 更新托盘图标
#[cfg(target_os = "macos")]
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> { pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
@@ -315,58 +318,94 @@ impl Tray {
(false, true) => TrayState::get_tun_tray_icon(), (false, true) => TrayState::get_tun_tray_icon(),
(false, false) => TrayState::get_common_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 enable_tray_speed = verge.enable_tray_speed.unwrap_or(false);
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
let _ = tray.set_icon_as_template(!is_colorful); let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
return Ok(()); let is_colorful = colorful == "colorful";
}
let rate = if let Some(rate) = rate { if !enable_tray_speed {
Some(rate) let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
} else { let _ = tray.set_icon_as_template(!is_colorful);
let guard = self.speed_rate.lock(); return Ok(());
if let Some(guard) = guard.as_ref() { }
if let Some(rate) = guard.get_curent_rate() {
Some(rate) let rate = if let Some(rate) = rate {
} else { Some(rate)
Some(Rate::default()) } 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 { } else {
Some(Rate::default()) 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(); let rate = rate_guard.as_ref();
if *rate_guard != rate { if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) {
*rate_guard = 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 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);
}
} }
Ok(())
} }
Ok(())
}
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
{ pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); let app_handle = match handle::Handle::global().app_handle() {
Ok(()) 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_menu()?;
self.update_icon(None)?; self.update_icon(None)?;
self.update_tooltip()?; self.update_tooltip()?;
// 更新轻量模式显示状态
self.update_tray_display()?;
Ok(()) Ok(())
} }
@@ -591,6 +632,12 @@ impl Tray {
let icon_bytes = TrayState::get_common_tray_icon().1; let icon_bytes = TrayState::get_common_tray_icon().1;
let icon = tauri::image::Image::from_bytes(&icon_bytes)?; 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") let mut builder = TrayIconBuilder::with_id("main")
.icon(icon) .icon(icon)
.icon_as_template(false); .icon_as_template(false);
@@ -621,10 +668,14 @@ impl Tray {
"system_proxy" => feat::toggle_system_proxy(), "system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None), "tun_mode" => feat::toggle_tun_mode(None),
"main_window" => { "main_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Tray点击事件: 显示主窗口");
if crate::module::lightweight::is_in_lightweight_mode() { if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
crate::module::lightweight::exit_lightweight_mode(); 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", "系统托盘创建成功"); log::info!(target: "app", "系统托盘创建成功");
Ok(()) 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( fn create_tray_menu(
@@ -885,15 +947,23 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
feat::change_clash_mode(mode.into()); feat::change_clash_mode(mode.into());
} }
"open_window" => { "open_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "托盘菜单点击: 打开窗口");
// 如果在轻量模式中,先退出轻量模式 // 如果在轻量模式中,先退出轻量模式
if crate::module::lightweight::is_in_lightweight_mode() { if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出");
crate::module::lightweight::exit_lightweight_mode(); 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(), "copy_env" => feat::copy_clash_env(),
"open_app_dir" => { "open_app_dir" => {
let _ = cmd::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_clash" => feat::restart_clash_core(),
"restart_app" => feat::restart_app(), "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" => { "quit" => {
feat::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);
}
} }
+1 -1
View File
@@ -22,7 +22,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
let verge = Config::verge(); let verge = Config::verge();
let verge = verge.latest(); let verge = verge.latest();
( (
verge.clash_core.clone(), Some(verge.get_valid_clash_core()),
verge.enable_tun_mode.unwrap_or(false), verge.enable_tun_mode.unwrap_or(false),
verge.enable_builtin_enhanced.unwrap_or(true), verge.enable_builtin_enhanced.unwrap_or(true),
verge.verge_socks_enabled.unwrap_or(false), verge.verge_socks_enabled.unwrap_or(false),
+62 -13
View File
@@ -33,37 +33,40 @@ pub fn use_script(
} }
let _ = context.eval(Source::from_bytes( let _ = context.eval(Source::from_bytes(
r#"var console = Object.freeze({ r#"var console = Object.freeze({
log(data){__verge_log__("log",JSON.stringify(data))}, log(data){__verge_log__("log",JSON.stringify(data, null, 2))},
info(data){__verge_log__("info",JSON.stringify(data))}, info(data){__verge_log__("info",JSON.stringify(data, null, 2))},
error(data){__verge_log__("error",JSON.stringify(data))}, error(data){__verge_log__("error",JSON.stringify(data, null, 2))},
debug(data){__verge_log__("debug",JSON.stringify(data))}, 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 = use_lowercase(config.clone());
let config_str = serde_json::to_string(&config)?; let config_str = serde_json::to_string(&config)?;
// 仅处理 name 参数中的特殊字符
let safe_name = escape_js_string_for_single_quote(&name);
let code = format!( let code = format!(
r#"try{{ r#"try{{
{script}; {script};
JSON.stringify(main({config_str},'{name}')||'') JSON.stringify(main({config_str},'{safe_name}')||'')
}} catch(err) {{ }} catch(err) {{
`__error_flag__ ${{err.toString()}}` `__error_flag__ ${{err.toString()}}`
}}"# }}"#
); );
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) { if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
if !result.is_string() { if !result.is_string() {
anyhow::bail!("main function should return object"); anyhow::bail!("main function should return object");
} }
let result = result.to_string(&mut context).unwrap(); let result = result.to_string(&mut context).unwrap();
let result = result.to_std_string().unwrap(); let result = result.to_std_string().unwrap();
if result.starts_with("__error_flag__") {
anyhow::bail!(result[15..].to_owned()); // 直接解析JSON结果,不做其他解析
} let res: Result<Mapping, Error> = parse_json_safely(&result);
if result == "\"\"" {
anyhow::bail!("main function should return object");
}
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
let mut out = outputs.lock().unwrap(); let mut out = outputs.lock().unwrap();
match res { match res {
Ok(config) => Ok((use_lowercase(config), out.to_vec())), 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] #[test]
fn test_script() { fn test_script() {
let script = r#" let script = r#"
@@ -104,6 +128,31 @@ fn test_script() {
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap(); let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
let _ = serde_yaml::to_string(&config).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); 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
View File
@@ -1 +0,0 @@
pub mod service;
-1
View File
@@ -1 +0,0 @@
+1 -1
View File
@@ -84,7 +84,7 @@ pub fn change_clash_mode(mode: String) {
after_change_clash_mode(); after_change_clash_mode();
} }
} }
Err(err) => println!("{err}"), Err(err) => log::error!(target: "app", "{err}"),
} }
}); });
} }
+16 -14
View File
@@ -2,7 +2,9 @@ use crate::{
cmd, cmd,
config::{Config, PrfItem, PrfOption}, config::{Config, PrfItem, PrfOption},
core::{handle, CoreManager, *}, core::{handle, CoreManager, *},
logging,
process::AsyncHandler, process::AsyncHandler,
utils::logging::Type,
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -29,7 +31,7 @@ pub async fn update_profile(
option: Option<PrfOption>, option: Option<PrfOption>,
auto_refresh: Option<bool>, auto_refresh: Option<bool>,
) -> Result<()> { ) -> Result<()> {
println!("[订阅更新] 开始更新订阅 {}", uid); logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性 let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
let url_opt = { 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"); let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote { if !is_remote {
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid); log::info!(target: "app", "[订阅更新] {} 不是远程订阅,跳过更新", uid);
None // 非远程订阅直接更新 None // 非远程订阅直接更新
} else if item.url.is_none() { } else if item.url.is_none() {
println!("[订阅更新] {} 缺少URL,无法更新", uid); log::warn!(target: "app", "[订阅更新] {} 缺少URL,无法更新", uid);
bail!("failed to get the profile item url"); bail!("failed to get the profile item url");
} else { } else {
println!( log::info!(target: "app",
"[订阅更新] {} 是远程订阅,URL: {}", "[订阅更新] {} 是远程订阅,URL: {}",
uid, uid,
item.url.clone().unwrap() item.url.clone().unwrap()
@@ -56,24 +58,24 @@ pub async fn update_profile(
let should_update = match url_opt { let should_update = match url_opt {
Some((url, opt)) => { Some((url, opt)) => {
println!("[订阅更新] 开始下载新的订阅内容"); log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt.clone(), option.clone()); let merged_opt = PrfOption::merge(opt.clone(), option.clone());
// 尝试使用正常设置更新 // 尝试使用正常设置更新
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await { match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => { Ok(item) => {
println!("[订阅更新] 更新订阅配置成功"); log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles(); let profiles = Config::profiles();
let mut profiles = profiles.latest(); let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?; profiles.update_item(uid.clone(), item)?;
let is_current = Some(uid.clone()) == profiles.get_current(); let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current); log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
is_current && auto_refresh is_current && auto_refresh
} }
Err(err) => { Err(err) => {
// 首次更新失败,尝试使用Clash代理 // 首次更新失败,尝试使用Clash代理
println!("[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err); log::warn!(target: "app", "[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
// 发送通知 // 发送通知
handle::Handle::notice_message("update_retry_with_clash", uid.clone()); handle::Handle::notice_message("update_retry_with_clash", uid.clone());
@@ -90,7 +92,7 @@ pub async fn update_profile(
// 使用Clash代理重试 // 使用Clash代理重试
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await { match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
Ok(mut item) => { Ok(mut item) => {
println!("[订阅更新] 使用Clash代理更新成功"); log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
// 恢复原始代理设置到item // 恢复原始代理设置到item
if let Some(option) = item.option.as_mut() { 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); handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.get_current(); let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current); log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
is_current && auto_refresh is_current && auto_refresh
} }
Err(retry_err) => { Err(retry_err) => {
println!("[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err); log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
handle::Handle::notice_message( handle::Handle::notice_message(
"update_failed_even_with_clash", "update_failed_even_with_clash",
format!("{}", retry_err), format!("{}", retry_err),
@@ -129,14 +131,14 @@ pub async fn update_profile(
}; };
if should_update { if should_update {
println!("[订阅更新] 更新内核配置"); logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
match CoreManager::global().update_config().await { match CoreManager::global().update_config().await {
Ok(_) => { Ok(_) => {
println!("[订阅更新] 更新成功"); logging!(info, Type::Config, true, "[订阅更新] 更新成功");
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
Err(err) => { Err(err) => {
println!("[订阅更新] 更新失败: {}", err); logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("update_failed", format!("{err}")); handle::Handle::notice_message("update_failed", format!("{err}"));
log::error!(target: "app", "{err}"); log::error!(target: "app", "{err}");
} }
+156 -63
View File
@@ -3,55 +3,37 @@ use crate::AppHandleManager;
use crate::{ use crate::{
config::Config, config::Config,
core::{handle, sysopt, CoreManager}, core::{handle, sysopt, CoreManager},
logging,
module::mihomo::MihomoManager, module::mihomo::MihomoManager,
utils::resolve, utils::logging::Type,
}; };
/// Open or close the dashboard window /// Open or close the dashboard window
#[allow(dead_code)] #[allow(dead_code)]
pub fn open_or_close_dashboard() { 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"); log::info!(target: "app", "Attempting to open/close dashboard");
if let Some(window) = handle::Handle::global().get_window() { // 检查是否在轻量模式下
println!("Found existing window"); if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Found existing window"); 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");
match window.is_visible() { let result = WindowManager::show_main_window();
Ok(visible) => { log::info!(target: "app", "Window operation result: {:?}", result);
println!("Window visibility status: {}", visible); return;
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);
} }
// 使用统一的窗口管理器切换窗口状态
let result = WindowManager::toggle_main_window();
log::info!(target: "app", "Window toggle result: {:?}", result);
} }
/// 优化的应用退出函数 /// 异步优化的应用退出函数
pub fn quit() { 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(); 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() { if let Some(window) = handle::Handle::global().get_window() {
let _ = window.hide(); let _ = window.hide();
log::info!(target: "app", "窗口已隐藏");
} }
// 在单独线程中处理资源清理,避免阻塞主线程 // 使用异步任务处理资源清理,避免阻塞
std::thread::spawn(move || { AsyncHandler::spawn(move || async move {
let cleanup_result = clean(); logging!(info, Type::System, true, "开始异步清理资源");
app_handle.exit(match cleanup_result { let cleanup_result = clean_async().await;
true => 0,
false => 1, 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}; use tokio::time::{timeout, Duration};
let rt = tokio::runtime::Runtime::new().unwrap();
let cleanup_result = rt.block_on(async { logging!(info, Type::System, true, "开始执行异步清理操作...");
// 1. 处理TUN模式
let tun_success = if Config::verge().data().enable_tun_mode.unwrap_or(false) { // 1. 处理TUN模式
let tun_task = async {
if Config::verge().data().enable_tun_mode.unwrap_or(false) {
let disable_tun = serde_json::json!({ let disable_tun = serde_json::json!({
"tun": { "tun": {
"enable": false "enable": false
} }
}); });
timeout( match timeout(
Duration::from_secs(1), Duration::from_secs(2),
MihomoManager::global().patch_configs(disable_tun), MihomoManager::global().patch_configs(disable_tun),
) )
.await .await
.is_ok() {
Ok(_) => {
log::info!(target: "app", "TUN模式已禁用");
true
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时");
false
}
}
} else { } else {
true true
}; }
};
// 2. 顺序执行关键清理 // 2. 系统代理重置
let proxy_res = timeout( let proxy_task = async {
Duration::from_secs(1), match timeout(
Duration::from_secs(3),
sysopt::Sysopt::global().reset_sysproxy(), 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. 平台特定清理 // 4. DNS恢复(仅macOS)
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let _dns_res = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await; 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")] #[cfg(target_os = "macos")]
+22 -11
View File
@@ -2,10 +2,10 @@ mod cmd;
mod config; mod config;
mod core; mod core;
mod enhance; mod enhance;
mod error;
mod feat; mod feat;
mod module; mod module;
mod process; mod process;
mod state;
mod utils; mod utils;
use crate::{ use crate::{
core::hotkey, core::hotkey,
@@ -15,8 +15,8 @@ use crate::{
use config::Config; use config::Config;
use std::sync::{Mutex, Once}; use std::sync::{Mutex, Once};
use tauri::AppHandle; use tauri::AppHandle;
#[cfg(target_os = "macos")]
use tauri::Manager; use tauri::Manager;
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{timeout, Duration}; use tokio::time::{timeout, Duration};
@@ -90,17 +90,20 @@ pub fn run() {
let _ = utils::dirs::init_portable_flag(); 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, "开始检查单例实例..."); logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await { match timeout(Duration::from_secs(3), server::check_singleton()).await {
Ok(result) => { Ok(result) => {
if result.is_err() { if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行"); logging!(info, Type::Setup, true, "检测到已有应用实例运行");
true if let Some(app_handle) = AppHandleManager::global().get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else { } else {
logging!(info, Type::Setup, true, "未检测到其他应用实例"); logging!(info, Type::Setup, true, "未检测到其他应用实例");
false
} }
} }
Err(_) => { Err(_) => {
@@ -110,13 +113,9 @@ pub fn run() {
true, true,
"单例检查超时,假定没有其他实例运行" "单例检查超时,假定没有其他实例运行"
); );
false
} }
} }
}); });
if app_exists {
return;
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); 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(); let app_handle = app.handle().clone();
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
@@ -205,6 +212,9 @@ pub fn run() {
logging!(error, Type::Setup, true, "初始化资源失败: {}", e); 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, "初始化完成,继续执行"); logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(()) Ok(())
}) })
@@ -253,6 +263,7 @@ pub fn run() {
cmd::invoke_uwp_tool, cmd::invoke_uwp_tool,
cmd::copy_clash_env, cmd::copy_clash_env,
cmd::get_proxies, cmd::get_proxies,
cmd::force_refresh_proxies,
cmd::get_providers_proxies, cmd::get_providers_proxies,
cmd::save_dns_config, cmd::save_dns_config,
cmd::apply_dns_config, cmd::apply_dns_config,
@@ -366,7 +377,7 @@ pub fn run() {
if core::handle::Handle::global().is_exiting() { if core::handle::Handle::global().is_exiting() {
return; return;
} }
println!("closing window..."); log::info!(target: "app", "closing window...");
api.prevent_close(); api.prevent_close();
if let Some(window) = core::handle::Handle::global().get_window() { if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide(); let _ = window.hide();
+115 -67
View File
@@ -1,47 +1,92 @@
use crate::{ use crate::{
config::Config, config::Config,
core::{handle, timer::Timer}, core::{handle, timer::Timer, tray::Tray},
log_err, logging, logging_error, log_err, logging,
state::lightweight::LightWeightState,
utils::logging::Type, utils::logging::Type,
AppHandleManager,
}; };
#[cfg(target_os = "macos")]
use crate::logging_error;
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder; use delay_timer::prelude::TaskBuilder;
use once_cell::sync::OnceCell; use std::sync::Mutex;
use parking_lot::{Mutex, RwLock};
use std::{
sync::Arc,
time::{Duration, Instant},
};
use tauri::{Listener, Manager}; use tauri::{Listener, Manager};
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
// 轻量模式状态标志 fn with_lightweight_status<F, R>(f: F) -> R
static IS_LIGHTWEIGHT_MODE: OnceCell<Arc<RwLock<bool>>> = OnceCell::new(); where
F: FnOnce(&mut LightWeightState) -> R,
// 添加一个锁来防止并发退出轻量模式 {
static EXIT_LOCK: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new(); let app_handle = handle::Handle::global().app_handle().unwrap();
let state = app_handle.state::<Mutex<LightWeightState>>();
fn get_lightweight_mode() -> &'static Arc<RwLock<bool>> { let mut guard = state.lock().unwrap();
IS_LIGHTWEIGHT_MODE.get_or_init(|| Arc::new(RwLock::new(false))) f(&mut guard)
} }
fn get_exit_lock() -> &'static Mutex<(bool, Instant)> { pub fn run_once_auto_lightweight() {
EXIT_LOCK.get_or_init(|| Mutex::new((false, Instant::now()))) 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 { pub fn is_in_lightweight_mode() -> bool {
*get_lightweight_mode().read() with_lightweight_status(|state| state.is_lightweight)
} }
// 设置轻量模式状态 // 设置轻量模式状态
fn set_lightweight_mode(value: bool) { fn set_lightweight_mode(value: bool) {
let mut mode = get_lightweight_mode().write(); with_lightweight_status(|state| {
*mode = value; state.set_lightweight_mode(value);
logging!(info, Type::Lightweight, true, "轻量模式状态: {}", value); });
// 触发托盘更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {}", e);
}
} }
pub fn enable_auto_light_weight_mode() { pub fn enable_auto_light_weight_mode() {
@@ -58,10 +103,18 @@ pub fn disable_auto_light_weight_mode() {
} }
pub fn entry_lightweight_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 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") { if let Some(webview) = window.get_webview_window("main") {
let _ = webview.destroy(); let _ = webview.destroy();
} }
@@ -69,49 +122,36 @@ pub fn entry_lightweight_mode() {
AppHandleManager::global().set_activation_policy_accessory(); AppHandleManager::global().set_activation_policy_accessory();
logging!(info, Type::Lightweight, true, "轻量模式已开启"); logging!(info, Type::Lightweight, true, "轻量模式已开启");
} }
// 标记已进入轻量模式
set_lightweight_mode(true); set_lightweight_mode(true);
let _ = cancel_light_weight_timer(); let _ = cancel_light_weight_timer();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
} }
// 添加从轻量模式恢复的函数 // 添加从轻量模式恢复的函数
pub fn exit_lightweight_mode() { 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() { if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出"); logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
exit_lock.0 = false;
return; return;
} }
// 标记退出轻量模式
set_lightweight_mode(false); set_lightweight_mode(false);
logging!(info, Type::Lightweight, true, "正在退出轻量模式"); logging!(info, Type::Lightweight, true, "正在退出轻量模式");
// macOS激活策略
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_regular();
// 重置UI就绪状态 // 重置UI就绪状态
crate::utils::resolve::reset_ui_ready(); 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() { pub fn add_light_weight_timer() {
logging_error!(Type::Lightweight, setup_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<()> { fn setup_light_weight_timer() -> Result<()> {
Timer::global().init()?; 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() let once_by_minutes = Config::verge()
.latest() .latest()
.auto_light_weight_minutes .auto_light_weight_minutes
.unwrap_or(10); .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() let task = TaskBuilder::default()
.set_task_id(task_id) .set_task_id(task_id)
.set_maximum_parallel_runnable_num(1) .set_maximum_parallel_runnable_num(1)
@@ -179,17 +220,24 @@ fn setup_light_weight_timer() -> Result<()> {
}) })
.context("failed to create timer task")?; .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, let mut timer_map = Timer::global().timer_map.write();
last_run: chrono::Local::now().timestamp(), let timer_task = crate::core::timer::TimerTask {
}; task_id,
interval_minutes: once_by_minutes,
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task); last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
}
logging!( logging!(
info, info,
+4 -1
View File
@@ -3,7 +3,9 @@ use mihomo_api;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tauri::http::{HeaderMap, HeaderValue}; use tauri::http::HeaderMap;
#[cfg(target_os = "macos")]
use tauri::http::HeaderValue;
// 缓存的最大有效期(5秒) // 缓存的最大有效期(5秒)
const CACHE_TTL: Duration = Duration::from_secs(5); const CACHE_TTL: Duration = Duration::from_secs(5);
@@ -105,6 +107,7 @@ impl MihomoManager {
} }
// 提供默认值的版本,避免在connection_info为None时panic // 提供默认值的版本,避免在connection_info为None时panic
#[cfg(target_os = "macos")]
fn get_clash_client_info_or_default() -> (String, HeaderMap) { fn get_clash_client_info_or_default() -> (String, HeaderMap) {
Self::get_clash_client_info().unwrap_or_else(|| { Self::get_clash_client_info().unwrap_or_else(|| {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
-16
View File
@@ -11,20 +11,4 @@ impl AsyncHandler {
{ {
async_runtime::spawn(f()) 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())
}
} }
+44
View File
@@ -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()
}
}
+5
View File
@@ -0,0 +1,5 @@
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
// https://tauri.app/develop/state-management/#do-you-need-arc
pub mod lightweight;
pub mod proxy;
+19
View File
@@ -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),
}
}
}
+2 -2
View File
@@ -204,8 +204,8 @@ pub fn format_bytes_speed(speed: u64) -> String {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[test] #[test]
fn test_format_bytes_speed() { fn test_format_bytes_speed() {
assert_eq!(format_bytes_speed(0), "0B/s"); assert_eq!(format_bytes_speed(0), "0.0B/s");
assert_eq!(format_bytes_speed(1023), "1023B/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), "1.0KB/s");
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s"); assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s"); assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
+3
View File
@@ -286,6 +286,9 @@ pub fn init_config() -> Result<()> {
<Result<()>>::Ok(()) <Result<()>>::Ok(())
})); }));
// 验证并修正verge.yaml中的clash_core配置
crate::log_err!(IVerge::validate_and_fix_config());
crate::log_err!(dirs::profiles_path().map(|path| { crate::log_err!(dirs::profiles_path().map(|path| {
if !path.exists() { if !path.exists() {
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?; help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
+1
View File
@@ -8,3 +8,4 @@ pub mod network;
pub mod resolve; pub mod resolve;
pub mod server; pub mod server;
pub mod tmpl; pub mod tmpl;
pub mod window_manager;
+193 -47
View File
@@ -1,8 +1,10 @@
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use crate::{ use crate::{
config::{Config, IVerge, PrfItem}, config::{Config, IVerge, PrfItem},
core::*, core::*,
logging, logging_error, logging, logging_error,
module::lightweight, module::lightweight::{self, auto_lightweight_mode_init},
process::AsyncHandler, process::AsyncHandler,
utils::{init, logging::Type, server}, utils::{init, logging::Type, server},
wrap_err, wrap_err,
@@ -142,7 +144,8 @@ pub async fn find_unused_port() -> Result<u16> {
/// 异步方式处理启动后的额外任务 /// 异步方式处理启动后的额外任务
pub async fn resolve_setup_async(app_handle: &AppHandle) { 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() { if VERSION.get().is_none() {
let version = app_handle.package_info().version.to_string(); 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!(trace, Type::Config, true, "初始化配置...");
logging_error!(Type::Config, true, Config::init_config().await); 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!(trace, Type::Core, true, "启动核心管理器...");
logging_error!(Type::Core, true, CoreManager::global().init().await); 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); 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); create_window(!is_silent_start);
// 初始化定时器 // 初始化定时器
logging_error!(Type::System, true, timer::Timer::global().init()); logging_error!(Type::System, true, timer::Timer::global().init());
// 自动进入轻量模式 // 自动进入轻量模式
let enable_auto_light_weight_mode = auto_lightweight_mode_init();
{ 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();
}
logging_error!(Type::Tray, true, tray::Tray::global().update_part()); logging_error!(Type::Tray, true, tray::Tray::global().update_part());
logging!(trace, Type::System, true, "初始化热键..."); logging!(trace, Type::System, true, "初始化热键...");
logging_error!(Type::System, true, hotkey::Hotkey::global().init()); 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 (异步) /// reset system proxy (异步)
@@ -258,12 +292,27 @@ pub fn create_window(is_show: bool) -> bool {
is_show 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(app_handle) = handle::Handle::global().app_handle() {
if let Some(window) = app_handle.get_webview_window("main") { if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口"); logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
if is_show { if is_show {
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
let _ = window.unminimize();
}
let _ = window.show(); let _ = window.show();
let _ = window.set_focus(); let _ = window.set_focus();
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
} }
return true; return true;
} }
@@ -299,7 +348,72 @@ pub fn create_window(is_show: bool) -> bool {
.fullscreen(false) .fullscreen(false)
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
.min_inner_size(520.0, 520.0) .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() .build()
{ {
Ok(newly_created_window) => { 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 { if is_show {
let window_clone = newly_created_window.clone(); let window_clone = newly_created_window.clone();
// Attempt to show and focus the window first. // 立即显示窗口
let _ = window_clone.show(); let _ = window_clone.show();
let _ = window_clone.set_focus(); let _ = window_clone.set_focus();
logging!(debug, Type::Window, true, "窗口已尝试显示和聚焦"); logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")]
tokio::time::sleep(Duration::from_millis(100)).await; // Crucial delay {
AppHandleManager::global().set_activation_policy_regular();
logging!( }
debug,
Type::Window,
true,
"延时后,尝试发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() { let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
2 3
} else { } else {
5 8
}; };
logging!( logging!(
info, info,
Type::Window, Type::Window,
true, true,
"等待UI就绪 (最多{}秒)...", "开始监控UI加载状态 (最多{}秒)...",
timeout_seconds timeout_seconds
); );
let wait_result = // 异步监控UI状态,不影响窗口显示
tokio::time::timeout(Duration::from_secs(timeout_seconds), async { tokio::spawn(async move {
while !*get_ui_ready().read() { let wait_result =
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::timeout(Duration::from_secs(timeout_seconds), async {
} let mut check_count = 0;
}) while !*get_ui_ready().read() {
.await; tokio::time::sleep(Duration::from_millis(100)).await;
check_count += 1;
match wait_result { // 每2秒记录一次等待状态
Ok(_) => { if check_count % 20 == 0 {
logging!(info, Type::Window, true, "UI就绪"); 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, "窗口显示流程完成"); logging!(info, Type::Window, true, "窗口显示流程完成");
} else { } else {
logging!( logging!(
debug, debug,
Type::Window, Type::Window,
true, true,
"is_show为false,窗口保持隐藏。尝试发送启动事件。" "is_show为false,窗口保持隐藏状态"
); );
handle::Handle::notify_startup_completed();
} }
}); });
true true
+272
View File
@@ -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" name = "mihomo_api"
edition = "2024" edition = "2024"
[features]
debug = []
[dependencies] [dependencies]
reqwest = { version = "0.12.15", features = ["json"] } reqwest = { version = "0.12.20", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" 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] [dev-dependencies]
@@ -1,11 +1,8 @@
use reqwest::{Method, header::HeaderMap}; use reqwest::{Method, header::HeaderMap};
use serde_json::json; use serde_json::{Value, json};
use std::{ use std::time::Duration;
sync::{Arc, Mutex},
time::Duration,
};
pub mod model; pub mod model;
pub use model::{MihomoData, MihomoManager}; pub use model::MihomoManager;
impl MihomoManager { impl MihomoManager {
pub fn new(mihomo_server: String, headers: HeaderMap) -> Self { pub fn new(mihomo_server: String, headers: HeaderMap) -> Self {
@@ -20,38 +17,10 @@ impl MihomoManager {
Self { Self {
mihomo_server, mihomo_server,
data: Arc::new(Mutex::new(MihomoData {
proxies: serde_json::Value::Null,
providers_proxies: serde_json::Value::Null,
})),
client, 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( async fn send_request(
&self, &self,
method: Method, method: Method,
@@ -87,18 +56,16 @@ impl MihomoManager {
Ok(response) 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 url = format!("{}/proxies", self.mihomo_server);
let proxies = self.send_request(Method::GET, url, None).await?; let proxies = self.send_request(Method::GET, url, None).await?;
self.update_proxies(proxies); Ok(proxies)
Ok(self)
} }
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 url = format!("{}/providers/proxies", self.mihomo_server);
let providers_proxies = self.send_request(Method::GET, url, None).await?; let providers_proxies = self.send_request(Method::GET, url, None).await?;
self.update_providers_proxies(providers_proxies); Ok(providers_proxies)
Ok(self)
} }
pub async fn close_all_connections(&self) -> Result<(), String> { 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)] #[derive(Clone)]
pub struct MihomoManager { pub struct MihomoManager {
pub(crate) mihomo_server: String, pub(crate) mihomo_server: String,
pub(crate) data: Arc<Mutex<MihomoData>>,
pub(crate) client: reqwest::Client, 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; use reqwest::header::HeaderMap;
#[test] #[test]
fn test_mihomo_manager_init() { fn test_mihomo_manager_init() {
let manager = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new()); let _ = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
assert_eq!(manager.get_proxies(), serde_json::Value::Null); assert_eq!(true, true);
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);
} }
+7 -23
View File
@@ -1,5 +1,5 @@
{ {
"version": "2.3.0", "version": "2.3.1",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": { "bundle": {
"active": true, "active": true,
@@ -11,15 +11,9 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": [ "resources": ["resources", "resources/locales/*"],
"resources",
"resources/locales/*"
],
"publisher": "Clash Verge Rev", "publisher": "Clash Verge Rev",
"externalBin": [ "externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"sidecar/verge-mihomo",
"sidecar/verge-mihomo-alpha"
],
"copyright": "GNU General Public License v3.0", "copyright": "GNU General Public License v3.0",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "Clash Verge Rev", "shortDescription": "Clash Verge Rev",
@@ -50,28 +44,18 @@
}, },
"deep-link": { "deep-link": {
"desktop": { "desktop": {
"schemes": [ "schemes": ["clash", "clash-verge"]
"clash",
"clash-verge"
]
} }
} }
}, },
"app": { "app": {
"security": { "security": {
"capabilities": [ "capabilities": ["desktop-capability", "migrated"],
"desktop-capability",
"migrated"
],
"assetProtocol": { "assetProtocol": {
"scope": [ "scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
"$APPDATA/**",
"$RESOURCE/../**",
"**"
],
"enable": true "enable": true
}, },
"csp": null "csp": null
} }
} }
} }
+8 -1
View File
@@ -21,6 +21,13 @@
} }
}, },
"app": { "app": {
"windows": [] "windows": [],
"security": {
"capabilities": [
"desktop-capability",
"desktop-windows-capability",
"migrated"
]
}
} }
} }
+3 -3
View File
@@ -4,9 +4,9 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", font-family:
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
sans-serif; "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
user-select: none; user-select: none;
+44 -47
View File
@@ -1,20 +1,17 @@
import React, { useState, useEffect } from 'react'; import React, { useSyncExternalStore } from "react";
import { Snackbar, Alert, IconButton, Box } from '@mui/material'; import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import { CloseRounded } from '@mui/icons-material'; import { CloseRounded } from "@mui/icons-material";
import { subscribeNotices, hideNotice, NoticeItem } from '@/services/noticeService'; import {
subscribeNotices,
hideNotice,
getSnapshotNotices,
} from "@/services/noticeService";
export const NoticeManager: React.FC = () => { export const NoticeManager: React.FC = () => {
const [currentNotices, setCurrentNotices] = useState<NoticeItem[]>([]); const currentNotices = useSyncExternalStore(
subscribeNotices,
useEffect(() => { getSnapshotNotices,
const unsubscribe = subscribeNotices((notices) => { );
setCurrentNotices(notices);
});
return () => {
unsubscribe();
};
}, []);
const handleClose = (id: number) => { const handleClose = (id: number) => {
hideNotice(id); hideNotice(id);
@@ -23,49 +20,49 @@ export const NoticeManager: React.FC = () => {
return ( return (
<Box <Box
sx={{ sx={{
position: 'fixed', position: "fixed",
top: '20px', top: "20px",
right: '20px', right: "20px",
zIndex: 1500, zIndex: 1500,
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: '10px', gap: "10px",
maxWidth: '360px', maxWidth: "360px",
}} }}
> >
{currentNotices.map((notice) => ( {currentNotices.map((notice) => (
<Snackbar <Snackbar
key={notice.id} key={notice.id}
open={true} open={true}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }} anchorOrigin={{ vertical: "top", horizontal: "right" }}
sx={{ sx={{
position: 'relative', position: "relative",
transform: 'none', transform: "none",
top: 'auto', top: "auto",
right: 'auto', right: "auto",
bottom: 'auto', bottom: "auto",
left: 'auto', left: "auto",
width: '100%', width: "100%",
}} }}
> >
<Alert <Alert
severity={notice.type} severity={notice.type}
variant="filled" variant="filled"
sx={{ width: '100%' }} sx={{ width: "100%" }}
action={ action={
<IconButton <IconButton
size="small" size="small"
color="inherit" color="inherit"
onClick={() => handleClose(notice.id)} onClick={() => handleClose(notice.id)}
> >
<CloseRounded fontSize="inherit" /> <CloseRounded fontSize="inherit" />
</IconButton> </IconButton>
} }
> >
{notice.message} {notice.message}
</Alert> </Alert>
</Snackbar> </Snackbar>
))} ))}
</Box> </Box>
); );
}; };
+1 -1
View File
@@ -157,7 +157,7 @@ export const BaseSearchBox = (props: SearchProps) => {
</Tooltip> </Tooltip>
</Box> </Box>
), ),
} },
}} }}
/> />
</Tooltip> </Tooltip>
@@ -107,7 +107,14 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{information.map((each) => ( {information.map((each) => (
<div key={each.label}> <div key={each.label}>
<b>{each.label}</b> <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> </div>
))} ))}
+1 -1
View File
@@ -25,7 +25,7 @@ export const ClashInfoCard = () => {
// 使用备忘录组件内容,减少重新渲染 // 使用备忘录组件内容,减少重新渲染
const cardContent = useMemo(() => { const cardContent = useMemo(() => {
if (!clashConfig) return null; if (!clashConfig) return null;
return ( return (
<Stack spacing={1.5}> <Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between"> <Stack direction="row" justifyContent="space-between">
+32 -23
View File
@@ -23,12 +23,24 @@ export const ClashModeCard = () => {
// 直接使用API返回的模式,不维护本地状态 // 直接使用API返回的模式,不维护本地状态
const currentMode = clashConfig?.mode?.toLowerCase(); 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(() => ({ const modeIcons = useMemo(
rule: <MultipleStopRounded fontSize="small" />, () => ({
global: <LanguageRounded fontSize="small" />, rule: <MultipleStopRounded fontSize="small" />,
direct: <DirectionsRounded fontSize="small" /> global: <LanguageRounded fontSize="small" />,
}), []); direct: <DirectionsRounded fontSize="small" />,
}),
[],
);
// 切换模式的处理函数 // 切换模式的处理函数
const onChangeMode = useLockFn(async (mode: string) => { const onChangeMode = useLockFn(async (mode: string) => {
@@ -68,18 +80,19 @@ export const ClashModeCard = () => {
"&:active": { "&:active": {
transform: "translateY(1px)", transform: "translateY(1px)",
}, },
"&::after": mode === currentMode "&::after":
? { mode === currentMode
content: '""', ? {
position: "absolute", content: '""',
bottom: -16, position: "absolute",
left: "50%", bottom: -16,
width: 2, left: "50%",
height: 16, width: 2,
bgcolor: "primary.main", height: 16,
transform: "translateX(-50%)", bgcolor: "primary.main",
} transform: "translateX(-50%)",
: {}, }
: {},
}); });
// 描述样式 // 描述样式
@@ -143,12 +156,8 @@ export const ClashModeCard = () => {
overflow: "visible", overflow: "visible",
}} }}
> >
<Typography <Typography variant="caption" component="div" sx={descriptionStyles}>
variant="caption" {modeDescription}
component="div"
sx={descriptionStyles}
>
{t(`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`)}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
+48 -32
View File
@@ -105,7 +105,7 @@ export const CurrentProxyCard = () => {
// 添加排序类型状态 // 添加排序类型状态
const [sortType, setSortType] = useState<ProxySortType>(() => { const [sortType, setSortType] = useState<ProxySortType>(() => {
const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE); 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) => primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()), 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 || ""; return primaryGroup?.name || "";
}; };
@@ -200,11 +201,13 @@ export const CurrentProxyCard = () => {
// 只保留 Selector 类型的组用于选择 // 只保留 Selector 类型的组用于选择
const filteredGroups = proxies.groups const filteredGroups = proxies.groups
.filter((g: { name: string; type?: string }) => g.type === "Selector") .filter((g: { name: string; type?: string }) => g.type === "Selector")
.map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({ .map(
name: g.name, (g: { name: string; now: string; all: Array<{ name: string }> }) => ({
now: g.now || "", name: g.name,
all: g.all.map((p: { name: string }) => p.name), now: g.now || "",
})); all: g.all.map((p: { name: string }) => p.name),
}),
);
let newProxy = ""; let newProxy = "";
let newDisplayProxy = null; let newDisplayProxy = null;
@@ -230,12 +233,12 @@ export const CurrentProxyCard = () => {
if (selectorGroup) { if (selectorGroup) {
newGroup = selectorGroup.name; newGroup = selectorGroup.name;
newProxy = selectorGroup.now || selectorGroup.all[0] || ""; newProxy = selectorGroup.now || selectorGroup.all[0] || "";
newDisplayProxy = proxies.records?.[newProxy] || null; newDisplayProxy = proxies.records?.[newProxy] || null;
if (!isGlobalMode && !isDirectMode) { if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup); localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) { if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy); localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
} }
} }
} }
@@ -280,7 +283,9 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup); localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
setState((prev) => { 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) { if (group) {
return { return {
...prev, ...prev,
@@ -368,14 +373,16 @@ export const CurrentProxyCard = () => {
}, [state.displayProxy]); }, [state.displayProxy]);
// 获取当前节点的延迟(增加非空校验) // 获取当前节点的延迟(增加非空校验)
const currentDelay = currentProxy && state.selection.group const currentDelay =
? delayManager.getDelayFix(currentProxy, state.selection.group) currentProxy && state.selection.group
: -1; ? delayManager.getDelayFix(currentProxy, state.selection.group)
: -1;
// 信号图标(增加非空校验) // 信号图标(增加非空校验)
const signalInfo = currentProxy && state.selection.group const signalInfo =
? getSignalIcon(currentDelay) currentProxy && state.selection.group
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" }; ? getSignalIcon(currentDelay)
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
// 自定义渲染选择框中的值 // 自定义渲染选择框中的值
const renderProxyValue = useCallback( const renderProxyValue = useCallback(
@@ -384,7 +391,7 @@ export const CurrentProxyCard = () => {
const delayValue = delayManager.getDelayFix( const delayValue = delayManager.getDelayFix(
state.proxyData.records[selected], state.proxyData.records[selected],
state.selection.group state.selection.group,
); );
return ( return (
@@ -441,7 +448,7 @@ export const CurrentProxyCard = () => {
return list; 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) { if (isGlobalMode && proxies?.global) {
const options = proxies.global.all const options = proxies.global.all
.filter((p: any) => { .filter((p: any) => {
const name = typeof p === 'string' ? p : p.name; const name = typeof p === "string" ? p : p.name;
return name !== "DIRECT" && name !== "REJECT"; return name !== "DIRECT" && name !== "REJECT";
}) })
.map((p: any) => ({ .map((p: any) => ({
name: typeof p === 'string' ? p : p.name name: typeof p === "string" ? p : p.name,
})); }));
return sortProxies(options); return sortProxies(options);
@@ -464,7 +471,7 @@ export const CurrentProxyCard = () => {
// 规则模式 // 规则模式
const group = state.selection.group 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; : null;
if (group) { if (group) {
@@ -473,7 +480,14 @@ export const CurrentProxyCard = () => {
} }
return []; return [];
}, [isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortProxies]); }, [
isDirectMode,
isGlobalMode,
proxies,
state.proxyData,
state.selection.group,
sortProxies,
]);
// 获取排序图标 // 获取排序图标
const getSortIcon = () => { const getSortIcon = () => {
@@ -660,12 +674,14 @@ export const CurrentProxyCard = () => {
{isDirectMode {isDirectMode
? null ? null
: proxyOptions.map((proxy, index) => { : proxyOptions.map((proxy, index) => {
const delayValue = state.proxyData.records[proxy.name] && state.selection.group const delayValue =
? delayManager.getDelayFix( state.proxyData.records[proxy.name] &&
state.proxyData.records[proxy.name], state.selection.group
state.selection.group, ? delayManager.getDelayFix(
) state.proxyData.records[proxy.name],
: -1; state.selection.group,
)
: -1;
return ( return (
<MenuItem <MenuItem
key={`${proxy.name}-${index}`} key={`${proxy.name}-${index}`}
@@ -706,4 +722,4 @@ export const CurrentProxyCard = () => {
)} )}
</EnhancedCard> </EnhancedCard>
); );
}; };
+14 -14
View File
@@ -38,7 +38,7 @@ export const EnhancedCard = ({
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
display: "block" display: "block",
}; };
return ( return (
@@ -62,13 +62,15 @@ export const EnhancedCard = ({
borderColor: "divider", borderColor: "divider",
}} }}
> >
<Box sx={{ <Box
display: "flex", sx={{
alignItems: "center", display: "flex",
minWidth: 0, alignItems: "center",
flex: 1, minWidth: 0,
overflow: "hidden" flex: 1,
}}> overflow: "hidden",
}}
>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@@ -87,9 +89,9 @@ export const EnhancedCard = ({
</Box> </Box>
<Box sx={{ minWidth: 0, flex: 1 }}> <Box sx={{ minWidth: 0, flex: 1 }}>
{typeof title === "string" ? ( {typeof title === "string" ? (
<Typography <Typography
variant="h6" variant="h6"
fontWeight="medium" fontWeight="medium"
fontSize={18} fontSize={18}
sx={titleTruncateStyle} sx={titleTruncateStyle}
title={title} title={title}
@@ -97,9 +99,7 @@ export const EnhancedCard = ({
{title} {title}
</Typography> </Typography>
) : ( ) : (
<Box sx={titleTruncateStyle}> <Box sx={titleTruncateStyle}>{title}</Box>
{title}
</Box>
)} )}
</Box> </Box>
</Box> </Box>
+231 -179
View File
@@ -30,7 +30,7 @@ ChartJS.register(
PointElement, PointElement,
LineElement, LineElement,
Tooltip, Tooltip,
Filler Filler,
); );
// 流量数据项接口 // 流量数据项接口
@@ -54,8 +54,8 @@ type DataPoint = ITrafficItem & { name: string; timestamp: number };
/** /**
* 增强型流量图表组件 * 增强型流量图表组件
*/ */
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>( export const EnhancedTrafficGraph = memo(
(props, ref) => { forwardRef<EnhancedTrafficGraphRef>((props, ref) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -63,20 +63,20 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
const [timeRange, setTimeRange] = useState<TimeRange>(10); const [timeRange, setTimeRange] = useState<TimeRange>(10);
const [chartStyle, setChartStyle] = useState<"line" | "area">("area"); const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
const [displayData, setDisplayData] = useState<DataPoint[]>([]); const [displayData, setDisplayData] = useState<DataPoint[]>([]);
// 数据缓冲区 // 数据缓冲区
const dataBufferRef = useRef<DataPoint[]>([]); const dataBufferRef = useRef<DataPoint[]>([]);
// 根据时间范围计算保留的数据点数量 // 根据时间范围计算保留的数据点数量
const getMaxPointsByTimeRange = useCallback( const getMaxPointsByTimeRange = useCallback(
(minutes: TimeRange): number => minutes * 60, (minutes: TimeRange): number => minutes * 60,
[] [],
); );
// 最大数据点数量 // 最大数据点数量
const MAX_BUFFER_SIZE = useMemo( const MAX_BUFFER_SIZE = useMemo(
() => getMaxPointsByTimeRange(10), () => getMaxPointsByTimeRange(10),
[getMaxPointsByTimeRange] [getMaxPointsByTimeRange],
); );
// 颜色配置 // 颜色配置
@@ -89,23 +89,28 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
text: theme.palette.text.primary, text: theme.palette.text.primary,
tooltipBorder: theme.palette.divider, tooltipBorder: theme.palette.divider,
}), }),
[theme] [theme],
); );
// 切换时间范围 // 切换时间范围
const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => { const handleTimeRangeClick = useCallback(
event.stopPropagation(); (event: React.MouseEvent<SVGTextElement>) => {
setTimeRange((prevRange) => { event.stopPropagation();
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1; 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 handleToggleStyleClick = useCallback(
(event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
event.stopPropagation();
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
},
[],
);
// 初始化数据缓冲区 // 初始化数据缓冲区
useEffect(() => { useEffect(() => {
@@ -121,7 +126,9 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
let nameValue: string; let nameValue: string;
try { try {
if (isNaN(date.getTime())) { 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 = "??:??:??"; nameValue = "??:??:??";
} else { } else {
nameValue = date.toLocaleTimeString("en-US", { nameValue = date.toLocaleTimeString("en-US", {
@@ -132,7 +139,14 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}); });
} }
} catch (e) { } 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"; nameValue = "Err:Time";
} }
@@ -142,55 +156,66 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
timestamp: pointTime, timestamp: pointTime,
name: nameValue, name: nameValue,
}; };
} },
); );
dataBufferRef.current = initialBuffer; dataBufferRef.current = initialBuffer;
// 更新显示数据 // 更新显示数据
const pointsToShow = getMaxPointsByTimeRange(timeRange); const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(initialBuffer.slice(-pointsToShow)); setDisplayData(initialBuffer.slice(-pointsToShow));
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]); }, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
// 添加数据点方法 // 添加数据点方法
const appendData = useCallback((data: ITrafficItem) => { const appendData = useCallback(
const safeData = { (data: ITrafficItem) => {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0, const safeData = {
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0, 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 timestamp = data.timestamp || Date.now();
const date = new Date(timestamp); const date = new Date(timestamp);
let nameValue: string; let nameValue: string;
try { try {
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
console.warn(`appendData: Invalid date for timestamp ${timestamp}`); console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
nameValue = "??:??:??"; nameValue = "??:??:??";
} else { } else {
nameValue = date.toLocaleTimeString("en-US", { nameValue = date.toLocaleTimeString("en-US", {
hour12: false, hour12: false,
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
second: "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); const newPoint: DataPoint = {
nameValue = "Err:Time"; ...safeData,
} name: nameValue,
// 带时间标签的新数据点 timestamp: timestamp,
const newPoint: DataPoint = { };
...safeData,
name: nameValue,
timestamp: timestamp,
};
const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer; dataBufferRef.current = newBuffer;
const pointsToShow = getMaxPointsByTimeRange(timeRange); const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow)); setDisplayData(newBuffer.slice(-pointsToShow));
}, [timeRange, getMaxPointsByTimeRange]); },
[timeRange, getMaxPointsByTimeRange],
);
// 监听时间范围变化 // 监听时间范围变化
useEffect(() => { useEffect(() => {
@@ -202,7 +227,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
// 切换图表样式 // 切换图表样式
const toggleStyle = useCallback(() => { 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, appendData,
toggleStyle, toggleStyle,
}), }),
[appendData, toggleStyle] [appendData, toggleStyle],
); );
const formatYAxis = useCallback((value: number | string): string => { 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); const [num, unit] = parseTraffic(value);
return `${num}${unit}`; return `${num}${unit}`;
}, []); }, []);
const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => { const formatXLabel = useCallback(
const dataPoint = displayData[index as number]; (tickValue: string | number, index: number, ticks: any[]) => {
if (dataPoint && dataPoint.name) { const dataPoint = displayData[index as number];
const parts = dataPoint.name.split(":"); if (dataPoint && dataPoint.name) {
return `${parts[0]}:${parts[1]}`; const parts = dataPoint.name.split(":");
} return `${parts[0]}:${parts[1]}`;
if(typeof tickValue === 'string') { }
const parts = tickValue.split(":"); if (typeof tickValue === "string") {
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`; const parts = tickValue.split(":");
return tickValue; if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
} return tickValue;
return ''; }
}, [displayData]); return "";
},
[displayData],
);
// 获取当前时间范围文本 // 获取当前时间范围文本
const getTimeRangeText = useCallback(() => { const getTimeRangeText = useCallback(() => {
@@ -243,13 +269,13 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}, [timeRange, t]); }, [timeRange, t]);
const chartData = useMemo(() => { const chartData = useMemo(() => {
const labels = displayData.map(d => d.name); const labels = displayData.map((d) => d.name);
return { return {
labels, labels,
datasets: [ datasets: [
{ {
label: t("Upload"), label: t("Upload"),
data: displayData.map(d => d.up), data: displayData.map((d) => d.up),
borderColor: colors.up, borderColor: colors.up,
backgroundColor: chartStyle === "area" ? colors.up : colors.up, backgroundColor: chartStyle === "area" ? colors.up : colors.up,
fill: chartStyle === "area", fill: chartStyle === "area",
@@ -260,7 +286,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}, },
{ {
label: t("Download"), label: t("Download"),
data: displayData.map(d => d.down), data: displayData.map((d) => d.down),
borderColor: colors.down, borderColor: colors.down,
backgroundColor: chartStyle === "area" ? colors.down : colors.down, backgroundColor: chartStyle === "area" ? colors.down : colors.down,
fill: chartStyle === "area", fill: chartStyle === "area",
@@ -268,113 +294,130 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 4, pointHoverRadius: 4,
borderWidth: 2, borderWidth: 2,
} },
] ],
}; };
}, [displayData, colors.up, colors.down, t, chartStyle]); }, [displayData, colors.up, colors.down, t, chartStyle]);
const chartOptions = useMemo(() => ({ const chartOptions = useMemo(
responsive: true, () => ({
maintainAspectRatio: false, responsive: true,
animation: false as false, maintainAspectRatio: false,
scales: { animation: false as false,
x: { scales: {
display: true, x: {
type: 'category' as const,
labels: displayData.map(d => d.name),
ticks: {
display: true, display: true,
color: colors.text, type: "category" as const,
font: { size: 10 }, labels: displayData.map((d) => d.name),
callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined { ticks: {
let labelToFormat: string | undefined = undefined; 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]; const currentDisplayTick = ticks[index];
if (currentDisplayTick && typeof currentDisplayTick.label === 'string') { if (
labelToFormat = currentDisplayTick.label; currentDisplayTick &&
} else { typeof currentDisplayTick.label === "string"
const sourceLabels = displayData.map(d => d.name); ) {
if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) { labelToFormat = currentDisplayTick.label;
labelToFormat = sourceLabels[tickValue]; } else {
} else if (typeof tickValue === 'string') { const sourceLabels = displayData.map((d) => d.name);
labelToFormat = tickValue; if (
typeof tickValue === "number" &&
tickValue >= 0 &&
tickValue < sourceLabels.length
) {
labelToFormat = sourceLabels[tickValue];
} else if (typeof tickValue === "string") {
labelToFormat = tickValue;
}
} }
}
if (typeof labelToFormat !== 'string') { if (typeof labelToFormat !== "string") {
return undefined; return undefined;
} }
const parts: string[] = labelToFormat.split(':'); const parts: string[] = labelToFormat.split(":");
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat; 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: { y: {
display: true, beginAtZero: true,
drawOnChartArea: false, ticks: {
drawTicks: true, color: colors.text,
tickLength: 2, font: { size: 10 },
color: colors.text, callback: formatYAxis,
},
grid: {
display: true,
drawTicks: true,
tickLength: 3,
color: colors.grid,
},
}, },
}, },
y: { plugins: {
beginAtZero: true, tooltip: {
ticks: { enabled: true,
color: colors.text, mode: "index" as const,
font: { size: 10 }, intersect: false,
callback: formatYAxis, backgroundColor: colors.tooltipBg,
}, titleColor: colors.text,
grid: { bodyColor: colors.text,
display: true, borderColor: colors.tooltipBorder,
drawTicks: true, borderWidth: 1,
tickLength: 3, cornerRadius: 4,
color: colors.grid, padding: 8,
callbacks: {
}, title: (tooltipItems: any[]) => {
} return `${t("Time")}: ${tooltipItems[0].label}`;
}, },
plugins: { label: (context: any): string => {
tooltip: { const label = context.dataset.label || "";
enabled: true, const value = context.parsed.y;
mode: 'index' as const, const [num, unit] = parseTraffic(value);
intersect: false, return `${label}: ${num} ${unit}/s`;
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 || ''; legend: {
const value = context.parsed.y; display: false,
const [num, unit] = parseTraffic(value); },
return `${label}: ${num} ${unit}/s`;
}
}
}, },
legend: { layout: {
display: false padding: {
} top: 16,
}, right: 7,
layout: { left: 3,
padding: { },
top: 16, },
right: 7, }),
left: 3, [colors, t, formatYAxis, timeRange, displayData],
} );
}
}), [colors, t, formatYAxis, timeRange, displayData]);
return ( return (
<Box <Box
@@ -392,8 +435,17 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
{displayData.length > 0 && ( {displayData.length > 0 && (
<ChartJsLine data={chartData} options={chartOptions} /> <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 <text
x="3.5%" x="3.5%"
y="10%" y="10%"
@@ -402,11 +454,11 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={11} fontSize={11}
fontWeight="bold" fontWeight="bold"
onClick={handleTimeRangeClick} onClick={handleTimeRangeClick}
style={{ cursor: "pointer", pointerEvents: 'all' }} style={{ cursor: "pointer", pointerEvents: "all" }}
> >
{getTimeRangeText()} {getTimeRangeText()}
</text> </text>
<text <text
x="99%" x="99%"
y="10%" y="10%"
@@ -415,7 +467,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={12} fontSize={12}
fontWeight="bold" fontWeight="bold"
onClick={handleToggleStyleClick} onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: 'all' }} style={{ cursor: "pointer", pointerEvents: "all" }}
> >
{t("Upload")} {t("Upload")}
</text> </text>
@@ -428,7 +480,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={12} fontSize={12}
fontWeight="bold" fontWeight="bold"
onClick={handleToggleStyleClick} onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: 'all' }} style={{ cursor: "pointer", pointerEvents: "all" }}
> >
{t("Download")} {t("Download")}
</text> </text>
@@ -436,7 +488,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
</div> </div>
</Box> </Box>
); );
}, }),
)); );
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph"; EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";
+213 -155
View File
@@ -66,85 +66,90 @@ const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新 const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
// 统计卡片组件 - 使用memo优化 // 统计卡片组件 - 使用memo优化
const CompactStatCard = memo(({ const CompactStatCard = memo(
icon, ({ icon, title, value, unit, color, onClick }: StatCardProps) => {
title, const theme = useTheme();
value,
unit,
color,
onClick,
}: StatCardProps) => {
const theme = useTheme();
// 获取调色板颜色 - 使用useMemo避免重复计算 // 获取调色板颜色 - 使用useMemo避免重复计算
const colorValue = useMemo(() => { const colorValue = useMemo(() => {
const palette = theme.palette; const palette = theme.palette;
if ( if (
color in palette && color in palette &&
palette[color as keyof typeof palette] && palette[color as keyof typeof palette] &&
"main" in (palette[color as keyof typeof palette] as PaletteColor) "main" in (palette[color as keyof typeof palette] as PaletteColor)
) { ) {
return (palette[color as keyof typeof palette] as PaletteColor).main; return (palette[color as keyof typeof palette] as PaletteColor).main;
} }
return palette.primary.main; return palette.primary.main;
}, [theme.palette, color]); }, [theme.palette, color]);
return ( return (
<Paper <Paper
elevation={0} 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"
sx={{ sx={{
mr: 1,
ml: "2px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", borderRadius: 2,
width: 32, bgcolor: alpha(colorValue, 0.05),
height: 32, border: `1px solid ${alpha(colorValue, 0.15)}`,
borderRadius: "50%", padding: "8px",
bgcolor: alpha(colorValue, 0.1), transition: "all 0.2s ease-in-out",
color: colorValue, 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={{
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}> mr: 1,
<Typography variant="caption" color="text.secondary" noWrap> ml: "2px",
{title} display: "flex",
</Typography> alignItems: "center",
<Grid component="div" sx={{ display: "flex", alignItems: "baseline" }}> justifyContent: "center",
<Typography variant="body1" fontWeight="bold" noWrap sx={{ mr: 0.5 }}> width: 32,
{value} height: 32,
</Typography> borderRadius: "50%",
<Typography variant="caption" color="text.secondary"> bgcolor: alpha(colorValue, 0.1),
{unit} color: colorValue,
</Typography> }}
>
{icon}
</Grid> </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"; CompactStatCard.displayName = "CompactStatCard";
@@ -205,25 +210,25 @@ export const EnhancedTrafficStats = () => {
down: data.down, down: data.down,
timestamp: now, timestamp: now,
}); });
} catch { } } catch {}
return; return;
} }
lastUpdateRef.current.traffic = now; lastUpdateRef.current.traffic = now;
const safeUp = isNaN(data.up) ? 0 : data.up; const safeUp = isNaN(data.up) ? 0 : data.up;
const safeDown = isNaN(data.down) ? 0 : data.down; const safeDown = isNaN(data.down) ? 0 : data.down;
try { try {
setStats(prev => ({ setStats((prev) => ({
...prev, ...prev,
traffic: { up: safeUp, down: safeDown } traffic: { up: safeUp, down: safeDown },
})); }));
} catch { } } catch {}
try { try {
trafficRef.current?.appendData({ trafficRef.current?.appendData({
up: safeUp, up: safeUp,
down: safeDown, down: safeDown,
timestamp: now, timestamp: now,
}); });
} catch { } } catch {}
} }
} catch (err) { } catch (err) {
console.error("[Traffic] 解析数据错误:", err, event.data); console.error("[Traffic] 解析数据错误:", err, event.data);
@@ -235,12 +240,12 @@ export const EnhancedTrafficStats = () => {
try { try {
const data = JSON.parse(event.data) as MemoryUsage; const data = JSON.parse(event.data) as MemoryUsage;
if (data && typeof data.inuse === "number") { if (data && typeof data.inuse === "number") {
setStats(prev => ({ setStats((prev) => ({
...prev, ...prev,
memory: { memory: {
inuse: isNaN(data.inuse) ? 0 : data.inuse, inuse: isNaN(data.inuse) ? 0 : data.inuse,
oslimit: data.oslimit, oslimit: data.oslimit,
} },
})); }));
} }
} catch (err) { } catch (err) {
@@ -257,7 +262,7 @@ export const EnhancedTrafficStats = () => {
// 清理现有连接的函数 // 清理现有连接的函数
const cleanupSockets = () => { const cleanupSockets = () => {
Object.values(socketRefs.current).forEach(socket => { Object.values(socketRefs.current).forEach((socket) => {
if (socket) { if (socket) {
socket.close(); socket.close();
} }
@@ -269,40 +274,78 @@ export const EnhancedTrafficStats = () => {
cleanupSockets(); cleanupSockets();
// 创建新连接 // 创建新连接
console.log(`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`); console.log(
socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, { `[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
onmessage: handleTrafficUpdate, );
onopen: (event) => { socketRefs.current.traffic = createAuthSockette(
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event); `${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, { socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
onmessage: handleMemoryUpdate, onmessage: handleMemoryUpdate,
onopen: (event) => { onopen: (event) => {
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event); console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
}, },
onerror: (event) => { onerror: (event) => {
console.error(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event); console.error(
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } })); `[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
}, },
onclose: (event) => { 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) { if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`); console.warn(
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } })); `[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
} }
}, },
}); });
@@ -314,11 +357,11 @@ export const EnhancedTrafficStats = () => {
useEffect(() => { useEffect(() => {
return () => { return () => {
try { try {
Object.values(socketRefs.current).forEach(socket => { Object.values(socketRefs.current).forEach((socket) => {
if (socket) socket.close(); if (socket) socket.close();
}); });
socketRefs.current = { traffic: null, memory: null }; socketRefs.current = { traffic: null, memory: null };
} catch { } } catch {}
}; };
}, []); }, []);
@@ -339,13 +382,25 @@ export const EnhancedTrafficStats = () => {
const [up, upUnit] = parseTraffic(stats.traffic.up); const [up, upUnit] = parseTraffic(stats.traffic.up);
const [down, downUnit] = parseTraffic(stats.traffic.down); const [down, downUnit] = parseTraffic(stats.traffic.down);
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse); const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal); const [uploadTotal, uploadTotalUnit] = parseTraffic(
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal); connections.uploadTotal,
);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
connections.downloadTotal,
);
return { return {
up, upUnit, down, downUnit, inuse, inuseUnit, up,
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit, upUnit,
connectionsCount: connections.count down,
downUnit,
inuse,
inuseUnit,
uploadTotal,
uploadTotalUnit,
downloadTotal,
downloadTotalUnit,
connectionsCount: connections.count,
}; };
}, [stats, connections]); }, [stats, connections]);
@@ -392,51 +447,54 @@ export const EnhancedTrafficStats = () => {
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]); }, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
// 使用useMemo计算统计卡片配置 // 使用useMemo计算统计卡片配置
const statCards = useMemo(() => [ const statCards = useMemo(
{ () => [
icon: <ArrowUpwardRounded fontSize="small" />, {
title: t("Upload Speed"), icon: <ArrowUpwardRounded fontSize="small" />,
value: parsedData.up, title: t("Upload Speed"),
unit: `${parsedData.upUnit}/s`, value: parsedData.up,
color: "secondary" as const, unit: `${parsedData.upUnit}/s`,
}, color: "secondary" as const,
{ },
icon: <ArrowDownwardRounded fontSize="small" />, {
title: t("Download Speed"), icon: <ArrowDownwardRounded fontSize="small" />,
value: parsedData.down, title: t("Download Speed"),
unit: `${parsedData.downUnit}/s`, value: parsedData.down,
color: "primary" as const, unit: `${parsedData.downUnit}/s`,
}, color: "primary" as const,
{ },
icon: <LinkRounded fontSize="small" />, {
title: t("Active Connections"), icon: <LinkRounded fontSize="small" />,
value: parsedData.connectionsCount, title: t("Active Connections"),
unit: "", value: parsedData.connectionsCount,
color: "success" as const, unit: "",
}, color: "success" as const,
{ },
icon: <CloudUploadRounded fontSize="small" />, {
title: t("Uploaded"), icon: <CloudUploadRounded fontSize="small" />,
value: parsedData.uploadTotal, title: t("Uploaded"),
unit: parsedData.uploadTotalUnit, value: parsedData.uploadTotal,
color: "secondary" as const, unit: parsedData.uploadTotalUnit,
}, color: "secondary" as const,
{ },
icon: <CloudDownloadRounded fontSize="small" />, {
title: t("Downloaded"), icon: <CloudDownloadRounded fontSize="small" />,
value: parsedData.downloadTotal, title: t("Downloaded"),
unit: parsedData.downloadTotalUnit, value: parsedData.downloadTotal,
color: "primary" as const, unit: parsedData.downloadTotalUnit,
}, color: "primary" as const,
{ },
icon: <MemoryRounded fontSize="small" />, {
title: t("Memory Usage"), icon: <MemoryRounded fontSize="small" />,
value: parsedData.inuse, title: t("Memory Usage"),
unit: parsedData.inuseUnit, value: parsedData.inuse,
color: "error" as const, unit: parsedData.inuseUnit,
onClick: isDebug ? handleGarbageCollection : undefined, color: "error" as const,
}, onClick: isDebug ? handleGarbageCollection : undefined,
], [t, parsedData, isDebug, handleGarbageCollection]); },
],
[t, parsedData, isDebug, handleGarbageCollection],
);
return ( return (
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}> <Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
+48 -30
View File
@@ -78,12 +78,16 @@ const truncateStyle = {
maxWidth: "calc(100% - 28px)", maxWidth: "calc(100% - 28px)",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap" whiteSpace: "nowrap",
}; };
// 提取独立组件减少主组件复杂度 // 提取独立组件减少主组件复杂度
const ProfileDetails = ({ current, onUpdateProfile, updating }: { const ProfileDetails = ({
current: ProfileItem; current,
onUpdateProfile,
updating,
}: {
current: ProfileItem;
onUpdateProfile: () => void; onUpdateProfile: () => void;
updating: boolean; updating: boolean;
}) => { }) => {
@@ -99,7 +103,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
if (!current.extra || !current.extra.total) return 1; if (!current.extra || !current.extra.total) return 1;
return Math.min( return Math.min(
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1, Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
100 100,
); );
}, [current.extra, usedTraffic]); }, [current.extra, usedTraffic]);
@@ -109,19 +113,24 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
{current.url && ( {current.url && (
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
<DnsOutlined fontSize="small" color="action" /> <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> <span style={{ flexShrink: 0 }}>{t("From")}: </span>
{current.home ? ( {current.home ? (
<Link <Link
component="button" component="button"
fontWeight="medium" fontWeight="medium"
onClick={() => current.home && openWebUrl(current.home)} onClick={() => current.home && openWebUrl(current.home)}
sx={{ sx={{
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
minWidth: 0, minWidth: 0,
maxWidth: "calc(100% - 40px)", maxWidth: "calc(100% - 40px)",
ml: 0.5 ml: 0.5,
}} }}
title={parseUrl(current.url)} title={parseUrl(current.url)}
> >
@@ -132,14 +141,19 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
minWidth: 0, minWidth: 0,
flex: 1 flex: 1,
}} }}
> >
{parseUrl(current.url)} {parseUrl(current.url)}
</Typography> </Typography>
<LaunchOutlined <LaunchOutlined
fontSize="inherit" 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> </Link>
) : ( ) : (
@@ -152,7 +166,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
whiteSpace: "nowrap", whiteSpace: "nowrap",
minWidth: 0, minWidth: 0,
flex: 1, flex: 1,
ml: 0.5 ml: 0.5,
}} }}
title={parseUrl(current.url)} title={parseUrl(current.url)}
> >
@@ -195,7 +209,8 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("Used / Total")}:{" "} {t("Used / Total")}:{" "}
<Box component="span" fontWeight="medium"> <Box component="span" fontWeight="medium">
{parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)} {parseTraffic(usedTraffic)} /{" "}
{parseTraffic(current.extra.total)}
</Box> </Box>
</Typography> </Typography>
</Stack> </Stack>
@@ -240,7 +255,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
// 提取空配置组件 // 提取空配置组件
const EmptyProfile = ({ onClick }: { onClick: () => void }) => { const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Box <Box
sx={{ 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 { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { refreshAll } = useAppData(); const { refreshAll } = useAppData();
// 更新当前订阅 // 更新当前订阅
const [updating, setUpdating] = useState(false); const [updating, setUpdating] = useState(false);
const onUpdateProfile = useLockFn(async () => { const onUpdateProfile = useLockFn(async () => {
if (!current?.uid) return; if (!current?.uid) return;
setUpdating(true); setUpdating(true);
try { try {
await updateProfile(current.uid, current.option); await updateProfile(current.uid, current.option);
showNotice('success', t("Update subscription successfully"), 1000); showNotice("success", t("Update subscription successfully"), 1000);
onProfileUpdated?.(); onProfileUpdated?.();
// 刷新首页数据 // 刷新首页数据
refreshAll(); refreshAll();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString(), 3000); showNotice("error", err.message || err.toString(), 3000);
} finally { } finally {
setUpdating(false); setUpdating(false);
} }
@@ -302,9 +320,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
// 卡片标题 // 卡片标题
const cardTitle = useMemo(() => { const cardTitle = useMemo(() => {
if (!current) return t("Profiles"); if (!current) return t("Profiles");
if (!current.home) return current.name; if (!current.home) return current.name;
return ( return (
<Link <Link
component="button" component="button"
@@ -323,19 +341,19 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
flex: 1 flex: 1,
} },
}} }}
title={current.name} title={current.name}
> >
<span>{current.name}</span> <span>{current.name}</span>
<LaunchOutlined <LaunchOutlined
fontSize="inherit" fontSize="inherit"
sx={{ sx={{
ml: 0.5, ml: 0.5,
fontSize: "0.8rem", fontSize: "0.8rem",
opacity: 0.7, opacity: 0.7,
flexShrink: 0 flexShrink: 0,
}} }}
/> />
</Link> </Link>
@@ -345,7 +363,7 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
// 卡片操作按钮 // 卡片操作按钮
const cardAction = useMemo(() => { const cardAction = useMemo(() => {
if (!current) return null; if (!current) return null;
return ( return (
<Button <Button
variant="outlined" variant="outlined"
@@ -367,10 +385,10 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
action={cardAction} action={cardAction}
> >
{current ? ( {current ? (
<ProfileDetails <ProfileDetails
current={current} current={current}
onUpdateProfile={onUpdateProfile} onUpdateProfile={onUpdateProfile}
updating={updating} updating={updating}
/> />
) : ( ) : (
<EmptyProfile onClick={goToProfiles} /> <EmptyProfile onClick={goToProfiles} />
+7 -9
View File
@@ -83,28 +83,28 @@ export const IpInfoCard = () => {
// 组件加载时获取IP信息 // 组件加载时获取IP信息
useEffect(() => { useEffect(() => {
fetchIpInfo(); fetchIpInfo();
// 倒计时实现优化,减少不必要的重渲染 // 倒计时实现优化,减少不必要的重渲染
let timer: number | null = null; let timer: number | null = null;
let currentCount = IP_REFRESH_SECONDS; let currentCount = IP_REFRESH_SECONDS;
// 只在必要时更新状态,减少重渲染次数 // 只在必要时更新状态,减少重渲染次数
const startCountdown = () => { const startCountdown = () => {
timer = window.setInterval(() => { timer = window.setInterval(() => {
currentCount -= 1; currentCount -= 1;
if (currentCount <= 0) { if (currentCount <= 0) {
fetchIpInfo(); fetchIpInfo();
currentCount = IP_REFRESH_SECONDS; currentCount = IP_REFRESH_SECONDS;
} }
// 每5秒或倒计时结束时才更新UI // 每5秒或倒计时结束时才更新UI
if (currentCount % 5 === 0 || currentCount <= 0) { if (currentCount % 5 === 0 || currentCount <= 0) {
setCountdown(currentCount); setCountdown(currentCount);
} }
}, 1000); }, 1000);
}; };
startCountdown(); startCountdown();
return () => { return () => {
if (timer) clearInterval(timer); if (timer) clearInterval(timer);
@@ -112,7 +112,7 @@ export const IpInfoCard = () => {
}, [fetchIpInfo]); }, [fetchIpInfo]);
const toggleShowIp = useCallback(() => { 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("ORG")} value={ipInfo?.asn_organization} />
<InfoItem <InfoItem
label={t("Location")} label={t("Location")}
value={[ipInfo?.city, ipInfo?.region] value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
.filter(Boolean)
.join(", ")}
/> />
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} /> <InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
</Box> </Box>
+71 -52
View File
@@ -1,19 +1,24 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Typography, Stack, Divider, Chip, IconButton, Tooltip } from "@mui/material"; import {
import { Typography,
InfoOutlined, Stack,
SettingsOutlined, Divider,
WarningOutlined, Chip,
IconButton,
Tooltip,
} from "@mui/material";
import {
InfoOutlined,
SettingsOutlined,
WarningOutlined,
AdminPanelSettingsOutlined, AdminPanelSettingsOutlined,
DnsOutlined, DnsOutlined,
ExtensionOutlined ExtensionOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { EnhancedCard } from "./enhanced-card"; import { EnhancedCard } from "./enhanced-card";
import useSWR from "swr"; import useSWR from "swr";
import { import { getSystemInfo } from "@/services/cmds";
getSystemInfo,
} from "@/services/cmds";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { version as appVersion } from "@root/package.json"; import { version as appVersion } from "@root/package.json";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
@@ -30,32 +35,35 @@ export const SystemInfoCard = () => {
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState(); const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
const { installServiceAndRestartCore } = useServiceInstaller(); const { installServiceAndRestartCore } = useServiceInstaller();
// 系统信息状态 // 系统信息状态
const [systemState, setSystemState] = useState({ const [systemState, setSystemState] = useState({
osInfo: "", osInfo: "",
lastCheckUpdate: "-", lastCheckUpdate: "-",
}); });
// 初始化系统信息 // 初始化系统信息
useEffect(() => { useEffect(() => {
getSystemInfo() getSystemInfo()
.then((info) => { .then((info) => {
const lines = info.split("\n"); const lines = info.split("\n");
if (lines.length > 0) { if (lines.length > 0) {
const sysName = lines[0].split(": ")[1] || ""; const sysName = lines[0].split(": ")[1] || "";
let sysVersion = lines[1].split(": ")[1] || ""; let sysVersion = lines[1].split(": ")[1] || "";
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) { if (
sysVersion = sysVersion.substring(sysName.length).trim(); sysName &&
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
) {
sysVersion = sysVersion.substring(sysName.length).trim();
}
setSystemState((prev) => ({
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
} }
})
setSystemState((prev) => ({ .catch(console.error);
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
}
})
.catch(console.error);
// 获取最后检查更新时间 // 获取最后检查更新时间
const lastCheck = localStorage.getItem("last_check_update"); const lastCheck = localStorage.getItem("last_check_update");
@@ -122,7 +130,6 @@ useEffect(() => {
} }
}, [verge, patchVerge]); }, [verge, patchVerge]);
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务 // 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
const handleRunningModeClick = useCallback(() => { const handleRunningModeClick = useCallback(() => {
if (isSidecarMode || (isAdminMode && isSidecarMode)) { if (isSidecarMode || (isAdminMode && isSidecarMode)) {
@@ -135,13 +142,13 @@ useEffect(() => {
try { try {
const info = await checkUpdate(); const info = await checkUpdate();
if (!info?.available) { if (!info?.available) {
showNotice('success', t("Currently on the Latest Version")); showNotice("success", t("Currently on the Latest Version"));
} else { } else {
showNotice('info', t("Update Available"), 2000); showNotice("info", t("Update Available"), 2000);
goToSettings(); goToSettings();
} }
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -155,13 +162,15 @@ useEffect(() => {
const runningModeStyle = useMemo( const runningModeStyle = useMemo(
() => ({ () => ({
// Sidecar或纯管理员模式允许安装服务 // Sidecar或纯管理员模式允许安装服务
cursor: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "pointer" : "default", cursor:
textDecoration: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "underline" : "none", isSidecarMode || (isAdminMode && isSidecarMode) ? "pointer" : "default",
textDecoration:
isSidecarMode || (isAdminMode && isSidecarMode) ? "underline" : "none",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: 0.5, gap: 0.5,
"&:hover": { "&:hover": {
opacity: (isSidecarMode || (isAdminMode && isSidecarMode)) ? 0.7 : 1, opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
}, },
}), }),
[isSidecarMode, isAdminMode], [isSidecarMode, isAdminMode],
@@ -174,34 +183,34 @@ useEffect(() => {
if (!isSidecarMode) { if (!isSidecarMode) {
return ( return (
<> <>
<AdminPanelSettingsOutlined <AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }} sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")} titleAccess={t("Administrator Mode")}
/> />
<DnsOutlined <DnsOutlined
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }} sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
titleAccess={t("Service Mode")} titleAccess={t("Service Mode")}
/> />
</> </>
); );
} }
return ( return (
<AdminPanelSettingsOutlined <AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }} sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")} titleAccess={t("Administrator Mode")}
/> />
); );
} else if (isSidecarMode) { } else if (isSidecarMode) {
return ( return (
<ExtensionOutlined <ExtensionOutlined
sx={{ color: "info.main", fontSize: 16 }} sx={{ color: "info.main", fontSize: 16 }}
titleAccess={t("Sidecar Mode")} titleAccess={t("Sidecar Mode")}
/> />
); );
} else { } else {
return ( return (
<DnsOutlined <DnsOutlined
sx={{ color: "success.main", fontSize: 16 }} sx={{ color: "success.main", fontSize: 16 }}
titleAccess={t("Service Mode")} titleAccess={t("Service Mode")}
/> />
); );
@@ -247,13 +256,19 @@ useEffect(() => {
</Typography> </Typography>
</Stack> </Stack>
<Divider /> <Divider />
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("Auto Launch")} {t("Auto Launch")}
</Typography> </Typography>
<Stack direction="row" spacing={1} alignItems="center"> <Stack direction="row" spacing={1} alignItems="center">
{isAdminMode && ( {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 }} /> <WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
</Tooltip> </Tooltip>
)} )}
@@ -268,7 +283,11 @@ useEffect(() => {
</Stack> </Stack>
</Stack> </Stack>
<Divider /> <Divider />
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("Running Mode")} {t("Running Mode")}
</Typography> </Typography>
+24 -21
View File
@@ -87,12 +87,12 @@ export const TestCard = () => {
} }
const newList = testList.map((x) => const newList = testList.map((x) =>
x.uid === uid ? { ...x, ...patch } : x x.uid === uid ? { ...x, ...patch } : x,
); );
mutateVerge({ ...verge, test_list: newList }, false); mutateVerge({ ...verge, test_list: newList }, false);
}, },
[testList, verge, mutateVerge] [testList, verge, mutateVerge],
); );
const onDeleteTestListItem = useCallback( const onDeleteTestListItem = useCallback(
@@ -101,7 +101,7 @@ export const TestCard = () => {
patchVerge({ test_list: newList }); patchVerge({ test_list: newList });
mutateVerge({ ...verge, test_list: newList }, false); mutateVerge({ ...verge, test_list: newList }, false);
}, },
[testList, verge, patchVerge, mutateVerge] [testList, verge, patchVerge, mutateVerge],
); );
const onDragEnd = useCallback( const onDragEnd = useCallback(
@@ -122,7 +122,7 @@ export const TestCard = () => {
const patchFn = () => { const patchFn = () => {
try { try {
patchVerge({ test_list: newList }); patchVerge({ test_list: newList });
} catch { } } catch {}
}; };
if (window.requestIdleCallback) { if (window.requestIdleCallback) {
window.requestIdleCallback(patchFn); window.requestIdleCallback(patchFn);
@@ -131,7 +131,7 @@ export const TestCard = () => {
} }
} }
}, },
[testList, verge, mutateVerge, patchVerge] [testList, verge, mutateVerge, patchVerge],
); );
// 仅在verge首次加载时初始化测试列表 // 仅在verge首次加载时初始化测试列表
@@ -142,22 +142,25 @@ export const TestCard = () => {
}, [verge, patchVerge]); }, [verge, patchVerge]);
// 使用useMemo优化UI内容,减少渲染计算 // 使用useMemo优化UI内容,减少渲染计算
const renderTestItems = useMemo(() => ( const renderTestItems = useMemo(
<Grid container spacing={1} columns={12}> () => (
<SortableContext items={testList.map((x) => x.uid)}> <Grid container spacing={1} columns={12}>
{testList.map((item) => ( <SortableContext items={testList.map((x) => x.uid)}>
<Grid key={item.uid} size={3}> {testList.map((item) => (
<TestItem <Grid key={item.uid} size={3}>
id={item.uid} <TestItem
itemData={item} id={item.uid}
onEdit={() => viewerRef.current?.edit(item)} itemData={item}
onDelete={onDeleteTestListItem} onEdit={() => viewerRef.current?.edit(item)}
/> onDelete={onDeleteTestListItem}
</Grid> />
))} </Grid>
</SortableContext> ))}
</Grid> </SortableContext>
), [testList, onDeleteTestListItem]); </Grid>
),
[testList, onDeleteTestListItem],
);
const handleTestAll = useCallback(() => { const handleTestAll = useCallback(() => {
emit("verge://test-all"); emit("verge://test-all");
+1 -1
View File
@@ -24,7 +24,7 @@ export const UpdateButton = (props: Props) => {
errorRetryCount: 2, errorRetryCount: 2,
revalidateIfStale: false, revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour focusThrottleInterval: 36e5, // 1 hour
} },
); );
if (!updateInfo?.available) return null; if (!updateInfo?.available) return null;
+96 -28
View File
@@ -1,6 +1,9 @@
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material"; 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 { useSetThemeMode, useThemeMode } from "@/services/states";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -49,16 +52,16 @@ export const useCustomTheme = () => {
let isMounted = true; let isMounted = true;
const timerId = setTimeout(() => { appWindow
if (!isMounted) return; .theme()
appWindow.theme().then((systemTheme) => { .then((systemTheme) => {
if (isMounted && systemTheme) { if (isMounted && systemTheme) {
setMode(systemTheme); setMode(systemTheme);
} }
}).catch(err => { })
.catch((err) => {
console.error("Failed to get initial system theme:", err); console.error("Failed to get initial system theme:", err);
}); });
}, 0);
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => { const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
if (isMounted) { if (isMounted) {
@@ -68,14 +71,15 @@ export const useCustomTheme = () => {
return () => { return () => {
isMounted = false; isMounted = false;
clearTimeout(timerId); unlistenPromise
unlistenPromise.then((unlistenFn) => { .then((unlistenFn) => {
if (typeof unlistenFn === 'function') { if (typeof unlistenFn === "function") {
unlistenFn(); unlistenFn();
} }
}).catch(err => { })
console.error("Failed to unlisten from theme changes:", err); .catch((err) => {
}); console.error("Failed to unlisten from theme changes:", err);
});
}; };
}, [theme_mode, appWindow, setMode]); }, [theme_mode, appWindow, setMode]);
@@ -86,7 +90,10 @@ export const useCustomTheme = () => {
if (theme_mode === "system") { if (theme_mode === "system") {
appWindow.setTheme(null).catch((err) => { 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) { } else if (mode) {
appWindow.setTheme(mode as TauriOsTheme).catch((err) => { appWindow.setTheme(mode as TauriOsTheme).catch((err) => {
@@ -120,6 +127,7 @@ export const useCustomTheme = () => {
}, },
background: { background: {
paper: dt.background_color, paper: dt.background_color,
default: dt.background_color,
}, },
}, },
shadows: Array(25).fill("none") as Shadows, shadows: Array(25).fill("none") as Shadows,
@@ -146,6 +154,10 @@ export const useCustomTheme = () => {
warning: { main: dt.warning_color }, warning: { main: dt.warning_color },
success: { main: dt.success_color }, success: { main: dt.success_color },
text: { primary: dt.primary_text, secondary: dt.secondary_text }, text: { primary: dt.primary_text, secondary: dt.secondary_text },
background: {
paper: dt.background_color,
default: dt.background_color,
},
}, },
typography: { fontFamily: dt.font_family }, typography: { fontFamily: dt.font_family },
}); });
@@ -153,31 +165,87 @@ export const useCustomTheme = () => {
const rootEle = document.documentElement; const rootEle = document.documentElement;
if (rootEle) { if (rootEle) {
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d"; const backgroundColor =
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5"; mode === "light" ? "#ECECEC" : dt.background_color;
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee"; const selectColor = mode === "light" ? "#f5f5f5" : "#3E3E3E";
const dividerColor = const scrollColor = mode === "light" ? "#90939980" : "#555555";
const dividerColor =
mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)"; mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";
rootEle.style.setProperty("--divider-color", dividerColor); rootEle.style.setProperty("--divider-color", dividerColor);
rootEle.style.setProperty("--background-color", backgroundColor); rootEle.style.setProperty("--background-color", backgroundColor);
rootEle.style.setProperty("--selection-color", selectColor); rootEle.style.setProperty("--selection-color", selectColor);
rootEle.style.setProperty("--scroller-color", scrollColor); rootEle.style.setProperty("--scroller-color", scrollColor);
rootEle.style.setProperty("--primary-main", muiTheme.palette.primary.main); rootEle.style.setProperty(
rootEle.style.setProperty( "--primary-main",
muiTheme.palette.primary.main,
);
rootEle.style.setProperty(
"--background-color-alpha", "--background-color-alpha",
alpha(muiTheme.palette.primary.main, 0.1), 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"); let styleElement = document.querySelector("style#verge-theme");
if (!styleElement) { if (!styleElement) {
styleElement = document.createElement("style"); styleElement = document.createElement("style");
styleElement.id = "verge-theme"; styleElement.id = "verge-theme";
document.head.appendChild(styleElement!); document.head.appendChild(styleElement!);
} }
if (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; const { palette } = muiTheme;
+3 -3
View File
@@ -127,7 +127,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
currData.current = value; currData.current = value;
onChange?.(prevData.current, currData.current); onChange?.(prevData.current, currData.current);
} catch (err: any) { } 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); !readOnly && onSave?.(prevData.current, currData.current);
onClose(); onClose();
} catch (err: any) { } 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 { try {
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
+2 -2
View File
@@ -70,8 +70,8 @@ export const GroupItem = (props: Props) => {
? alpha(palette.background.paper, 0.3) ? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3) : alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.3) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3), : alpha(palette.success.main, 0.3),
height: "100%", height: "100%",
margin: "8px 0", margin: "8px 0",
borderRadius: "8px", borderRadius: "8px",
+26 -25
View File
@@ -90,27 +90,27 @@ export const GroupsEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo( const filteredPrependSeq = useMemo(
() => prependSeq.filter((group) => match(group.name)), () => prependSeq.filter((group) => match(group.name)),
[prependSeq, match] [prependSeq, match],
); );
const filteredGroupList = useMemo( const filteredGroupList = useMemo(
() => groupList.filter((group) => match(group.name)), () => groupList.filter((group) => match(group.name)),
[groupList, match] [groupList, match],
); );
const filteredAppendSeq = useMemo( const filteredAppendSeq = useMemo(
() => appendSeq.filter((group) => match(group.name)), () => appendSeq.filter((group) => match(group.name)),
[appendSeq, match] [appendSeq, match],
); );
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const reorder = ( const reorder = (
list: IProxyGroupConfig[], list: IProxyGroupConfig[],
startIndex: number, startIndex: number,
endIndex: number endIndex: number,
) => { ) => {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
@@ -188,8 +188,8 @@ export const GroupsEditorViewer = (props: Props) => {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true } { forceQuotes: true },
) ),
); );
} catch (e) { } catch (e) {
// 防止异常导致UI卡死 // 防止异常导致UI卡死
@@ -226,7 +226,7 @@ export const GroupsEditorViewer = (props: Props) => {
return !moreDeleteProxies.includes(proxy); return !moreDeleteProxies.includes(proxy);
} }
}), }),
moreAppendProxies moreAppendProxies,
); );
setProxyPolicyList( setProxyPolicyList(
@@ -236,8 +236,8 @@ export const GroupsEditorViewer = (props: Props) => {
.map((group: IProxyGroupConfig) => group.name) .map((group: IProxyGroupConfig) => group.name)
.filter((name) => !deleteSeq.includes(name)) || [], .filter((name) => !deleteSeq.includes(name)) || [],
appendSeq.map((group: IProxyGroupConfig) => group.name), appendSeq.map((group: IProxyGroupConfig) => group.name),
proxies.map((proxy: any) => proxy.name) proxies.map((proxy: any) => proxy.name),
) ),
); );
}; };
const fetchProfile = async () => { const fetchProfile = async () => {
@@ -266,7 +266,7 @@ export const GroupsEditorViewer = (props: Props) => {
{}, {},
originProvider, originProvider,
moreProvider, moreProvider,
globalProvider globalProvider,
); );
setProxyProviderList(Object.keys(provider)); setProxyProviderList(Object.keys(provider));
@@ -297,11 +297,11 @@ export const GroupsEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
await saveProfileFile(property, currData); await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully")); showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData); onSave?.(prevData, currData);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -502,7 +502,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("seconds")} {t("seconds")}
</InputAdornment> </InputAdornment>
), ),
} },
}} }}
/> />
</Item> </Item>
@@ -530,7 +530,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("millis")} {t("millis")}
</InputAdornment> </InputAdornment>
), ),
} },
}} }}
/> />
</Item> </Item>
@@ -742,7 +742,7 @@ export const GroupsEditorViewer = (props: Props) => {
} }
setPrependSeq([formIns.getValues(), ...prependSeq]); setPrependSeq([formIns.getValues(), ...prependSeq]);
} catch (err: any) { } 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()]); setAppendSeq([...appendSeq, formIns.getValues()]);
} catch (err: any) { } 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={() => { onDelete={() => {
setPrependSeq( setPrependSeq(
prependSeq.filter( prependSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -838,8 +838,8 @@ export const GroupsEditorViewer = (props: Props) => {
) { ) {
setDeleteSeq( setDeleteSeq(
deleteSeq.filter( deleteSeq.filter(
(v) => v !== filteredGroupList[newIndex].name (v) => v !== filteredGroupList[newIndex].name,
) ),
); );
} else { } else {
setDeleteSeq((prev) => [ setDeleteSeq((prev) => [
@@ -871,8 +871,8 @@ export const GroupsEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setAppendSeq( setAppendSeq(
appendSeq.filter( appendSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -906,8 +906,9 @@ export const GroupsEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets 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, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}
+51 -50
View File
@@ -1,57 +1,58 @@
import { alpha, Box, styled } from "@mui/material"; import { alpha, Box, styled } from "@mui/material";
export const ProfileBox = styled(Box)( export const ProfileBox = styled(Box)(({
({ theme, "aria-selected": selected }) => { theme,
const { mode, primary, text } = theme.palette; "aria-selected": selected,
const key = `${mode}-${!!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 = { const color = {
"light-true": text.secondary, "light-true": text.secondary,
"light-false": text.secondary, "light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.65), "dark-true": alpha(text.secondary, 0.65),
"dark-false": alpha(text.secondary, 0.65), "dark-false": alpha(text.secondary, 0.65),
}[key]!; }[key]!;
const h2color = { const h2color = {
"light-true": primary.main, "light-true": primary.main,
"light-false": text.primary, "light-false": text.primary,
"dark-true": primary.main, "dark-true": primary.main,
"dark-false": text.primary, "dark-false": text.primary,
}[key]!; }[key]!;
const borderSelect = { const borderSelect = {
"light-true": { "light-true": {
borderLeft: `3px solid ${primary.main}`, borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`, width: `calc(100% + 3px)`,
marginLeft: `-3px`, marginLeft: `-3px`,
}, },
"light-false": { "light-false": {
width: "100%", width: "100%",
}, },
"dark-true": { "dark-true": {
borderLeft: `3px solid ${primary.main}`, borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`, width: `calc(100% + 3px)`,
marginLeft: `-3px`, marginLeft: `-3px`,
}, },
"dark-false": { "dark-false": {
width: "100%", width: "100%",
}, },
}[key]; }[key];
return { return {
position: "relative", position: "relative",
display: "block", display: "block",
cursor: "pointer", cursor: "pointer",
textAlign: "left", textAlign: "left",
padding: "8px 16px", padding: "8px 16px",
boxSizing: "border-box", boxSizing: "border-box",
backgroundColor, backgroundColor,
...borderSelect, ...borderSelect,
borderRadius: "8px", borderRadius: "8px",
color, color,
"& h2": { color: h2color }, "& h2": { color: h2color },
}; };
} });
);
+47 -14
View File
@@ -75,7 +75,10 @@ export const ProfileItem = (props: Props) => {
// 获取下次更新时间的函数 // 获取下次更新时间的函数
const fetchNextUpdateTime = useLockFn(async (forceRefresh = false) => { 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 { try {
console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`); console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`);
@@ -97,7 +100,7 @@ export const ProfileItem = (props: Props) => {
setNextUpdateTime(t("Last Update failed")); setNextUpdateTime(t("Last Update failed"));
} else { } else {
// 否则显示剩余时间 // 否则显示剩余时间
const diffMinutes = nextUpdateDate.diff(now, 'minute'); const diffMinutes = nextUpdateDate.diff(now, "minute");
if (diffMinutes < 60) { if (diffMinutes < 60) {
if (diffMinutes <= 0) { 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 () => { return () => {
// 清理事件监听 // 清理事件监听
window.removeEventListener('verge://timer-updated', handleTimerUpdate as EventListener); window.removeEventListener(
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
}; };
}, [showNextUpdate, itemData.uid]); }, [showNextUpdate, itemData.uid]);
@@ -271,7 +280,7 @@ export const ProfileItem = (props: Props) => {
try { try {
await viewProfile(itemData.uid); await viewProfile(itemData.uid);
} catch (err: any) { } 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); await updateProfile(itemData.uid, option);
// 更新成功,刷新列表 // 更新成功,刷新列表
showNotice('success', t("Update subscription successfully")); showNotice("success", t("Update subscription successfully"));
mutate("getProfiles"); mutate("getProfiles");
} catch (err: any) { } catch (err: any) {
// 更新完全失败(包括后端的回退尝试) // 更新完全失败(包括后端的回退尝试)
@@ -421,13 +430,25 @@ export const ProfileItem = (props: Props) => {
}; };
// 注册事件监听 // 注册事件监听
window.addEventListener('profile-update-started', handleUpdateStarted as EventListener); window.addEventListener(
window.addEventListener('profile-update-completed', handleUpdateCompleted as EventListener); "profile-update-started",
handleUpdateStarted as EventListener,
);
window.addEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
return () => { return () => {
// 清理事件监听 // 清理事件监听
window.removeEventListener('profile-update-started', handleUpdateStarted as EventListener); window.removeEventListener(
window.removeEventListener('profile-update-completed', handleUpdateCompleted as EventListener); "profile-update-started",
handleUpdateStarted as EventListener,
);
window.removeEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
}; };
}, [itemData.uid, showNextUpdate]); }, [itemData.uid, showNextUpdate]);
@@ -541,13 +562,23 @@ export const ProfileItem = (props: Props) => {
) )
)} )}
{hasUrl && ( {hasUrl && (
<Box sx={{ display: "flex", justifyContent: "flex-end", ml: "auto" }}> <Box
sx={{
display: "flex",
justifyContent: "flex-end",
ml: "auto",
}}
>
<Typography <Typography
noWrap noWrap
component="span" component="span"
fontSize={14} fontSize={14}
textAlign="right" 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={{ sx={{
cursor: "pointer", cursor: "pointer",
display: "inline-block", display: "inline-block",
@@ -556,13 +587,15 @@ export const ProfileItem = (props: Props) => {
"&:hover": { "&:hover": {
borderBottomColor: "primary.main", borderBottomColor: "primary.main",
color: "primary.main", color: "primary.main",
} },
}} }}
onClick={toggleUpdateTimeDisplay} onClick={toggleUpdateTimeDisplay}
> >
{showNextUpdate {showNextUpdate
? nextUpdateTime ? nextUpdateTime
: (updated > 0 ? dayjs(updated * 1000).fromNow() : "")} : updated > 0
? dayjs(updated * 1000).fromNow()
: ""}
</Typography> </Typography>
</Box> </Box>
)} )}
+1 -1
View File
@@ -43,7 +43,7 @@ export const ProfileMore = (props: Props) => {
try { try {
await viewProfile(id); await viewProfile(id);
} catch (err: any) { } catch (err: any) {
showNotice('error', err?.message || err.toString()); showNotice("error", err?.message || err.toString());
} }
}); });
+1 -1
View File
@@ -201,7 +201,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
setOpen(false); setOpen(false);
fileDataRef.current = null; fileDataRef.current = null;
setTimeout(() => formIns.reset(), 500); setTimeout(() => formIns.reset(), 500);
} catch { } } catch {}
}; };
const text = { const text = {
@@ -66,27 +66,27 @@ export const ProxiesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo( const filteredPrependSeq = useMemo(
() => prependSeq.filter((proxy) => match(proxy.name)), () => prependSeq.filter((proxy) => match(proxy.name)),
[prependSeq, match] [prependSeq, match],
); );
const filteredProxyList = useMemo( const filteredProxyList = useMemo(
() => proxyList.filter((proxy) => match(proxy.name)), () => proxyList.filter((proxy) => match(proxy.name)),
[proxyList, match] [proxyList, match],
); );
const filteredAppendSeq = useMemo( const filteredAppendSeq = useMemo(
() => appendSeq.filter((proxy) => match(proxy.name)), () => appendSeq.filter((proxy) => match(proxy.name)),
[appendSeq, match] [appendSeq, match],
); );
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const reorder = ( const reorder = (
list: IProxyConfig[], list: IProxyConfig[],
startIndex: number, startIndex: number,
endIndex: number endIndex: number,
) => { ) => {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
@@ -208,8 +208,8 @@ export const ProxiesEditorViewer = (props: Props) => {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true } { forceQuotes: true },
) ),
); );
} catch (e) { } catch (e) {
// 防止异常导致UI卡死 // 防止异常导致UI卡死
@@ -232,11 +232,11 @@ export const ProxiesEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
await saveProfileFile(property, currData); await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully")); showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData); onSave?.(prevData, currData);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -358,8 +358,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setPrependSeq( setPrependSeq(
prependSeq.filter( prependSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -385,8 +385,8 @@ export const ProxiesEditorViewer = (props: Props) => {
) { ) {
setDeleteSeq( setDeleteSeq(
deleteSeq.filter( deleteSeq.filter(
(v) => v !== filteredProxyList[newIndex].name (v) => v !== filteredProxyList[newIndex].name,
) ),
); );
} else { } else {
setDeleteSeq((prev) => [ setDeleteSeq((prev) => [
@@ -418,8 +418,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setAppendSeq( setAppendSeq(
appendSeq.filter( appendSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -453,8 +453,9 @@ export const ProxiesEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets 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, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}
+2 -2
View File
@@ -49,8 +49,8 @@ export const ProxyItem = (props: Props) => {
? alpha(palette.background.paper, 0.3) ? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3) : alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.3) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3), : alpha(palette.success.main, 0.3),
height: "100%", height: "100%",
margin: "8px 0", margin: "8px 0",
borderRadius: "8px", borderRadius: "8px",
+2 -2
View File
@@ -52,8 +52,8 @@ export const RuleItem = (props: Props) => {
? alpha(palette.background.paper, 0.3) ? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3) : alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.3) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3), : alpha(palette.success.main, 0.3),
height: "100%", height: "100%",
margin: "8px 0", margin: "8px 0",
borderRadius: "8px", borderRadius: "8px",
+182 -180
View File
@@ -55,17 +55,17 @@ interface Props {
const portValidator = (value: string): boolean => { const portValidator = (value: string): boolean => {
return new RegExp( 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); ).test(value);
}; };
const ipv4CIDRValidator = (value: string): boolean => { const ipv4CIDRValidator = (value: string): boolean => {
return new RegExp( 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); ).test(value);
}; };
const ipv6CIDRValidator = (value: string): boolean => { const ipv6CIDRValidator = (value: string): boolean => {
return new RegExp( 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); ).test(value);
}; };
@@ -76,161 +76,161 @@ const rules: {
noResolve?: boolean; noResolve?: boolean;
validator?: (value: string) => boolean; validator?: (value: string) => boolean;
}[] = [ }[] = [
{ {
name: "DOMAIN", name: "DOMAIN",
example: "example.com", example: "example.com",
}, },
{ {
name: "DOMAIN-SUFFIX", name: "DOMAIN-SUFFIX",
example: "example.com", example: "example.com",
}, },
{ {
name: "DOMAIN-KEYWORD", name: "DOMAIN-KEYWORD",
example: "example", example: "example",
}, },
{ {
name: "DOMAIN-REGEX", name: "DOMAIN-REGEX",
example: "example.*", example: "example.*",
}, },
{ {
name: "GEOSITE", name: "GEOSITE",
example: "youtube", example: "youtube",
}, },
{ {
name: "GEOIP", name: "GEOIP",
example: "CN", example: "CN",
noResolve: true, noResolve: true,
}, },
{ {
name: "SRC-GEOIP", name: "SRC-GEOIP",
example: "CN", example: "CN",
}, },
{ {
name: "IP-ASN", name: "IP-ASN",
example: "13335", example: "13335",
noResolve: true, noResolve: true,
validator: (value) => (+value ? true : false), validator: (value) => (+value ? true : false),
}, },
{ {
name: "SRC-IP-ASN", name: "SRC-IP-ASN",
example: "9808", example: "9808",
validator: (value) => (+value ? true : false), validator: (value) => (+value ? true : false),
}, },
{ {
name: "IP-CIDR", name: "IP-CIDR",
example: "127.0.0.0/8", example: "127.0.0.0/8",
noResolve: true, noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "IP-CIDR6", name: "IP-CIDR6",
example: "2620:0:2d0:200::7/32", example: "2620:0:2d0:200::7/32",
noResolve: true, noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "SRC-IP-CIDR", name: "SRC-IP-CIDR",
example: "192.168.1.201/32", example: "192.168.1.201/32",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "IP-SUFFIX", name: "IP-SUFFIX",
example: "8.8.8.8/24", example: "8.8.8.8/24",
noResolve: true, noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "SRC-IP-SUFFIX", name: "SRC-IP-SUFFIX",
example: "192.168.1.201/8", example: "192.168.1.201/8",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "SRC-PORT", name: "SRC-PORT",
example: "7777", example: "7777",
validator: (value) => portValidator(value), validator: (value) => portValidator(value),
}, },
{ {
name: "DST-PORT", name: "DST-PORT",
example: "80", example: "80",
validator: (value) => portValidator(value), validator: (value) => portValidator(value),
}, },
{ {
name: "IN-PORT", name: "IN-PORT",
example: "7890", example: "7890",
validator: (value) => portValidator(value), validator: (value) => portValidator(value),
}, },
{ {
name: "DSCP", name: "DSCP",
example: "4", example: "4",
}, },
{ {
name: "PROCESS-NAME", name: "PROCESS-NAME",
example: getSystem() === "windows" ? "chrome.exe" : "curl", example: getSystem() === "windows" ? "chrome.exe" : "curl",
}, },
{ {
name: "PROCESS-PATH", name: "PROCESS-PATH",
example: example:
getSystem() === "windows" getSystem() === "windows"
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" ? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
: "/usr/bin/wget", : "/usr/bin/wget",
}, },
{ {
name: "PROCESS-NAME-REGEX", name: "PROCESS-NAME-REGEX",
example: ".*telegram.*", example: ".*telegram.*",
}, },
{ {
name: "PROCESS-PATH-REGEX", name: "PROCESS-PATH-REGEX",
example: example:
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget", getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
}, },
{ {
name: "NETWORK", name: "NETWORK",
example: "udp", example: "udp",
validator: (value) => ["tcp", "udp"].includes(value), validator: (value) => ["tcp", "udp"].includes(value),
}, },
{ {
name: "UID", name: "UID",
example: "1001", example: "1001",
validator: (value) => (+value ? true : false), validator: (value) => (+value ? true : false),
}, },
{ {
name: "IN-TYPE", name: "IN-TYPE",
example: "SOCKS/HTTP", example: "SOCKS/HTTP",
}, },
{ {
name: "IN-USER", name: "IN-USER",
example: "mihomo", example: "mihomo",
}, },
{ {
name: "IN-NAME", name: "IN-NAME",
example: "ss", example: "ss",
}, },
{ {
name: "SUB-RULE", name: "SUB-RULE",
example: "(NETWORK,tcp)", example: "(NETWORK,tcp)",
}, },
{ {
name: "RULE-SET", name: "RULE-SET",
example: "providername", example: "providername",
noResolve: true, noResolve: true,
}, },
{ {
name: "AND", name: "AND",
example: "((DOMAIN,baidu.com),(NETWORK,UDP))", example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
}, },
{ {
name: "OR", name: "OR",
example: "((NETWORK,UDP),(DOMAIN,baidu.com))", example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
}, },
{ {
name: "NOT", name: "NOT",
example: "((DOMAIN,baidu.com))", example: "((DOMAIN,baidu.com))",
}, },
{ {
name: "MATCH", name: "MATCH",
required: false, required: false,
}, },
]; ];
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
@@ -260,22 +260,22 @@ export const RulesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo( const filteredPrependSeq = useMemo(
() => prependSeq.filter((rule) => match(rule)), () => prependSeq.filter((rule) => match(rule)),
[prependSeq, match] [prependSeq, match],
); );
const filteredRuleList = useMemo( const filteredRuleList = useMemo(
() => ruleList.filter((rule) => match(rule)), () => ruleList.filter((rule) => match(rule)),
[ruleList, match] [ruleList, match],
); );
const filteredAppendSeq = useMemo( const filteredAppendSeq = useMemo(
() => appendSeq.filter((rule) => match(rule)), () => appendSeq.filter((rule) => match(rule)),
[appendSeq, match] [appendSeq, match],
); );
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const reorder = (list: string[], startIndex: number, endIndex: number) => { const reorder = (list: string[], startIndex: number, endIndex: number) => {
const result = Array.from(list); const result = Array.from(list);
@@ -333,11 +333,11 @@ export const RulesEditorViewer = (props: Props) => {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true } { forceQuotes: true },
) ),
); );
} catch (e: any) { } catch (e: any) {
showNotice('error', e?.message || e?.toString() || 'YAML dump error'); showNotice("error", e?.message || e?.toString() || "YAML dump error");
} }
}; };
if (window.requestIdleCallback) { if (window.requestIdleCallback) {
@@ -371,7 +371,7 @@ export const RulesEditorViewer = (props: Props) => {
return !moreDeleteGroups.includes(group); return !moreDeleteGroups.includes(group);
} }
}), }),
moreAppendGroups moreAppendGroups,
); );
let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null; let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null;
@@ -396,7 +396,7 @@ export const RulesEditorViewer = (props: Props) => {
let globalSubRule = globalSubRuleObj?.["sub-rules"] || {}; let globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule); let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule);
setProxyPolicyList( setProxyPolicyList(
builtinProxyPolicies.concat(groups.map((group: any) => group.name)) builtinProxyPolicies.concat(groups.map((group: any) => group.name)),
); );
setRuleSetList(Object.keys(ruleSet)); setRuleSetList(Object.keys(ruleSet));
setSubRuleList(Object.keys(subRule)); setSubRuleList(Object.keys(subRule));
@@ -417,19 +417,20 @@ export const RulesEditorViewer = (props: Props) => {
throw new Error(t("Invalid Rule")); throw new Error(t("Invalid Rule"));
} }
const condition = ruleType.required ?? true ? ruleContent : ""; const condition = (ruleType.required ?? true) ? ruleContent : "";
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : "" return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
}`; ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`;
}; };
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
await saveProfileFile(property, currData); await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully")); showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData); onSave?.(prevData, currData);
onClose(); onClose();
} catch (err: any) { } 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; if (prependSeq.includes(raw)) return;
setPrependSeq([raw, ...prependSeq]); setPrependSeq([raw, ...prependSeq]);
} catch (err: any) { } 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; if (appendSeq.includes(raw)) return;
setAppendSeq([...appendSeq, raw]); setAppendSeq([...appendSeq, raw]);
} catch (err: any) { } 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} ruleRaw={item}
onDelete={() => { onDelete={() => {
setPrependSeq( 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])) { if (deleteSeq.includes(filteredRuleList[newIndex])) {
setDeleteSeq( setDeleteSeq(
deleteSeq.filter( deleteSeq.filter(
(v) => v !== filteredRuleList[newIndex] (v) => v !== filteredRuleList[newIndex],
) ),
); );
} else { } else {
setDeleteSeq((prev) => [ setDeleteSeq((prev) => [
@@ -677,7 +678,7 @@ export const RulesEditorViewer = (props: Props) => {
ruleRaw={item} ruleRaw={item}
onDelete={() => { onDelete={() => {
setAppendSeq( setAppendSeq(
appendSeq.filter((v) => v !== item) appendSeq.filter((v) => v !== item),
); );
}} }}
/> />
@@ -711,8 +712,9 @@ export const RulesEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets 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, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}
+102 -78
View File
@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Button, Button,
Box, Box,
Dialog, Dialog,
@@ -15,7 +15,7 @@ import {
LinearProgress, LinearProgress,
alpha, alpha,
styled, styled,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
@@ -65,77 +65,83 @@ export const ProviderButton = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData(); const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
const [updating, setUpdating] = useState<Record<string, boolean>>({}); const [updating, setUpdating] = useState<Record<string, boolean>>({});
// 检查是否有提供者 // 检查是否有提供者
const hasProviders = Object.keys(proxyProviders || {}).length > 0; const hasProviders = Object.keys(proxyProviders || {}).length > 0;
// 更新单个代理提供者 // 更新单个代理提供者
const updateProvider = useLockFn(async (name: string) => { const updateProvider = useLockFn(async (name: string) => {
try { try {
// 设置更新状态 // 设置更新状态
setUpdating(prev => ({ ...prev, [name]: true })); setUpdating((prev) => ({ ...prev, [name]: true }));
await proxyProviderUpdate(name); await proxyProviderUpdate(name);
// 刷新数据 // 刷新数据
await refreshProxy(); await refreshProxy();
await refreshProxyProviders(); await refreshProxyProviders();
showNotice('success', `${name} 更新成功`); showNotice("success", `${name} 更新成功`);
} catch (err: any) { } catch (err: any) {
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`); showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
} finally { } finally {
// 清除更新状态 // 清除更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} }
}); });
// 更新所有代理提供者 // 更新所有代理提供者
const updateAllProviders = useLockFn(async () => { const updateAllProviders = useLockFn(async () => {
try { try {
// 获取所有provider的名称 // 获取所有provider的名称
const allProviders = Object.keys(proxyProviders || {}); const allProviders = Object.keys(proxyProviders || {});
if (allProviders.length === 0) { if (allProviders.length === 0) {
showNotice('info', "没有可更新的代理提供者"); showNotice("info", "没有可更新的代理提供者");
return; return;
} }
// 设置所有provider为更新中状态 // 设置所有provider为更新中状态
const newUpdating = allProviders.reduce((acc, key) => { const newUpdating = allProviders.reduce(
acc[key] = true; (acc, key) => {
return acc; acc[key] = true;
}, {} as Record<string, boolean>); return acc;
},
{} as Record<string, boolean>,
);
setUpdating(newUpdating); setUpdating(newUpdating);
// 改为串行逐个更新所有provider // 改为串行逐个更新所有provider
for (const name of allProviders) { for (const name of allProviders) {
try { try {
await proxyProviderUpdate(name); await proxyProviderUpdate(name);
// 每个更新完成后更新状态 // 每个更新完成后更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) { } catch (err) {
console.error(`更新 ${name} 失败`, err); console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程 // 继续执行下一个,不中断整体流程
} }
} }
// 刷新数据 // 刷新数据
await refreshProxy(); await refreshProxy();
await refreshProxyProviders(); await refreshProxyProviders();
showNotice('success', "全部代理提供者更新成功"); showNotice("success", "全部代理提供者更新成功");
} catch (err: any) { } catch (err: any) {
showNotice('error', `更新失败: ${err?.message || err.toString()}`); showNotice("error", `更新失败: ${err?.message || err.toString()}`);
} finally { } finally {
// 清除所有更新状态 // 清除所有更新状态
setUpdating({}); setUpdating({});
} }
}); });
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
}; };
if (!hasProviders) return null; if (!hasProviders) return null;
return ( return (
@@ -149,15 +155,14 @@ export const ProviderButton = () => {
> >
{t("Proxy Provider")} {t("Proxy Provider")}
</Button> </Button>
<Dialog <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<DialogTitle> <DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Proxy Provider")}</Typography> <Typography variant="h6">{t("Proxy Provider")}</Typography>
<Box> <Box>
<Button <Button
@@ -170,14 +175,14 @@ export const ProviderButton = () => {
</Box> </Box>
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<List sx={{ py: 0, minHeight: 250 }}> <List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(proxyProviders || {}).map(([key, item]) => { {Object.entries(proxyProviders || {}).map(([key, item]) => {
const provider = item as ProxyProviderItem; const provider = item as ProxyProviderItem;
const time = dayjs(provider.updatedAt); const time = dayjs(provider.updatedAt);
const isUpdating = updating[key]; const isUpdating = updating[key];
// 订阅信息 // 订阅信息
const sub = provider.subscriptionInfo; const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub; const hasSubInfo = !!sub;
@@ -185,46 +190,53 @@ export const ProviderButton = () => {
const download = sub?.Download || 0; const download = sub?.Download || 0;
const total = sub?.Total || 0; const total = sub?.Total || 0;
const expire = sub?.Expire || 0; const expire = sub?.Expire || 0;
// 流量使用进度 // 流量使用进度
const progress = total > 0 const progress =
? Math.min(Math.round(((download + upload) * 100) / total) + 1, 100) total > 0
: 0; ? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
return ( return (
<ListItem <ListItem
key={key} key={key}
sx={[ sx={[
{ {
p: 0, p: 0,
mb: "8px", mb: "8px",
borderRadius: 2, borderRadius: 2,
overflow: "hidden", overflow: "hidden",
transition: "all 0.2s" transition: "all 0.2s",
}, },
({ palette: { mode, primary } }) => { ({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor = mode === "light" const hoverColor =
? alpha(primary.main, 0.1) mode === "light"
: alpha(primary.main, 0.2); ? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return { return {
backgroundColor: bgcolor, backgroundColor: bgcolor,
"&:hover": { "&:hover": {
backgroundColor: hoverColor, backgroundColor: hoverColor,
} },
}; };
} },
]} ]}
> >
<ListItemText <ListItemText
sx={{ px: 2, py: 1 }} sx={{ px: 2, py: 1 }}
primary={ primary={
<Box sx={{ <Box
display: "flex", sx={{
justifyContent: "space-between", display: "flex",
alignItems: "center", justifyContent: "space-between",
}}> alignItems: "center",
}}
>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
component="div" component="div"
@@ -232,7 +244,7 @@ export const ProviderButton = () => {
title={key} title={key}
sx={{ display: "flex", alignItems: "center" }} sx={{ display: "flex", alignItems: "center" }}
> >
<span style={{ marginRight: "8px" }}>{key}</span> <span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span"> <TypeBox component="span">
{provider.proxies.length} {provider.proxies.length}
</TypeBox> </TypeBox>
@@ -240,9 +252,14 @@ export const ProviderButton = () => {
{provider.vehicleType} {provider.vehicleType}
</TypeBox> </TypeBox>
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" noWrap> <Typography
<small>{t("Update At")}: </small>{time.fromNow()} variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography> </Typography>
</Box> </Box>
} }
@@ -251,26 +268,29 @@ export const ProviderButton = () => {
{/* 订阅信息 */} {/* 订阅信息 */}
{hasSubInfo && ( {hasSubInfo && (
<> <>
<Box sx={{ <Box
mb: 1, sx={{
display: "flex", mb: 1,
alignItems: "center", display: "flex",
justifyContent: "space-between", alignItems: "center",
}}> justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}> <span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} / {parseTraffic(total)} {parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span> </span>
<span title={t("Expire Time") as string}> <span title={t("Expire Time") as string}>
{parseExpire(expire)} {parseExpire(expire)}
</span> </span>
</Box> </Box>
{/* 进度条 */} {/* 进度条 */}
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={progress} value={progress}
sx={{ sx={{
height: 6, height: 6,
borderRadius: 3, borderRadius: 3,
opacity: total > 0 ? 1 : 0, opacity: total > 0 ? 1 : 0,
}} }}
@@ -281,12 +301,14 @@ export const ProviderButton = () => {
} }
/> />
<Divider orientation="vertical" flexItem /> <Divider orientation="vertical" flexItem />
<Box sx={{ <Box
width: 40, sx={{
display: "flex", width: 40,
justifyContent: "center", display: "flex",
alignItems: "center" justifyContent: "center",
}}> alignItems: "center",
}}
>
<IconButton <IconButton
size="small" size="small"
color="primary" color="primary"
@@ -295,11 +317,13 @@ export const ProviderButton = () => {
}} }}
disabled={isUpdating} disabled={isUpdating}
sx={{ sx={{
animation: isUpdating ? "spin 1s linear infinite" : "none", animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": { "@keyframes spin": {
"0%": { transform: "rotate(0deg)" }, "0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" } "100%": { transform: "rotate(360deg)" },
} },
}} }}
title={t("Update Provider") as string} title={t("Update Provider") as string}
> >
@@ -311,7 +335,7 @@ export const ProviderButton = () => {
})} })}
</List> </List>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose} variant="outlined"> <Button onClick={handleClose} variant="outlined">
{t("Close")} {t("Close")}
+14 -5
View File
@@ -106,10 +106,12 @@ const LetterItem = memo(
name, name,
onClick, onClick,
getFirstChar, getFirstChar,
enableAutoScroll = true,
}: { }: {
name: string; name: string;
onClick: (name: string) => void; onClick: (name: string) => void;
getFirstChar: (str: string) => string; getFirstChar: (str: string) => string;
enableAutoScroll?: boolean;
}) => { }) => {
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const letterRef = useRef<HTMLDivElement>(null); const letterRef = useRef<HTMLDivElement>(null);
@@ -136,11 +138,14 @@ const LetterItem = memo(
const handleMouseEnter = useCallback(() => { const handleMouseEnter = useCallback(() => {
setShowTooltip(true); setShowTooltip(true);
// 添加 200ms 的延迟,避免鼠标快速划过时触发滚动 // 只有在启用自动滚动时才触发滚动
hoverTimeoutRef.current = setTimeout(() => { if (enableAutoScroll) {
onClick(name); // 添加 100ms 的延迟,避免鼠标快速划过时触发滚动
}, 100); hoverTimeoutRef.current = setTimeout(() => {
}, [name, onClick]); onClick(name);
}, 100);
}
}, [name, onClick, enableAutoScroll]);
const handleMouseLeave = useCallback(() => { const handleMouseLeave = useCallback(() => {
setShowTooltip(false); setShowTooltip(false);
@@ -198,6 +203,9 @@ export const ProxyGroups = (props: Props) => {
const { verge } = useVerge(); const { verge } = useVerge();
const { current, patchCurrent } = useProfiles(); const { current, patchCurrent } = useProfiles();
// 获取自动滚动开关状态,默认为 true
const enableAutoScroll = verge?.enable_hover_jump_navigator ?? true;
const timeout = verge?.default_latency_timeout || 10000; const timeout = verge?.default_latency_timeout || 10000;
const virtuosoRef = useRef<VirtuosoHandle>(null); const virtuosoRef = useRef<VirtuosoHandle>(null);
@@ -541,6 +549,7 @@ export const ProxyGroups = (props: Props) => {
name={name} name={name}
onClick={handleLetterClick} onClick={handleLetterClick}
getFirstChar={getFirstChar} getFirstChar={getFirstChar}
enableAutoScroll={enableAutoScroll}
/> />
))} ))}
</div> </div>
+12 -8
View File
@@ -31,7 +31,10 @@ interface RenderProps {
onLocation: (group: IRenderItem["group"]) => void; onLocation: (group: IRenderItem["group"]) => void;
onCheckAll: (groupName: string) => void; onCheckAll: (groupName: string) => void;
onHeadState: (groupName: string, patch: Partial<HeadState>) => 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) => { export const ProxyRender = (props: RenderProps) => {
@@ -129,14 +132,15 @@ export const ProxyRender = (props: RenderProps) => {
/> />
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: "flex", alignItems: "center" }}>
<Tooltip title={t("Proxy Count")} arrow> <Tooltip title={t("Proxy Count")} arrow>
<Chip <Chip
size="small" size="small"
label={`${group.all.length}`} label={`${group.all.length}`}
sx={{ sx={{
mr: 1, mr: 1,
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.1), backgroundColor: (theme) =>
alpha(theme.palette.primary.main, 0.1),
color: (theme) => theme.palette.primary.main, color: (theme) => theme.palette.primary.main,
}} }}
/> />
</Tooltip> </Tooltip>
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />} {headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
+4 -4
View File
@@ -8,7 +8,7 @@ export default function useFilterSort(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
filterText: string, filterText: string,
sortType: ProxySortType sortType: ProxySortType,
) { ) {
const [refresh, setRefresh] = useState({}); const [refresh, setRefresh] = useState({});
@@ -40,7 +40,7 @@ export function filterSort(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
filterText: string, filterText: string,
sortType: ProxySortType sortType: ProxySortType,
) { ) {
const fp = filterProxies(proxies, groupName, filterText); const fp = filterProxies(proxies, groupName, filterText);
const sp = sortProxies(fp, groupName, sortType); const sp = sortProxies(fp, groupName, sortType);
@@ -60,7 +60,7 @@ const regex2 = /type=(.*)/i;
function filterProxies( function filterProxies(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
filterText: string filterText: string,
) { ) {
if (!filterText) return proxies; if (!filterText) return proxies;
@@ -100,7 +100,7 @@ function filterProxies(
function sortProxies( function sortProxies(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
sortType: ProxySortType sortType: ProxySortType,
) { ) {
if (!proxies) return []; if (!proxies) return [];
if (sortType === 0) return proxies; if (sortType === 0) return proxies;

Some files were not shown because too many files have changed in this diff Show More