Compare commits

...
64 Commits
Author SHA1 Message Date
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
146 changed files with 8410 additions and 6980 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
+175 -136
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
@@ -262,13 +308,10 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ 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 +327,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
@@ -334,16 +378,13 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ 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 +418,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 +458,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
@@ -465,13 +507,10 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ 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 +554,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 }}
+15 -14
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 }}
@@ -203,10 +204,10 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ 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 +224,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
@@ -273,10 +274,10 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ 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 +355,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: |
@@ -402,10 +403,10 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ 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 +451,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 }}
+64
View File
@@ -0,0 +1,64 @@
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
# cache-all-crates: true
# cache-on-failure: true
- 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 prepare ${{ 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
+65
View File
@@ -0,0 +1,65 @@
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 prepare ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
- name: Cargo Check (deny warnings)
working-directory: src-tauri
run: |
cargo check --target ${{ matrix.target }} --workspace --all-features
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ matrix.target }}
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
+51
View File
@@ -0,0 +1,51 @@
# 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/*"
cache: "pnpm"
- 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
+38 -4
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:
@@ -65,7 +95,7 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ matrix.target }}
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@@ -88,6 +118,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:
@@ -128,7 +159,7 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ matrix.target }}
- name: "Setup for linux" - name: "Setup for linux"
run: |- run: |-
@@ -209,6 +240,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:
@@ -247,7 +279,7 @@ jobs:
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm prepare ${{ matrix.target }}
- name: Download WebView2 Runtime - name: Download WebView2 Runtime
run: | run: |
@@ -303,6 +335,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 +386,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
+7 -9
View File
@@ -1,13 +1,11 @@
#!/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
# if [ $? -ne 0 ]; then
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
# exit 1
# 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 prepare
# Use '--force' to force update to the latest version # Use '--force' to force update to the latest version
# pnpm run check --force # pnpm run prepare --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 或任何高负载应用
+185 -129
View File
@@ -1,143 +1,181 @@
## 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 版本与设置页面不同步
#### ✨ 新增功能 ### ⚠️ 已知问题
- Mihomo(Meta)内核升级至 1.19.10
- 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好
- 关闭系统代理时关闭已建立的网络连接
- 托盘显示当前轻量模式状态
- Webdav 请求加入 UA
- Webdav 支持目录重定向
- Webdav 备份目录检查和文件上传重试机制
- 系统代理守卫可检查意外设置变更并恢复
- 定时自动订阅更新也能自动回退使用代理
- 订阅请求超时机制,防止订阅更新的时候卡死
- 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示
- 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死
- 更新依赖,替换弃用元素
- 首页当前节点增加排序功能
- DNS 覆写下增加 Hosts 设置功能
- 修复服务模式安装后无法立即开启 TUN 模式的问题
- 支持手动卸载服务模式,回退到 Sidecar 模式
- 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持
- 卸载服务的按钮
- 添加了Zashboard的一键跳转URL
- 使用操作系统默认的窗口管理器
- 切换、升级、重启内核的状态管理
- 更精细化控制自动日志清理,新增1天选项
- Winodws 快捷键名称改为 `Clash Verge`
- 配置加载阶段自动补全 external-controller secret 字段。
- 添加 TCP Concurrency (TCP并发)
#### 🚀 优化改进 - 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
- 系统代理 Bypass 设置 - macOS:
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题 - MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
- 切换到规则页面时自动刷新规则数据 - 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新 - 彩色 Tray 图标颜色偏淡;
- 编辑非激活订阅的时候不在触发当前订阅配置重载
- 改进核心功能防止主进程阻塞、改进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 +186,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 +225,7 @@
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。 - 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
#### 新增了: #### 新增了:
1. **首页** 1. **首页**
- 首页文本过长自动截断 - 首页文本过长自动截断
2. **轻量模式** 2. **轻量模式**
@@ -197,7 +242,9 @@
## 2.2.0(已下架不再提供) ## 2.2.0(已下架不再提供)
#### 新增功能 #### 新增功能
1. **首页** 1. **首页**
- 新增首页功能,默认启动页面改为首页。 - 新增首页功能,默认启动页面改为首页。
- 首页流量图卡片显示上传/下载名称。 - 首页流量图卡片显示上传/下载名称。
- 首页支持轻量模式切换。 - 首页支持轻量模式切换。
@@ -205,17 +252,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 +279,9 @@
- 新增代理命令。 - 新增代理命令。
#### 修复 #### 修复
1. **系统** 1. **系统**
- 修复 Windows 热键崩溃。 - 修复 Windows 热键崩溃。
- 修复 macOS 无框标题。 - 修复 macOS 无框标题。
- 修复 macOS 静默启动崩溃。 - 修复 macOS 静默启动崩溃。
@@ -241,7 +294,9 @@
- 修复构建失败问题。 - 修复构建失败问题。
#### 优化 #### 优化
1. **性能** 1. **性能**
- 重构后端,巨幅性能优化。 - 重构后端,巨幅性能优化。
- 优化首页组件性能。 - 优化首页组件性能。
- 优化流量图表资源使用。 - 优化流量图表资源使用。
@@ -254,6 +309,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
+37 -36
View File
@@ -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", "prepare": "node scripts/prepare.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,15 +31,15 @@
"@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.1",
"@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.2",
"@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.2",
"@tauri-apps/plugin-process": "^2.2.1", "@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "2.2.1", "@tauri-apps/plugin-shell": "2.2.1",
@@ -46,61 +47,61 @@
"@tauri-apps/plugin-window-state": "^2.2.2", "@tauri-apps/plugin-window-state": "^2.2.2",
"@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.9.0",
"chart.js": "^4.4.9", "chart.js": "^4.4.9",
"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.2",
"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.57.0",
"react-i18next": "15.5.1", "react-i18next": "15.5.2",
"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.12.8",
"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.42.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"
} }
+647 -674
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);
+154 -199
View File
@@ -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",
@@ -1106,7 +1106,7 @@ dependencies = [
"warp", "warp",
"winapi", "winapi",
"winreg 0.55.0", "winreg 0.55.0",
"zip", "zip 4.0.0",
] ]
[[package]] [[package]]
@@ -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"
@@ -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",
@@ -7006,9 +7005,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 +7019,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 +7052,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 +7070,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 +7088,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",
@@ -7167,7 +7165,7 @@ dependencies = [
"tokio", "tokio",
"url", "url",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"zip", "zip 2.6.1",
] ]
[[package]] [[package]]
@@ -7285,12 +7283,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 +7512,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 +7523,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",
@@ -7730,7 +7728,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 +7787,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"
@@ -8144,7 +8160,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 +8530,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 +8786,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 +8825,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"
@@ -8904,29 +8909,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6", "windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6", "windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6", "windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6", "windows_i686_gnullvm",
"windows_i686_msvc 0.52.6", "windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6", "windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6", "windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6", "windows_x86_64_msvc 0.52.6",
] ]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]] [[package]]
name = "windows-version" name = "windows-version"
version = "0.1.4" version = "0.1.4"
@@ -8954,12 +8943,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.42.2" version = "0.42.2"
@@ -8978,12 +8961,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.42.2" version = "0.42.2"
@@ -9002,24 +8979,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]] [[package]]
name = "windows_i686_gnullvm" name = "windows_i686_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.42.2" version = "0.42.2"
@@ -9038,12 +9003,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.42.2" version = "0.42.2"
@@ -9062,12 +9021,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.42.2" version = "0.42.2"
@@ -9086,12 +9039,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.42.2" version = "0.42.2"
@@ -9110,12 +9057,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.40" version = "0.5.40"
@@ -9313,21 +9254,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"
@@ -9517,29 +9455,46 @@ name = "zip"
version = "2.6.1" version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"indexmap 2.8.0",
"memchr",
]
[[package]]
name = "zip"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd"
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"
+18 -18
View File
@@ -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"
@@ -56,18 +56,18 @@ tauri = { version = "2.5.1", features = [
] } ] }
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.1"
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.1"
tauri-plugin-clipboard-manager = "2.2.2" tauri-plugin-clipboard-manager = "2.2.2"
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.2"
zip = "2.6.1" zip = "4.0.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.26.2"
futures = "0.3.31" futures = "0.3.31"
sys-locale = "0.3.2" sys-locale = "0.3.2"
@@ -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",
@@ -100,7 +100,7 @@ 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 = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-global-shortcut = "2.2.0" tauri-plugin-global-shortcut = "2.2.1"
tauri-plugin-updater = "2.7.1" tauri-plugin-updater = "2.7.1"
[features] [features]
@@ -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"
]
}
-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"
+6 -2
View File
@@ -22,7 +22,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
.await; .await;
match profiles_result { match profiles_result {
Ok(Ok(profiles)) => Ok(profiles), Ok(Ok(profiles)) => Ok(*profiles),
Ok(Err(join_err)) => { Ok(Err(join_err)) => {
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err); logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err);
Ok(IProfiles { Ok(IProfiles {
@@ -41,7 +41,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await { match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await {
Ok(profiles) => { Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用latest()成功获取配置"); logging!(info, Type::Cmd, true, "使用latest()成功获取配置");
Ok(profiles) Ok(*profiles)
} }
Err(_) => { Err(_) => {
logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置"); logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置");
@@ -93,6 +93,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();
+55 -49
View File
@@ -1,71 +1,77 @@
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)
} }
#[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)
} }
+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(())
}
}
}
} }
+7
View File
@@ -200,6 +200,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>,
} }
@@ -275,6 +278,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 +355,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,6 +497,7 @@ 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>,
} }
@@ -561,6 +567,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,
} }
} }
+2 -2
View File
@@ -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,
+68 -27
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,
+3 -5
View File
@@ -845,11 +845,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(());
}
} }
} }
+75 -43
View File
@@ -270,7 +270,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 +288,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 +316,83 @@ 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(())
} }
/// 更新托盘提示 /// 更新托盘提示
@@ -591,6 +617,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);
+117 -10
View File
@@ -33,37 +33,50 @@ 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(&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()); // 处理 JS 执行结果中的特殊字符
let unescaped_result = unescape_js_string(&result);
if unescaped_result.starts_with("__error_flag__") {
anyhow::bail!(unescaped_result[15..].to_owned());
} }
if result == "\"\"" { if unescaped_result == "\"\"" {
anyhow::bail!("main function should return object"); anyhow::bail!("main function should return object");
} }
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
// 安全地解析 JSON 结果
let res: Result<Mapping, Error> = parse_json_safely(&unescaped_result);
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 +90,74 @@ pub fn use_script(
} }
} }
// 解析 JSON 字符串,处理可能的转义字符
fn parse_json_safely(json_str: &str) -> Result<Mapping, Error> {
// 移除可能的引号包裹
let json_str = if json_str.starts_with('"') && json_str.ends_with('"') {
&json_str[1..json_str.len() - 1]
} else {
json_str
};
// 处理可能的 JSON 字符串中的转义字符
let json_str = json_str.replace("\\\"", "\"");
Ok(serde_json::from_str::<Mapping>(&json_str)?)
}
// 转义 JS 字符串中的特殊字符
fn escape_js_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\'' => result.push_str("\\'"),
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
'\0' => result.push_str("\\0"),
_ => result.push(c),
}
}
result
}
// 反转义 JS 字符串中的特殊字符
fn unescape_js_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('0') => result.push('\0'),
Some('\\') => result.push('\\'),
Some('\'') => result.push('\''),
Some('"') => result.push('"'),
Some('u') => {
// 处理转义序列
let hex = chars.by_ref().take(4).collect::<String>();
if let Ok(codepoint) = u32::from_str_radix(&hex, 16) {
if let Some(ch) = char::from_u32(codepoint) {
result.push(ch);
}
}
}
Some(other) => result.push(other),
None => break,
}
} else {
result.push(c);
}
}
result
}
#[test] #[test]
fn test_script() { fn test_script() {
let script = r#" let script = r#"
@@ -104,6 +185,32 @@ 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(test_string);
let unescaped = unescape_js_string(&escaped);
assert_eq!(test_string, unescaped);
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 @@
+130 -27
View File
@@ -13,6 +13,19 @@ pub fn open_or_close_dashboard() {
println!("Attempting to open/close dashboard"); println!("Attempting to open/close dashboard");
log::info!(target: "app", "Attempting to open/close dashboard"); log::info!(target: "app", "Attempting to open/close dashboard");
// 检查是否在轻量模式下
if crate::module::lightweight::is_in_lightweight_mode() {
println!("Currently in lightweight mode, exiting lightweight mode");
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode();
println!("Creating new window after exiting lightweight mode");
log::info!(target: "app", "Creating new window after exiting lightweight mode");
resolve::create_window(true);
return;
}
if let Some(window) = handle::Handle::global().get_window() { if let Some(window) = handle::Handle::global().get_window() {
println!("Found existing window"); println!("Found existing window");
log::info!(target: "app", "Found existing window"); log::info!(target: "app", "Found existing window");
@@ -49,8 +62,9 @@ pub fn open_or_close_dashboard() {
} }
} }
/// 优化的应用退出函数 /// 异步优化的应用退出函数
pub fn quit() { pub fn quit() {
use crate::process::AsyncHandler;
log::debug!(target: "app", "启动退出流程"); log::debug!(target: "app", "启动退出流程");
// 获取应用句柄并设置退出标志 // 获取应用句柄并设置退出标志
@@ -60,55 +74,144 @@ 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(); log::info!(target: "app", "开始异步清理资源");
app_handle.exit(match cleanup_result { let cleanup_result = clean_async().await;
true => 0,
false => 1, log::info!(target: "app", "资源清理完成,退出代码: {}", 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 { log::info!(target: "app", "开始执行异步清理操作...");
// 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), 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;
log::info!(
target: "app",
"异步清理操作完成 - 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 {
log::info!(target: "app", "开始执行清理操作...");
// 使用已有的异步清理函数
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) => {
log::info!(target: "app", "清理操作完成,结果: {}", result);
result
}
Err(_) => {
log::warn!(target: "app", "清理操作超时,返回成功状态避免阻塞");
true
}
}
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
+12 -10
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");
@@ -205,6 +204,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(())
}) })
+88 -64
View File
@@ -1,47 +1,82 @@
use crate::{ use crate::{
config::Config, config::Config,
core::{handle, timer::Timer}, core::{handle, timer::Timer},
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(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,
"Add timer listener when creating window in silent start mode"
);
set_lightweight_mode(true);
enable_auto_light_weight_mode();
}
});
}
pub fn auto_lightweight_mode_init() {
if let Some(app_handle) = handle::Handle::global().app_handle() {
// 通过 app_handle.state 保证同步
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,
"Add timer listener when creating window normally"
);
set_lightweight_mode(true);
enable_auto_light_weight_mode();
}
}
} }
// 检查是否处于轻量模式 // 检查是否处于轻量模式
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); });
} }
pub fn enable_auto_light_weight_mode() { pub fn enable_auto_light_weight_mode() {
@@ -69,49 +104,30 @@ 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();
} }
// 添加从轻量模式恢复的函数 // 添加从轻量模式恢复的函数
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;
} }
#[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 +172,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 +196,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");
+189 -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,6 +292,17 @@ pub fn create_window(is_show: bool) -> bool {
is_show is_show
); );
if !is_show {
logging!(
info,
Type::Window,
true,
"Not to create window when starting in silent mode"
);
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, "主窗口已存在,将显示现有窗口");
@@ -299,7 +344,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 +428,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
@@ -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);
} }
+6 -22
View File
@@ -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;
+41 -27
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";
@@ -51,13 +54,16 @@ export const useCustomTheme = () => {
const timerId = setTimeout(() => { const timerId = setTimeout(() => {
if (!isMounted) return; if (!isMounted) return;
appWindow.theme().then((systemTheme) => { appWindow
if (isMounted && systemTheme) { .theme()
setMode(systemTheme); .then((systemTheme) => {
} if (isMounted && systemTheme) {
}).catch(err => { setMode(systemTheme);
console.error("Failed to get initial system theme:", err); }
}); })
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
}, 0); }, 0);
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => { const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
@@ -69,13 +75,15 @@ export const useCustomTheme = () => {
return () => { return () => {
isMounted = false; isMounted = false;
clearTimeout(timerId); clearTimeout(timerId);
unlistenPromise.then((unlistenFn) => { unlistenPromise
if (typeof unlistenFn === 'function') { .then((unlistenFn) => {
unlistenFn(); if (typeof unlistenFn === "function") {
} 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 +94,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) => {
@@ -153,21 +164,24 @@ export const useCustomTheme = () => {
const rootEle = document.documentElement; const rootEle = document.documentElement;
if (rootEle) { if (rootEle) {
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d"; const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5"; const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee"; const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const dividerColor = 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),
); );
} }
// inject css // inject css
let styleElement = document.querySelector("style#verge-theme"); let styleElement = document.querySelector("style#verge-theme");
+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;
+2 -2
View File
@@ -37,7 +37,7 @@ export function useHeadStateNew() {
try { try {
const data = JSON.parse( const data = JSON.parse(
localStorage.getItem(HEAD_STATE_KEY)! localStorage.getItem(HEAD_STATE_KEY)!,
) as HeadStateStorage; ) as HeadStateStorage;
const value = data[current] || {}; const value = data[current] || {};
@@ -74,7 +74,7 @@ export function useHeadStateNew() {
return ret; return ret;
}); });
}, },
[current] [current],
); );
return [state, setHeadState] as const; return [state, setHeadState] as const;
+86 -71
View File
@@ -1,9 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Button, Button,
Box, Box,
Dialog, Dialog,
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogActions, DialogActions,
IconButton, IconButton,
@@ -14,7 +14,7 @@ import {
Divider, Divider,
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";
@@ -54,74 +54,80 @@ export const ProviderButton = () => {
// 检查是否有提供者 // 检查是否有提供者
const hasProviders = Object.keys(ruleProviders || {}).length > 0; const hasProviders = Object.keys(ruleProviders || {}).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 ruleProviderUpdate(name); await ruleProviderUpdate(name);
// 刷新数据 // 刷新数据
await refreshRules(); await refreshRules();
await refreshRuleProviders(); await refreshRuleProviders();
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(ruleProviders || {}); const allProviders = Object.keys(ruleProviders || {});
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 ruleProviderUpdate(name); await ruleProviderUpdate(name);
// 每个更新完成后更新状态 // 每个更新完成后更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) { } catch (err) {
console.error(`更新 ${name} 失败`, err); console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程 // 继续执行下一个,不中断整体流程
} }
} }
// 刷新数据 // 刷新数据
await refreshRules(); await refreshRules();
await refreshRuleProviders(); await refreshRuleProviders();
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 (
@@ -134,15 +140,14 @@ export const ProviderButton = () => {
> >
{t("Rule Provider")} {t("Rule 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("Rule Providers")}</Typography> <Typography variant="h6">{t("Rule Providers")}</Typography>
<Button <Button
variant="contained" variant="contained"
@@ -153,49 +158,52 @@ export const ProviderButton = () => {
</Button> </Button>
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<List sx={{ py: 0, minHeight: 250 }}> <List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(ruleProviders || {}).map(([key, item]) => { {Object.entries(ruleProviders || {}).map(([key, item]) => {
const provider = item as RuleProviderItem; const provider = item as RuleProviderItem;
const time = dayjs(provider.updatedAt); const time = dayjs(provider.updatedAt);
const isUpdating = updating[key]; const isUpdating = updating[key];
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,
borderColor: alpha(primary.main, 0.3) borderColor: alpha(primary.main, 0.3),
} },
}; };
} },
]} ]}
> >
<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"
@@ -203,14 +211,19 @@ 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.ruleCount} {provider.ruleCount}
</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>
} }
@@ -219,30 +232,32 @@ export const ProviderButton = () => {
<TypeBox component="span"> <TypeBox component="span">
{provider.vehicleType} {provider.vehicleType}
</TypeBox> </TypeBox>
<TypeBox component="span"> <TypeBox component="span">{provider.behavior}</TypeBox>
{provider.behavior}
</TypeBox>
</Box> </Box>
} }
/> />
<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"
onClick={() => updateProvider(key)} onClick={() => updateProvider(key)}
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}
> >
@@ -254,7 +269,7 @@ export const ProviderButton = () => {
})} })}
</List> </List>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose} variant="outlined"> <Button onClick={handleClose} variant="outlined">
{t("Close")} {t("Close")}
@@ -82,21 +82,21 @@ export const BackupConfigViewer = memo(
if (!url) { if (!url) {
urlRef.current?.focus(); urlRef.current?.focus();
showNotice('error', t("WebDAV URL Required")); showNotice("error", t("WebDAV URL Required"));
throw new Error(t("WebDAV URL Required")); throw new Error(t("WebDAV URL Required"));
} else if (!isValidUrl(url)) { } else if (!isValidUrl(url)) {
urlRef.current?.focus(); urlRef.current?.focus();
showNotice('error', t("Invalid WebDAV URL")); showNotice("error", t("Invalid WebDAV URL"));
throw new Error(t("Invalid WebDAV URL")); throw new Error(t("Invalid WebDAV URL"));
} }
if (!username) { if (!username) {
usernameRef.current?.focus(); usernameRef.current?.focus();
showNotice('error', t("WebDAV URL Required")); showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Username Required")); throw new Error(t("Username Required"));
} }
if (!password) { if (!password) {
passwordRef.current?.focus(); passwordRef.current?.focus();
showNotice('error', t("WebDAV URL Required")); showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Password Required")); throw new Error(t("Password Required"));
} }
}; };
@@ -110,11 +110,11 @@ export const BackupConfigViewer = memo(
data.username.trim(), data.username.trim(),
data.password, data.password,
).then(() => { ).then(() => {
showNotice('success', t("WebDAV Config Saved")); showNotice("success", t("WebDAV Config Saved"));
onSaveSuccess(); onSaveSuccess();
}); });
} catch (error) { } catch (error) {
showNotice('error', t("WebDAV Config Save Failed", { error }), 3000); showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -125,11 +125,11 @@ export const BackupConfigViewer = memo(
try { try {
setLoading(true); setLoading(true);
await createWebdavBackup().then(async () => { await createWebdavBackup().then(async () => {
showNotice('success', t("Backup Created")); showNotice("success", t("Backup Created"));
await onBackupSuccess(); await onBackupSuccess();
}); });
} catch (error) { } catch (error) {
showNotice('error', t("Backup Failed", { error })); showNotice("error", t("Backup Failed", { error }));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -61,7 +61,7 @@ export const BackupTableViewer = memo(
const handleRestore = useLockFn(async (filename: string) => { const handleRestore = useLockFn(async (filename: string) => {
await restoreWebDavBackup(filename).then(() => { await restoreWebDavBackup(filename).then(() => {
showNotice('success', t("Restore Success, App will restart in 1s")); showNotice("success", t("Restore Success, App will restart in 1s"));
}); });
await restartApp(); await restartApp();
}); });
@@ -52,7 +52,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const errorMsg = await changeClashCore(core); const errorMsg = await changeClashCore(core);
if (errorMsg) { if (errorMsg) {
showNotice('error', errorMsg); showNotice("error", errorMsg);
setChangingCore(null); setChangingCore(null);
return; return;
} }
@@ -65,7 +65,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
}, 500); }, 500);
} catch (err: any) { } catch (err: any) {
setChangingCore(null); setChangingCore(null);
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -73,11 +73,11 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
try { try {
setRestarting(true); setRestarting(true);
await restartCore(); await restartCore();
showNotice('success', t(`Clash Core Restarted`)); showNotice("success", t(`Clash Core Restarted`));
setRestarting(false); setRestarting(false);
} catch (err: any) { } catch (err: any) {
setRestarting(false); setRestarting(false);
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -86,14 +86,14 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
setUpgrading(true); setUpgrading(true);
await upgradeCore(); await upgradeCore();
setUpgrading(false); setUpgrading(false);
showNotice('success', t(`Core Version Updated`)); showNotice("success", t(`Core Version Updated`));
} catch (err: any) { } catch (err: any) {
setUpgrading(false); setUpgrading(false);
const errMsg = err.response?.data?.message || err.toString(); const errMsg = err.response?.data?.message || err.toString();
const showMsg = errMsg.includes("already using latest version") const showMsg = errMsg.includes("already using latest version")
? "Already Using Latest Core Version" ? "Already Using Latest Core Version"
: errMsg; : errMsg;
showNotice('error', t(showMsg)); showNotice("error", t(showMsg));
} }
}); });
+266 -246
View File
@@ -11,7 +11,7 @@ import {
ListItem, ListItem,
ListItemText, ListItemText,
Stack, Stack,
TextField TextField,
} from "@mui/material"; } from "@mui/material";
import { useLockFn, useRequest } from "ahooks"; import { useLockFn, useRequest } from "ahooks";
import { forwardRef, useImperativeHandle, useState } from "react"; import { forwardRef, useImperativeHandle, useState } from "react";
@@ -26,127 +26,136 @@ interface ClashPortViewerRef {
close: () => void; close: () => void;
} }
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025; const generateRandomPort = () =>
Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerProps>( export const ClashPortViewer = forwardRef<
(props, ref) => { ClashPortViewerRef,
const { t } = useTranslation(); ClashPortViewerProps
const { clashInfo, patchInfo } = useClashInfo(); >((props, ref) => {
const { verge, patchVerge } = useVerge(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
// Mixed Port // Mixed Port
const [mixedPort, setMixedPort] = useState( const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897 verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897,
); );
// 其他端口状态 // 其他端口状态
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898); const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
const [socksEnabled, setSocksEnabled] = useState(verge?.verge_socks_enabled ?? false); const [socksEnabled, setSocksEnabled] = useState(
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899); verge?.verge_socks_enabled ?? false,
const [httpEnabled, setHttpEnabled] = useState(verge?.verge_http_enabled ?? false); );
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895); const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
const [redirEnabled, setRedirEnabled] = useState(verge?.verge_redir_enabled ?? false); const [httpEnabled, setHttpEnabled] = useState(
const [tproxyPort, setTproxyPort] = useState(verge?.verge_tproxy_port ?? 7896); verge?.verge_http_enabled ?? false,
const [tproxyEnabled, setTproxyEnabled] = useState(verge?.verge_tproxy_enabled ?? false); );
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895);
const [redirEnabled, setRedirEnabled] = useState(
verge?.verge_redir_enabled ?? false,
);
const [tproxyPort, setTproxyPort] = useState(
verge?.verge_tproxy_port ?? 7896,
);
const [tproxyEnabled, setTproxyEnabled] = useState(
verge?.verge_tproxy_enabled ?? false,
);
// 添加保存请求,防止GUI卡死 // 添加保存请求,防止GUI卡死
const { loading, run: saveSettings } = useRequest( const { loading, run: saveSettings } = useRequest(
async (params: { async (params: { clashConfig: any; vergeConfig: any }) => {
clashConfig: any; const { clashConfig, vergeConfig } = params;
vergeConfig: any; await Promise.all([patchInfo(clashConfig), patchVerge(vergeConfig)]);
}) => { },
const { clashConfig, vergeConfig } = params; {
await Promise.all([ manual: true,
patchInfo(clashConfig), onSuccess: () => {
patchVerge(vergeConfig) setOpen(false);
]); showNotice("success", t("Port settings saved")); // 调用提示函数
}, },
{ onError: () => {
manual: true, showNotice("error", t("Failed to save settings")); // 调用提示函数
onSuccess: () => {
setOpen(false);
showNotice("success", t("Port settings saved")); // 调用提示函数
},
onError: () => {
showNotice("error", t("Failed to save settings")); // 调用提示函数
}
}
);
useImperativeHandle(ref, () => ({
open: () => {
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
setSocksPort(verge?.verge_socks_port ?? 7898);
setSocksEnabled(verge?.verge_socks_enabled ?? false);
setHttpPort(verge?.verge_port ?? 7899);
setHttpEnabled(verge?.verge_http_enabled ?? false);
setRedirPort(verge?.verge_redir_port ?? 7895);
setRedirEnabled(verge?.verge_redir_enabled ?? false);
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
setOpen( true);
}, },
close: () => setOpen(false), },
})); );
const onSave = useLockFn(async () => { useImperativeHandle(ref, () => ({
// 端口冲突检测 open: () => {
const portList = [ setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
mixedPort, setSocksPort(verge?.verge_socks_port ?? 7898);
socksEnabled ? socksPort : -1, setSocksEnabled(verge?.verge_socks_enabled ?? false);
httpEnabled ? httpPort : -1, setHttpPort(verge?.verge_port ?? 7899);
redirEnabled ? redirPort : -1, setHttpEnabled(verge?.verge_http_enabled ?? false);
tproxyEnabled ? tproxyPort : -1 setRedirPort(verge?.verge_redir_port ?? 7895);
].filter(p => p !== -1); setRedirEnabled(verge?.verge_redir_enabled ?? false);
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
setOpen(true);
},
close: () => setOpen(false),
}));
if (new Set(portList).size !== portList.length) { const onSave = useLockFn(async () => {
return; // 端口冲突检测
} const portList = [
mixedPort,
socksEnabled ? socksPort : -1,
httpEnabled ? httpPort : -1,
redirEnabled ? redirPort : -1,
tproxyEnabled ? tproxyPort : -1,
].filter((p) => p !== -1);
// 验证端口范围 if (new Set(portList).size !== portList.length) {
const isValidPort = (port: number) => port >= 1 && port <= 65535; return;
const allPortsValid = [ }
mixedPort,
socksEnabled ? socksPort : 0,
httpEnabled ? httpPort : 0,
redirEnabled ? redirPort : 0,
tproxyEnabled ? tproxyPort : 0
].every(port => port === 0 || isValidPort(port));
if (!allPortsValid) { // 验证端口范围
return; const isValidPort = (port: number) => port >= 1 && port <= 65535;
} const allPortsValid = [
mixedPort,
socksEnabled ? socksPort : 0,
httpEnabled ? httpPort : 0,
redirEnabled ? redirPort : 0,
tproxyEnabled ? tproxyPort : 0,
].every((port) => port === 0 || isValidPort(port));
// 准备配置数据 if (!allPortsValid) {
const clashConfig = { return;
"mixed-port": mixedPort, }
"socks-port": socksPort,
port: httpPort,
"redir-port": redirPort,
"tproxy-port": tproxyPort
};
const vergeConfig = { // 准备配置数据
verge_mixed_port: mixedPort, const clashConfig = {
verge_socks_port: socksPort, "mixed-port": mixedPort,
verge_socks_enabled: socksEnabled, "socks-port": socksPort,
verge_port: httpPort, port: httpPort,
verge_http_enabled: httpEnabled, "redir-port": redirPort,
verge_redir_port: redirPort, "tproxy-port": tproxyPort,
verge_redir_enabled: redirEnabled, };
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled
};
// 提交保存请求 const vergeConfig = {
await saveSettings({ clashConfig, vergeConfig }); verge_mixed_port: mixedPort,
}); verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: httpPort,
verge_http_enabled: httpEnabled,
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled,
};
// 优化的数字输入处理 // 提交保存请求
const handleNumericChange = (setter: (value: number) => void) => (e: React.ChangeEvent<HTMLInputElement>) => { await saveSettings({ clashConfig, vergeConfig });
const value = e.target.value.replace(/\D+/, ''); });
if (value === '') {
// 优化的数字输入处理
const handleNumericChange =
(setter: (value: number) => void) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D+/, "");
if (value === "") {
setter(0); setter(0);
return; return;
} }
@@ -157,190 +166,201 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerPro
} }
}; };
return ( return (
<BaseDialog <BaseDialog
open={open} open={open}
title={t("Port Configuration")} title={t("Port Configuration")}
contentSx={{ contentSx={{
width: 400 width: 400,
}} }}
okBtn={ okBtn={
loading ? ( loading ? (
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} /> <CircularProgress size={20} />
{t("Saving...")} {t("Saving...")}
</Stack> </Stack>
) : t("Save") ) : (
} t("Save")
cancelBtn={t("Cancel")} )
onClose={() => setOpen(false)} }
onCancel={() => setOpen(false)} cancelBtn={t("Cancel")}
onOk={onSave} onClose={() => setOpen(false)}
> onCancel={() => setOpen(false)}
<List sx={{ width: "100%" }}> onOk={onSave}
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> >
<ListItemText <List sx={{ width: "100%" }}>
primary={t("Mixed Port")} <ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
primaryTypographyProps={{ fontSize: 12 }} <ListItemText
primary={t("Mixed Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={mixedPort}
onChange={(e) =>
setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
inputProps={{ style: { fontSize: 12 } }}
/> />
<div style={{ display: "flex", alignItems: "center" }}> <IconButton
<TextField size="small"
size="small" onClick={() => setMixedPort(generateRandomPort())}
sx={{ width: 80, mr: 0.5, fontSize: 12 }} title={t("Random Port")}
value={mixedPort} sx={{ mr: 0.5 }}
onChange={(e) => setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} >
inputProps={{ style: { fontSize: 12 } }} <Shuffle fontSize="small" />
/> </IconButton>
<IconButton <Switch
size="small" size="small"
onClick={() => setMixedPort(generateRandomPort())} checked={true}
title={t("Random Port")} disabled={true}
sx={{ mr: 0.5 }} sx={{ ml: 0.5, opacity: 0.7 }}
> />
<Shuffle fontSize="small" /> </div>
</IconButton> </ListItem>
<Switch
size="small"
checked={true}
disabled={true}
sx={{ ml: 0.5, opacity: 0.7 }}
/>
</div>
</ListItem>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Socks Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort}
onChange={(e) =>
setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!socksEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setSocksPort(generateRandomPort())}
title={t("Random Port")}
disabled={!socksEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={socksEnabled}
onChange={(_, c) => setSocksEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Http Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort}
onChange={(e) =>
setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!httpEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setHttpPort(generateRandomPort())}
title={t("Random Port")}
disabled={!httpEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={httpEnabled}
onChange={(_, c) => setHttpEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
{OS !== "windows" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> <ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText <ListItemText
primary={t("Socks Port")} primary={t("Redir Port")}
primaryTypographyProps={{ fontSize: 12 }} primaryTypographyProps={{ fontSize: 12 }}
/> />
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<TextField <TextField
size="small" size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }} sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort} value={redirPort}
onChange={(e) => setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} onChange={(e) =>
disabled={!socksEnabled} setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!redirEnabled}
inputProps={{ style: { fontSize: 12 } }} inputProps={{ style: { fontSize: 12 } }}
/> />
<IconButton <IconButton
size="small" size="small"
onClick={() => setSocksPort(generateRandomPort())} onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")} title={t("Random Port")}
disabled={!socksEnabled} disabled={!redirEnabled}
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
> >
<Shuffle fontSize="small" /> <Shuffle fontSize="small" />
</IconButton> </IconButton>
<Switch <Switch
size="small" size="small"
checked={socksEnabled} checked={redirEnabled}
onChange={(_, c) => setSocksEnabled(c)} onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }} sx={{ ml: 0.5 }}
/> />
</div> </div>
</ListItem> </ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> <ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText <ListItemText
primary={t("HTTP Port")} primary={t("Tproxy Port")}
primaryTypographyProps={{ fontSize: 12 }} primaryTypographyProps={{ fontSize: 12 }}
/> />
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<TextField <TextField
size="small" size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }} sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort} value={tproxyPort}
onChange={(e) => setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} onChange={(e) =>
disabled={!httpEnabled} setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!tproxyEnabled}
inputProps={{ style: { fontSize: 12 } }} inputProps={{ style: { fontSize: 12 } }}
/> />
<IconButton <IconButton
size="small" size="small"
onClick={() => setHttpPort(generateRandomPort())} onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")} title={t("Random Port")}
disabled={!httpEnabled} disabled={!tproxyEnabled}
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
> >
<Shuffle fontSize="small" /> <Shuffle fontSize="small" />
</IconButton> </IconButton>
<Switch <Switch
size="small" size="small"
checked={httpEnabled} checked={tproxyEnabled}
onChange={(_, c) => setHttpEnabled(c)} onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }} sx={{ ml: 0.5 }}
/> />
</div> </div>
</ListItem> </ListItem>
)}
{OS !== "windows" && ( </List>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> </BaseDialog>
<ListItemText );
primary={t("Redir Port")} });
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={redirPort}
onChange={(e) => setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!redirEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")}
disabled={!redirEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={redirEnabled}
onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Tproxy Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={tproxyPort}
onChange={(e) => setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!tproxyEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")}
disabled={!tproxyEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={tproxyEnabled}
onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
</List>
</BaseDialog>
);
}
);
@@ -13,7 +13,7 @@ import {
ListItemText, ListItemText,
Snackbar, Snackbar,
TextField, TextField,
Tooltip Tooltip,
} from "@mui/material"; } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
@@ -42,58 +42,72 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置 // 保存配置
const onSave = useLockFn(async () => { const onSave = useLockFn(async () => {
if (!controller.trim()) { if (!controller.trim()) {
showNotice('error', t("Controller address cannot be empty"), 3000); showNotice("error", t("Controller address cannot be empty"), 3000);
return; return;
} }
if (!secret.trim()) { if (!secret.trim()) {
showNotice('error', t("Secret cannot be empty"), 3000); showNotice("error", t("Secret cannot be empty"), 3000);
return; return;
} }
try { try {
setIsSaving(true); setIsSaving(true);
await patchInfo({ "external-controller": controller, secret }); await patchInfo({ "external-controller": controller, secret });
showNotice('success', t("Configuration saved successfully"), 2000); showNotice("success", t("Configuration saved successfully"), 2000);
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || t("Failed to save configuration"), 4000); showNotice(
"error",
err.message || t("Failed to save configuration"),
4000,
);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}); });
// 复制到剪贴板 // 复制到剪贴板
const handleCopyToClipboard = useLockFn(async (text: string, type: string) => { const handleCopyToClipboard = useLockFn(
try { async (text: string, type: string) => {
await navigator.clipboard.writeText(text); try {
setCopySuccess(type); await navigator.clipboard.writeText(text);
setTimeout(() => setCopySuccess(null), 2000); setCopySuccess(type);
} catch (err) { setTimeout(() => setCopySuccess(null), 2000);
showNotice('error', t("Failed to copy"), 2000); } catch (err) {
} showNotice("error", t("Failed to copy"), 2000);
}); }
},
);
return ( return (
<BaseDialog <BaseDialog
open={open} open={open}
title={t("External Controller")} title={t("External Controller")}
contentSx={{ width: 400 }} contentSx={{ width: 400 }}
okBtn={isSaving ? ( okBtn={
<Box display="flex" alignItems="center" gap={1}> isSaving ? (
<CircularProgress size={16} color="inherit" /> <Box display="flex" alignItems="center" gap={1}>
{t("Saving...")} <CircularProgress size={16} color="inherit" />
</Box> {t("Saving...")}
) : ( </Box>
t("Save") ) : (
)} t("Save")
)
}
cancelBtn={t("Cancel")} cancelBtn={t("Cancel")}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<List> <List>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}> <ListItem
sx={{
padding: "5px 2px",
display: "flex",
justifyContent: "space-between",
}}
>
<ListItemText primary={t("External Controller")} /> <ListItemText primary={t("External Controller")} />
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<TextField <TextField
@@ -101,11 +115,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{ sx={{
width: 175, width: 175,
opacity: 1, opacity: 1,
pointerEvents: 'auto' pointerEvents: "auto",
}} }}
value={controller} value={controller}
placeholder="Required" placeholder="Required"
onChange={e => setController(e.target.value)} onChange={(e) => setController(e.target.value)}
disabled={isSaving} disabled={isSaving}
/> />
<Tooltip title={t("Copy to clipboard")}> <Tooltip title={t("Copy to clipboard")}>
@@ -121,7 +135,13 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
</Box> </Box>
</ListItem> </ListItem>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}> <ListItem
sx={{
padding: "5px 2px",
display: "flex",
justifyContent: "space-between",
}}
>
<ListItemText primary={t("Core Secret")} /> <ListItemText primary={t("Core Secret")} />
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<TextField <TextField
@@ -129,11 +149,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{ sx={{
width: 175, width: 175,
opacity: 1, opacity: 1,
pointerEvents: 'auto' pointerEvents: "auto",
}} }}
value={secret} value={secret}
placeholder={t("Recommended")} placeholder={t("Recommended")}
onChange={e => setSecret(e.target.value)} onChange={(e) => setSecret(e.target.value)}
disabled={isSaving} disabled={isSaving}
/> />
<Tooltip title={t("Copy to clipboard")}> <Tooltip title={t("Copy to clipboard")}>
@@ -153,13 +173,12 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<Snackbar <Snackbar
open={copySuccess !== null} open={copySuccess !== null}
autoHideDuration={2000} autoHideDuration={2000}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
> >
<Alert severity="success"> <Alert severity="success">
{copySuccess === "controller" {copySuccess === "controller"
? t("Controller address copied to clipboard") ? t("Controller address copied to clipboard")
: t("Secret copied to clipboard") : t("Secret copied to clipboard")}
}
</Alert> </Alert>
</Snackbar> </Snackbar>
</BaseDialog> </BaseDialog>
+45 -29
View File
@@ -59,7 +59,13 @@ const DEFAULT_DNS_CONFIG = {
"*.msftncsi.com", "*.msftncsi.com",
"www.msftconnecttest.com", "www.msftconnecttest.com",
], ],
"default-nameserver": ["system", "223.6.6.6", "8.8.8.8", "2400:3200::1", "2001:4860:4860::8888"], "default-nameserver": [
"system",
"223.6.6.6",
"8.8.8.8",
"2400:3200::1",
"2001:4860:4860::8888",
],
nameserver: [ nameserver: [
"8.8.8.8", "8.8.8.8",
"https://doh.pub/dns-query", "https://doh.pub/dns-query",
@@ -70,7 +76,7 @@ const DEFAULT_DNS_CONFIG = {
"proxy-server-nameserver": [ "proxy-server-nameserver": [
"https://doh.pub/dns-query", "https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query", "https://dns.alidns.com/dns-query",
"tls://223.5.5.5" "tls://223.5.5.5",
], ],
"direct-nameserver": [], "direct-nameserver": [],
"direct-nameserver-follow-policy": false, "direct-nameserver-follow-policy": false,
@@ -219,8 +225,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"], dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"], useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
useSystemHosts: useSystemHosts:
dnsConfig["use-system-hosts"] ?? dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
DEFAULT_DNS_CONFIG["use-system-hosts"],
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6, ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: fakeIpFilter:
dnsConfig["fake-ip-filter"]?.join(", ") ?? dnsConfig["fake-ip-filter"]?.join(", ") ??
@@ -229,7 +234,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig.nameserver?.join(", ") ?? dnsConfig.nameserver?.join(", ") ??
DEFAULT_DNS_CONFIG.nameserver.join(", "), DEFAULT_DNS_CONFIG.nameserver.join(", "),
fallback: fallback:
dnsConfig.fallback?.join(", ") ?? DEFAULT_DNS_CONFIG.fallback.join(", "), dnsConfig.fallback?.join(", ") ??
DEFAULT_DNS_CONFIG.fallback.join(", "),
defaultNameserver: defaultNameserver:
dnsConfig["default-nameserver"]?.join(", ") ?? dnsConfig["default-nameserver"]?.join(", ") ??
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
@@ -299,9 +305,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
// 从表单值更新YAML内容 // 从表单值更新YAML内容
const updateYamlFromValues = () => { const updateYamlFromValues = () => {
const config: Record<string, any> = {}; const config: Record<string, any> = {};
const dnsConfig = generateDnsConfig(); const dnsConfig = generateDnsConfig();
if (Object.keys(dnsConfig).length > 0) { if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig; config.dns = dnsConfig;
@@ -311,7 +316,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
if (Object.keys(hosts).length > 0) { if (Object.keys(hosts).length > 0) {
config.hosts = hosts; config.hosts = hosts;
} }
setYamlContent(yaml.dump(config, { forceQuotes: true })); setYamlContent(yaml.dump(config, { forceQuotes: true }));
}; };
@@ -320,10 +325,10 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
try { try {
const parsedYaml = yaml.load(yamlContent) as any; const parsedYaml = yaml.load(yamlContent) as any;
if (!parsedYaml) return; if (!parsedYaml) return;
updateValuesFromConfig(parsedYaml); updateValuesFromConfig(parsedYaml);
} catch (err: any) { } catch (err: any) {
showNotice('error', t("Invalid YAML format")); showNotice("error", t("Invalid YAML format"));
} }
}; };
@@ -505,7 +510,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
if (Object.keys(dnsConfig).length > 0) { if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig; config.dns = dnsConfig;
} }
const hosts = parseHosts(values.hosts); const hosts = parseHosts(values.hosts);
if (Object.keys(hosts).length > 0) { if (Object.keys(hosts).length > 0) {
config.hosts = hosts; config.hosts = hosts;
@@ -521,30 +526,41 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置 // 保存配置
await invoke("save_dns_config", { dnsConfig: config }); await invoke("save_dns_config", { dnsConfig: config });
// 验证配置 // 验证配置
const [isValid, errorMsg] = await invoke<[boolean, string]>("validate_dns_config", {}); const [isValid, errorMsg] = await invoke<[boolean, string]>(
"validate_dns_config",
{},
);
if (!isValid) { if (!isValid) {
let cleanErrorMsg = errorMsg; let cleanErrorMsg = errorMsg;
// 提取关键错误信息 // 提取关键错误信息
if (errorMsg.includes("level=error")) { if (errorMsg.includes("level=error")) {
const errorLines = errorMsg.split('\n').filter(line => const errorLines = errorMsg
line.includes("level=error") || .split("\n")
line.includes("level=fatal") || .filter(
line.includes("failed") (line) =>
); line.includes("level=error") ||
line.includes("level=fatal") ||
line.includes("failed"),
);
if (errorLines.length > 0) { if (errorLines.length > 0) {
cleanErrorMsg = errorLines.map(line => { cleanErrorMsg = errorLines
const msgMatch = line.match(/msg="([^"]+)"/); .map((line) => {
return msgMatch ? msgMatch[1] : line; const msgMatch = line.match(/msg="([^"]+)"/);
}).join(", "); return msgMatch ? msgMatch[1] : line;
})
.join(", ");
} }
} }
showNotice('error', t("DNS configuration error") + ": " + cleanErrorMsg); showNotice(
"error",
t("DNS configuration error") + ": " + cleanErrorMsg,
);
return; return;
} }
@@ -555,9 +571,9 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
} }
setOpen(false); setOpen(false);
showNotice('success', t("DNS settings saved")); showNotice("success", t("DNS settings saved"));
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -80,7 +80,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
}); });
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
+26 -1
View File
@@ -12,6 +12,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { BaseDialog, DialogRef, Switch } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { GuardState } from "./guard-state"; import { GuardState } from "./guard-state";
import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { convertFileSrc } from "@tauri-apps/api/core"; import { convertFileSrc } from "@tauri-apps/api/core";
@@ -88,7 +89,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const onSwitchFormat = (_e: any, value: boolean) => value; const onSwitchFormat = (_e: any, value: boolean) => value;
const onError = (err: any) => { const onError = (err: any) => {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
}; };
const onChangeData = (patch: Partial<IVergeConfig>) => { const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false); mutateVerge({ ...verge, ...patch }, false);
@@ -147,6 +148,30 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
</GuardState> </GuardState>
</Item> </Item>
<Item>
<ListItemText
primary={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<span>{t("Hover Jump Navigator")}</span>
<TooltipIcon
title={t("Hover Jump Navigator Info")}
sx={{ opacity: "0.7" }}
/>
</Box>
}
/>
<GuardState
value={verge?.enable_hover_jump_navigator ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_hover_jump_navigator: e })}
onGuard={(e) => patchVerge({ enable_hover_jump_navigator: e })}
>
<Switch edge="end" />
</GuardState>
</Item>
<Item> <Item>
<ListItemText primary={t("Nav Icon")} /> <ListItemText primary={t("Nav Icon")} />
<GuardState <GuardState

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