Compare commits

..
146 changed files with 6845 additions and 8278 deletions
-58
View File
@@ -1,58 +0,0 @@
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
+6 -49
View File
@@ -1,24 +1,10 @@
name: Alpha Build
on:
# 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境
# 所以不再使用 workflow_dispatch 触发
# 应当通过 git tag 来触发构建
# TODO 手动控制版本号
workflow_dispatch:
# inputs:
# tag_name:
# description: "Alpha tag name (e.g. v1.2.3-alpha.1)"
# required: true
# type: string
# push:
# # 应当限制在 dev 分支上触发发布。
# branches:
# - dev
# # 应当限制 v*.*.*-alpha* 的 tag 来触发发布。
# tags:
# - "v*.*.*-alpha*"
push:
tags:
- "v*.*.*-alpha*"
permissions: write-all
env:
TAG_NAME: alpha
@@ -29,37 +15,8 @@ concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
jobs:
check_alpha_tag:
name: Check Alpha Tag package.json Version Consistency
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check tag and package.json version
id: check_tag
run: |
TAG_REF="${GITHUB_REF##*/}"
echo "Current tag: $TAG_REF"
if [[ ! "$TAG_REF" =~ -alpha ]]; then
echo "Current tag is not an alpha tag."
exit 1
fi
PKG_VERSION=$(jq -r .version package.json)
echo "package.json version: $PKG_VERSION"
if [[ "$PKG_VERSION" != *alpha* ]]; then
echo "package.json version is not an alpha version."
exit 1
fi
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
exit 1
fi
echo "Alpha tag and package.json version are consistent."
delete_old_assets:
name: Delete Old Alpha Release Assets and Tags
needs: check_alpha_tag
runs-on: ubuntu-latest
steps:
- name: Delete Old Alpha Tags Except Latest
@@ -308,7 +265,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
# - name: Release ${{ env.TAG_CHANNEL }} Version
# run: pnpm release-version ${{ env.TAG_NAME }}
@@ -378,7 +335,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
# - name: Release ${{ env.TAG_CHANNEL }} Version
# run: pnpm release-version ${{ env.TAG_NAME }}
@@ -507,7 +464,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
# - name: Release ${{ env.TAG_CHANNEL }} Version
# run: pnpm release-version ${{ env.TAG_NAME }}
+3 -3
View File
@@ -204,7 +204,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
@@ -274,7 +274,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
@@ -403,7 +403,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
-64
View File
@@ -1,64 +0,0 @@
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
@@ -1,65 +0,0 @@
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
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
-51
View File
@@ -1,51 +0,0 @@
# 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
+4 -38
View File
@@ -1,16 +1,7 @@
name: Release Build
on:
# ! 为了避免重复发布版本,应当通过独特 git tag 触发。
# ! 不再使用 workflow_dispatch 触发。
# workflow_dispatch:
push:
# 应当限制在 main 分支上触发发布。
branches:
- main
# 应当限制 v*.*.* 的 tag 触发发布。
tags:
- "v*.*.*"
workflow_dispatch:
permissions: write-all
env:
CARGO_INCREMENTAL: 0
@@ -21,28 +12,7 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
check_tag_version:
name: Check Release Tag and package.json Version Consistency
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check tag and package.json version
run: |
TAG_REF="${GITHUB_REF##*/}"
echo "Current tag: $TAG_REF"
PKG_VERSION=$(jq -r .version package.json)
echo "package.json version: $PKG_VERSION"
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
exit 1
fi
echo "Tag and package.json version are consistent."
release:
name: Release Build
needs: check_tag_version
strategy:
fail-fast: false
matrix:
@@ -95,7 +65,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
@@ -118,7 +88,6 @@ jobs:
args: --target ${{ matrix.target }}
release-for-linux-arm:
name: Release Build for Linux ARM
strategy:
fail-fast: false
matrix:
@@ -159,7 +128,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
- name: "Setup for linux"
run: |-
@@ -240,7 +209,6 @@ jobs:
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
release-for-fixed-webview2:
name: Release Build for Fixed WebView2
strategy:
fail-fast: false
matrix:
@@ -279,7 +247,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm check ${{ matrix.target }}
- name: Download WebView2 Runtime
run: |
@@ -335,7 +303,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update:
name: Release Update
runs-on: ubuntu-latest
needs: [release, release-for-linux-arm]
steps:
@@ -386,7 +353,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget:
name: Submit to Winget
runs-on: ubuntu-latest
needs: [release-update]
steps:
+8 -16
View File
@@ -2,25 +2,17 @@
#pnpm pretty-quick --staged
if git diff --cached --name-only | grep -q '^src/'; then
pnpm format:check
if [ $? -ne 0 ]; then
echo "Code format check failed in src/. Please fix formatting issues."
exit 1
fi
fi
# 运行 clippy fmt
cd src-tauri
cargo fmt
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 ..
if [ $? -ne 0 ]; then
echo "rustfmt failed to format the code. Please fix the issues and try again."
exit 1
fi
cd ..
#git add .
git add .
# 允许提交
exit 0
+9 -7
View File
@@ -1,11 +1,13 @@
#!/bin/bash
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cargo clippy --manifest-path ./src-tauri/Cargo.toml
if [ $? -ne 0 ]; then
echo "Clippy found issues in src-tauri. Please fix them before pushing."
exit 1
fi
fi
# 运行 clippy
# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
# 如果 clippy 失败,阻止 push
# if [ $? -ne 0 ]; then
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
# exit 1
# fi
# 允许 push
exit 0
-8
View File
@@ -1,8 +0,0 @@
# README.md
# UPDATELOG.md
# CONTRIBUTING.md
pnpm-lock.yaml
src-tauri/target/
src-tauri/gen/
-6
View File
@@ -1,6 +0,0 @@
{
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"experimentalOperatorPosition": "start"
}
+10 -18
View File
@@ -33,15 +33,12 @@ npm install pnpm -g
```
### Install Dependencies
Install node packages
```shell
pnpm install
```
Install apt packages ONLY for Ubuntu
```shell
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
```
@@ -52,9 +49,9 @@ You have two options for downloading the clash binary:
- Automatically download it via the provided script:
```shell
pnpm run prepare
pnpm run check
# Use '--force' to force update to the latest version
# pnpm run prepare --force
# pnpm run check --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).
@@ -108,25 +105,20 @@ pnpm portable
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
1. Code quailty checks
1. Code style formatting
```bash
# For rust backend
$ clash-verge-rev: pnpm clippy
# For frontend (not yet).
```
2. Code style formatting
```bash
# For rust backend
$ clash-verge-rev: cd src-tauri
$ clash-verge-rev/src-tauri: cargo fmt
# For frontend
$ clash-verge-rev: pnpm format:check
$ clash-verge-rev: pnpm format
```
2. Code quailty checks
```bash
$ clash-verge-rev: pnpm clippy
```
Once you have made your changes:
1. Fork the repository.
+3 -12
View File
@@ -18,18 +18,10 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
## Install
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
Go to the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
Go to the [release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
#### 我应当怎样选择发行版
| 版本 | 特征 | 链接 |
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
| 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/) 查看
### 安装说明和常见问题,请到[文档页](https://clash-verge-rev.github.io/)查看:[Doc](https://clash-verge-rev.github.io/)
---
@@ -49,12 +41,11 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
- 解锁流媒体及 ChatGPT
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
🧩 YXVM 独立服务器优势:
- 🌎 优质网络,回程优化,下载快到飞起
- 🔧 物理机独享资源,非VPS可比,性能拉满
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
+129 -185
View File
@@ -1,181 +1,143 @@
## 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 发行版兼容,后续将逐步适配和优化。
- macOS:
- MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
- 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
- 彩色 Tray 图标颜色偏淡;
#### 🚀 优化改进
- 系统代理 Bypass 设置
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题
- 切换到规则页面时自动刷新规则数据
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新
- 编辑非激活订阅的时候不在触发当前订阅配置重载
- 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程
- 优化系统代理设置更新逻辑
- 重构前端通知系统分离通知线程防止前端卡死
- 优化网络请求和错误处理
- 重构通知系统
- 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题
- MacOS 下默认关闭托盘速率显示
- 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验
- 优化了其他语言的翻译问题
- Mihomo 内核默认日志等级为 warn
- Clash Verge Rev 应用默认日志等级为 warn
- 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题
- 对轮询检测机制进行了优化,引入洗牌算法来增强随机性
- 对获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息
- 优化窗口状态初始化逻辑和添加缺失的权限设置
- 异步化配置:优化端口查找和配置保存逻辑
- 重构事件通知机制到独立线程,避免前端卡死
- 优化端口设置,每个端口可随机设置端口号
- 优化了保存机制,使用平滑函数,防止客户端卡死
- 优化端口设置退出和保存机制
- 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖
- 修改 端口检测范围 (1111-65536)
- 配置文件缺失 secret 字段时自动填充默认值 set-your-secret
- 优化异步处理,防止部分组件 UI 阻塞
- 关闭 DNS 启用
- 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204
- 已确认窗口状态管理器存在上游缺陷,已暂时移除窗口大小与位置记忆功能。
---
### 🐞 修复问题
- 修复首页“代理模式”快速切换导致的卡死问题
- 修复 MacOS 快捷键关闭窗口无法启用自动轻量模式
- 修复静默启动异常窗口的创建与关闭流程
- 修复 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 跨平台备份恢复限制
---
#### 🗑️ 移除内容
- 窗口状态管理器
- Webdav 跨平台备份恢复限制
## v2.2.3
#### 已知问题
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
### 2.2.3 相对于 2.2.2
#### 修复了:
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- “开机自启”和“DNS覆写”开关跳动问题
- 自定义托盘图标未能应用更改
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
- MacOS 托盘速率显示不全
- Linux 在系统服务模式下无法拉起 Mihomo 内核
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
- 相同节点名称可能导致的页面渲染出错
- URL Schemes被截断的问题
- 首页流量统计卡更好的时间戳范围
- 静默启动无法触发自动轻量化计时器
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- “开机自启”和“DNS覆写”开关跳动问题
- 自定义托盘图标未能应用更改
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
- MacOS 托盘速率显示不全
- Linux 在系统服务模式下无法拉起 Mihomo 内核
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
- 相同节点名称可能导致的页面渲染出错
- URL Schemes被截断的问题
- 首页流量统计卡更好的时间戳范围
- 静默启动无法触发自动轻量化计时器
#### 新增了:
- Mihomo(Meta)内核升级至 1.19.4
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
- 代理组显示节点数量
- 统一运行模式检测,支持管理员模式下开启TUN模式
- 托盘切换代理模式会根据设置自动断开之前连接
- 如订阅获取失败回退使用Clash内核代理再次尝试
- Mihomo(Meta)内核升级至 1.19.4
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
- 代理组显示节点数量
- 统一运行模式检测,支持管理员模式下开启TUN模式
- 托盘切换代理模式会根据设置自动断开之前连接
- 如订阅获取失败回退使用Clash内核代理再次尝试
#### 移除了:
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
#### 优化了:
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
- 优化首页流量图表代码,调整图表文字边距
- MacOS 托盘速率更好的显示样式和更新逻辑
- 首页仅在有流量图表时显示流量图表区域
- 更新DNS默认覆写配置
- 移除测试目录,简化资源初始化逻辑
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
- 优化首页流量图表代码,调整图表文字边距
- MacOS 托盘速率更好的显示样式和更新逻辑
- 首页仅在有流量图表时显示流量图表区域
- 更新DNS默认覆写配置
- 移除测试目录,简化资源初始化逻辑
## v2.2.2
@@ -186,29 +148,23 @@
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
#### 已知问题
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
### 2.2.2 相对于 2.2.1(已下架不再提供)
#### 修复了:
- 弹黑框的问题(原因是服务崩溃触发重装机制)
- MacOS进入轻量模式以后隐藏Dock图标
- 增加轻量模式缺失的tray翻译
- Linux下的窗口边框被削掉的问题
- 弹黑框的问题(原因是服务崩溃触发重装机制)
- MacOS进入轻量模式以后隐藏Dock图标
- 增加轻量模式缺失的tray翻译
- Linux下的窗口边框被削掉的问题
#### 新增了:
- 加强服务检测和重装逻辑
- 增强内核与服务保活机制
- 增加服务模式下的僵尸进程清理机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
- 加强服务检测和重装逻辑
- 增强内核与服务保活机制
- 增加服务模式下的僵尸进程清理机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
### 2.2.1 相对于 2.2.0(已下架不再提供)
#### 修复了:
1. **首页**
- 修复 Direct 模式首页无法渲染
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
@@ -225,7 +181,6 @@
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
#### 新增了:
1. **首页**
- 首页文本过长自动截断
2. **轻量模式**
@@ -242,9 +197,7 @@
## 2.2.0(已下架不再提供)
#### 新增功能
1. **首页**
- 新增首页功能,默认启动页面改为首页。
- 首页流量图卡片显示上传/下载名称。
- 首页支持轻量模式切换。
@@ -252,21 +205,17 @@
- 限制首页配置文件卡片URL长度。
2. **DNS 设置与覆写**
- 新增 DNS 覆写功能。
- 默认启用 DNS 覆写。
3. **解锁测试**
- 新增解锁测试页面。
4. **轻量模式**
- 新增轻量模式及设置。
- 添加自动轻量模式定时器。
5. **系统支持**
- Mihomo(meta)内核升级 1.19.3
- macOS 支持 CMD+W 关闭窗口。
- 新增 macOS 应用菜单。
@@ -279,9 +228,7 @@
- 新增代理命令。
#### 修复
1. **系统**
- 修复 Windows 热键崩溃。
- 修复 macOS 无框标题。
- 修复 macOS 静默启动崩溃。
@@ -294,9 +241,7 @@
- 修复构建失败问题。
#### 优化
1. **性能**
- 重构后端,巨幅性能优化。
- 优化首页组件性能。
- 优化流量图表资源使用。
@@ -309,7 +254,6 @@
- 优化修改verge配置性能。
2. **重构**
- 重构后端,巨幅性能优化。
- 优化定时器管理。
- 重构 MihomoManager 处理流量。
-4
View File
@@ -1,4 +0,0 @@
files:
- source: /src/locales/en.json
translation: /src/locales
multilingual: 1
+37 -38
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.3.0",
"version": "2.3.0-alpha.0",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
@@ -11,18 +11,17 @@
"web:dev": "vite",
"web:build": "tsc --noEmit && vite build",
"web:serve": "vite preview",
"prepare": "node scripts/prepare.mjs",
"check": "node scripts/check.mjs",
"updater": "node scripts/updater.mjs",
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
"portable": "node scripts/portable.mjs",
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
"release-version": "node scripts/release-version.mjs",
"publish-version": "node scripts/publish-version.mjs",
"release-alpha-version": "node scripts/release-alpha_version.mjs",
"prepare": "husky",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"format": "prettier --write .",
"format:check": "prettier --check ."
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -31,15 +30,15 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.1.1",
"@mui/lab": "7.0.0-beta.13",
"@mui/material": "^7.1.1",
"@mui/x-data-grid": "^8.5.1",
"@mui/icons-material": "^7.0.2",
"@mui/lab": "7.0.0-beta.12",
"@mui/material": "^7.0.2",
"@mui/x-data-grid": "^8.2.0",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
"@tauri-apps/plugin-notification": "^2.2.2",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "2.2.1",
@@ -47,61 +46,61 @@
"@tauri-apps/plugin-window-state": "^2.2.2",
"@types/d3-shape": "^3.1.7",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.8.5",
"axios": "^1.9.0",
"ahooks": "^3.8.4",
"axios": "^1.8.3",
"chart.js": "^4.4.9",
"cli-color": "^2.0.4",
"d3-shape": "^3.2.0",
"dayjs": "1.11.13",
"foxact": "^0.2.49",
"glob": "^11.0.2",
"i18next": "^25.2.1",
"foxact": "^0.2.44",
"glob": "^11.0.1",
"i18next": "^25.0.2",
"js-base64": "^3.7.7",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.4.0",
"monaco-yaml": "^5.3.1",
"nanoid": "^5.1.5",
"peggy": "^5.0.3",
"peggy": "^5.0.0",
"react": "19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "19.1.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.57.0",
"react-i18next": "15.5.2",
"react-hook-form": "^7.54.2",
"react-i18next": "15.5.1",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.58.0",
"react-router-dom": "7.6.2",
"react-virtuoso": "^4.12.8",
"react-router-dom": "7.6.0",
"react-virtuoso": "^4.12.7",
"sockette": "^2.0.6",
"swr": "^2.3.3",
"tar": "^7.4.3",
"types-pac": "^1.0.3",
"zustand": "^5.0.5"
"zustand": "^5.0.3"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@tauri-apps/cli": "2.5.0",
"@actions/github": "^6.0.0",
"@tauri-apps/cli": "2.2.7",
"@types/js-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@vitejs/plugin-legacy": "^6.1.1",
"@vitejs/plugin-react": "4.5.2",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.5",
"@vitejs/plugin-legacy": "^6.0.2",
"@vitejs/plugin-react": "4.4.1",
"adm-zip": "^0.5.16",
"commander": "^14.0.0",
"cross-env": "^7.0.3",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"meta-json-schema": "^1.19.10",
"meta-json-schema": "^1.19.3",
"node-fetch": "^3.3.2",
"prettier": "^3.5.3",
"pretty-quick": "^4.2.2",
"sass": "^1.89.2",
"terser": "^5.42.0",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"pretty-quick": "^4.1.1",
"sass": "^1.86.0",
"terser": "^5.39.0",
"typescript": "^5.8.2",
"vite": "^6.2.2",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.0"
},
@@ -113,4 +112,4 @@
},
"type": "module",
"packageManager": "pnpm@9.13.2"
}
}
+675 -648
View File
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
{
"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"]
}
+27 -31
View File
@@ -1,21 +1,21 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const LOCALES_DIR = path.resolve(__dirname, '../src/locales');
const SRC_DIRS = [
path.resolve(__dirname, "../src"),
path.resolve(__dirname, "../src-tauri"),
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../src-tauri')
];
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
const exts = ['.js', '.ts', '.tsx', '.jsx', '.vue', '.rs'];
// 递归获取所有文件
function getAllFiles(dir, exts) {
let files = [];
fs.readdirSync(dir).forEach((file) => {
fs.readdirSync(dir).forEach(file => {
const full = path.join(dir, file);
if (fs.statSync(full).isDirectory()) {
files = files.concat(getAllFiles(full, exts));
@@ -28,21 +28,21 @@ function getAllFiles(dir, exts) {
// 读取所有源码内容为一个大字符串
function getAllSourceContent() {
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
const files = SRC_DIRS.flatMap(dir => getAllFiles(dir, exts));
return files.map(f => fs.readFileSync(f, 'utf8')).join('\n');
}
// 白名单 key,不检查这些 key 是否被使用
const WHITELIST_KEYS = [
"theme.light",
"theme.dark",
"theme.system",
"Already Using Latest Core Version",
'theme.light',
'theme.dark',
'theme.system',
"Already Using Latest Core Version"
];
// 主流程
function processI18nFile(i18nPath, lang, allSource) {
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
const i18n = JSON.parse(fs.readFileSync(i18nPath, 'utf8'));
const keys = Object.keys(i18n);
const used = {};
@@ -50,7 +50,7 @@ function processI18nFile(i18nPath, lang, allSource) {
let checked = 0;
const total = keys.length;
keys.forEach((key) => {
keys.forEach(key => {
if (WHITELIST_KEYS.includes(key)) {
used[key] = i18n[key];
} else {
@@ -65,10 +65,8 @@ function processI18nFile(i18nPath, lang, allSource) {
checked++;
if (checked % 20 === 0 || checked === total) {
const percent = ((checked / total) * 100).toFixed(1);
process.stdout.write(
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
);
if (checked === total) process.stdout.write("\n");
process.stdout.write(`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`);
if (checked === total) process.stdout.write('\n');
}
});
@@ -76,27 +74,25 @@ function processI18nFile(i18nPath, lang, allSource) {
console.log(`\n[${lang}] Unused keys:`, unused);
// 备份原文件
const oldPath = i18nPath + ".old";
const oldPath = i18nPath + '.old';
fs.renameSync(i18nPath, oldPath);
// 写入精简后的 i18n 文件(保留原文件名)
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
console.log(
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
);
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), 'utf8');
console.log(`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`);
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
}
function main() {
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
const files = fs
.readdirSync(LOCALES_DIR)
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
const files = fs.readdirSync(LOCALES_DIR).filter(f =>
/^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith('.old')
);
const allSource = getAllSourceContent();
files.forEach((file) => {
const lang = path.basename(file, ".json");
files.forEach(file => {
const lang = path.basename(file, '.json');
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
});
}
main();
main();
-66
View File
@@ -1,66 +0,0 @@
// 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();
+23 -63
View File
@@ -1,3 +1,5 @@
/**
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
*
@@ -28,35 +30,19 @@
import fs from "fs/promises";
import path from "path";
import { program } from "commander";
import { execSync } from "child_process";
/**
* 获取当前 git 短 commit hash
* 生成短时间戳(格式:YYMMDDHHMM)
* @returns {string}
*/
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) {
function generateShortTimestamp() {
const now = new Date();
const year = String(now.getFullYear()).slice(-2);
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
if (withCommit) {
const gitShort = getGitShortCommit();
return `${month}${day}.${gitShort}`;
}
return `${month}${day}`;
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
return `${year}${month}${day}${hours}${minutes}`;
}
/**
@@ -65,9 +51,7 @@ function generateShortTimestamp(withCommit = false) {
* @returns {boolean}
*/
function isValidVersion(version) {
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
version,
);
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(version);
}
/**
@@ -85,8 +69,8 @@ function normalizeVersion(version) {
* @returns {string}
*/
function getBaseVersion(version) {
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, '');
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, '');
return base;
}
@@ -101,21 +85,10 @@ async function updatePackageVersion(newVersion) {
const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data);
console.log(
"[INFO]: Current package.json version is: ",
packageJson.version,
);
packageJson.version = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
"utf8",
);
console.log(
`[INFO]: package.json version updated to: ${packageJson.version}`,
);
console.log("[INFO]: Current package.json version is: ", packageJson.version);
packageJson.version = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf8");
console.log(`[INFO]: package.json version updated to: ${packageJson.version}`);
} catch (error) {
console.error("Error updating package.json version:", error);
throw error;
@@ -132,17 +105,12 @@ async function updateCargoVersion(newVersion) {
try {
const data = await fs.readFile(cargoTomlPath, "utf8");
const lines = data.split("\n");
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
const baseVersion = getBaseVersion(versionWithoutV);
const updatedLines = lines.map((line) => {
if (line.trim().startsWith("version =")) {
return line.replace(
/version\s*=\s*"[^"]+"/,
`version = "${baseVersion}"`,
);
return line.replace(/version\s*=\s*"[^"]+"/, `version = "${baseVersion}"`);
}
return line;
});
@@ -165,21 +133,12 @@ async function updateTauriConfigVersion(newVersion) {
try {
const data = await fs.readFile(tauriConfigPath, "utf8");
const tauriConfig = JSON.parse(data);
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
const baseVersion = getBaseVersion(versionWithoutV);
console.log(
"[INFO]: Current tauri.conf.json version is: ",
tauriConfig.version,
);
console.log("[INFO]: Current tauri.conf.json version is: ", tauriConfig.version);
tauriConfig.version = baseVersion;
await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
"utf8",
);
await fs.writeFile(tauriConfigPath, JSON.stringify(tauriConfig, null, 2), "utf8");
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
} catch (error) {
console.error("Error updating tauri.conf.json version:", error);
@@ -221,8 +180,8 @@ async function main(versionArg) {
const baseVersion = getBaseVersion(currentVersion);
if (versionArg.toLowerCase() === "autobuild") {
// 格式: 2.3.0+autobuild.250613.cc39b27
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
const timestamp = generateShortTimestamp();
newVersion = `${baseVersion}+autobuild.${timestamp}`;
} else {
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
}
@@ -251,3 +210,4 @@ program
.argument("<version>", "version tag or full version")
.action(main)
.parse(process.argv);
+199 -154
View File
@@ -1057,7 +1057,7 @@ dependencies = [
"dunce",
"futures",
"gethostname 1.0.2",
"getrandom 0.3.3",
"getrandom 0.3.2",
"hex",
"hmac",
"image",
@@ -1106,7 +1106,7 @@ dependencies = [
"warp",
"winapi",
"winreg 0.55.0",
"zip 4.0.0",
"zip",
]
[[package]]
@@ -1337,6 +1337,21 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
@@ -1651,7 +1666,7 @@ dependencies = [
"tonic-health",
"tonic-web",
"tower 0.4.13",
"tower-http 0.4.4",
"tower-http",
"tower-layer",
"tracing",
"tracing-core",
@@ -2085,12 +2100,11 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.1.1"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
dependencies = [
"crc32fast",
"libz-rs-sys",
"miniz_oxide",
]
@@ -2448,9 +2462,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.3.3"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"js-sys",
@@ -2573,9 +2587,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "global-hotkey"
version = "0.7.0"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
checksum = "41fbb3a4e56c901ee66c190fdb3fa08344e6d09593cc6c61f8eb9add7144b271"
dependencies = [
"crossbeam-channel",
"keyboard-types",
@@ -2585,8 +2599,7 @@ dependencies = [
"serde",
"thiserror 2.0.12",
"windows-sys 0.59.0",
"x11rb",
"xkeysym",
"x11-dl",
]
[[package]]
@@ -2926,7 +2939,7 @@ dependencies = [
"httpdate",
"itoa 1.0.15",
"pin-project-lite",
"socket2 0.5.10",
"socket2 0.5.8",
"tokio",
"tower-service",
"tracing",
@@ -2968,7 +2981,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots 0.26.8",
"webpki-roots",
]
[[package]]
@@ -3001,28 +3014,21 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.14"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"hyper 1.6.0",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"system-configuration",
"socket2 0.5.8",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@@ -3374,16 +3380,6 @@ dependencies = [
"regex",
]
[[package]]
name = "iri-string"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "is-docker"
version = "0.2.0"
@@ -3611,26 +3607,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "liblzma"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66352d7a8ac12d4877b6e6ea5a9b7650ee094257dc40889955bea5bc5b08c1d0"
dependencies = [
"liblzma-sys",
]
[[package]]
name = "liblzma-sys"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5839bad90c3cc2e0b8c4ed8296b80e86040240f81d46b9c0e9bc8dd51ddd3af1"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "libm"
version = "0.2.11"
@@ -3648,15 +3624,6 @@ dependencies = [
"redox_syscall",
]
[[package]]
name = "libz-rs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
dependencies = [
"zlib-rs",
]
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
@@ -3701,9 +3668,9 @@ dependencies = [
[[package]]
name = "lock_api"
version = "0.4.13"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
@@ -3776,6 +3743,27 @@ dependencies = [
"hashbrown 0.15.2",
]
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc",
]
[[package]]
name = "lzma-sys"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "mac"
version = "0.1.1"
@@ -4789,9 +4777,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.4"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -4799,9 +4787,9 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.11"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
@@ -5393,7 +5381,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.5.10",
"socket2 0.5.8",
"thiserror 2.0.12",
"tokio",
"tracing",
@@ -5407,7 +5395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
dependencies = [
"bytes",
"getrandom 0.3.3",
"getrandom 0.3.2",
"rand 0.9.0",
"ring",
"rustc-hash",
@@ -5429,7 +5417,7 @@ dependencies = [
"cfg_aliases 0.2.1",
"libc",
"once_cell",
"socket2 0.5.10",
"socket2 0.5.8",
"tracing",
"windows-sys 0.59.0",
]
@@ -5539,7 +5527,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
"getrandom 0.3.2",
]
[[package]]
@@ -5739,9 +5727,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.12.20"
version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -5758,32 +5746,36 @@ dependencies = [
"hyper-rustls",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 1.0.2",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-util",
"tower 0.5.2",
"tower-http 0.6.6",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots 1.0.0",
"webpki-roots",
"windows-registry 0.4.0",
]
[[package]]
@@ -5975,6 +5967,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.11.0"
@@ -6520,9 +6521,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.5.10"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [
"libc",
"windows-sys 0.52.0",
@@ -6707,9 +6708,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.35.2"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422"
dependencies = [
"libc",
"memchr",
@@ -7005,9 +7006,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-deep-link"
version = "2.3.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4976ac728ebc0487515aa956cfdf200abcc52b784e441493fc544bc6ce369c8"
checksum = "dba4412f30eaff6f5d210e20383c2d6835593977402092e95b72497a4f8632fa"
dependencies = [
"dunce",
"rust-ini",
@@ -7019,7 +7020,7 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"url",
"windows-registry",
"windows-registry 0.5.1",
"windows-result 0.3.2",
]
@@ -7052,9 +7053,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.2.2"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8"
checksum = "bcaf6e5d6062423a0f711a23c2a573ccba222b6a16a9322d8499928f27e41376"
dependencies = [
"log",
"raw-window-handle",
@@ -7070,9 +7071,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.3.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da"
checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682"
dependencies = [
"anyhow",
"dunce",
@@ -7088,13 +7089,14 @@ dependencies = [
"thiserror 2.0.12",
"toml",
"url",
"uuid",
]
[[package]]
name = "tauri-plugin-global-shortcut"
version = "2.2.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31919f3c07bcb585afef217c0c33cde80da9ebccf5b8e2c90e0e0a535b14ab47"
checksum = "00f646a09511e8d283267dcdaa08c2ef27c4116bf271d9114849d9ca215606c3"
dependencies = [
"global-hotkey",
"log",
@@ -7165,7 +7167,7 @@ dependencies = [
"tokio",
"url",
"windows-sys 0.59.0",
"zip 2.6.1",
"zip",
]
[[package]]
@@ -7283,12 +7285,12 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.20.0"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
dependencies = [
"fastrand 2.3.0",
"getrandom 0.3.3",
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.3",
"windows-sys 0.59.0",
@@ -7512,9 +7514,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [
"backtrace",
"bytes",
@@ -7523,7 +7525,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.5.10",
"socket2 0.5.8",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
@@ -7728,7 +7730,7 @@ dependencies = [
"pin-project",
"tokio-stream",
"tonic",
"tower-http 0.4.4",
"tower-http",
"tower-layer",
"tower-service",
"tracing",
@@ -7787,24 +7789,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags 2.9.0",
"bytes",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"iri-string",
"pin-project-lite",
"tower 0.5.2",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@@ -8160,7 +8144,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.3",
"getrandom 0.3.2",
"serde",
]
@@ -8530,15 +8514,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.37.0"
@@ -8786,6 +8761,17 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result 0.3.2",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
[[package]]
name = "windows-registry"
version = "0.5.1"
@@ -8825,6 +8811,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
@@ -8909,13 +8904,29 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 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]]
name = "windows-version"
version = "0.1.4"
@@ -8943,6 +8954,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -8961,6 +8978,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -8979,12 +9002,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -9003,6 +9038,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -9021,6 +9062,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -9039,6 +9086,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -9057,6 +9110,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "winnow"
version = "0.5.40"
@@ -9254,18 +9313,21 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "xkeysym"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "xml-rs"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4"
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [
"lzma-sys",
]
[[package]]
name = "yoke"
version = "0.7.5"
@@ -9455,46 +9517,29 @@ name = "zip"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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 = [
"aes",
"arbitrary",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"deflate64",
"flate2",
"getrandom 0.3.3",
"getrandom 0.3.2",
"hmac",
"indexmap 2.8.0",
"liblzma",
"lzma-rs",
"memchr",
"pbkdf2",
"sha1",
"time",
"xz2",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zlib-rs"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
[[package]]
name = "zopfli"
version = "0.8.1"
+18 -18
View File
@@ -16,33 +16,33 @@ identifier = "io.github.clash-verge-rev.clash-verge-rev"
tauri-build = { version = "2.2.0", features = [] }
[dependencies]
warp = "0.3.7"
warp = "0.3"
anyhow = "1.0.98"
dirs = "6.0"
open = "5.3.2"
log = "0.4.27"
dunce = "1.0.5"
log4rs = "1.3.0"
open = "5.3"
log = "0.4"
dunce = "1.0"
log4rs = "1"
nanoid = "0.4"
chrono = "0.4.41"
sysinfo = "0.35.2"
sysinfo = "0.35.0"
boa_engine = "0.20.0"
serde_json = "1.0.140"
serde_yaml = "0.9.34-deprecated"
serde_yaml = "0.9.34"
once_cell = "1.21.3"
lazy_static = "1.5.0"
port_scanner = "0.1.5"
delay_timer = "0.11.6"
parking_lot = "0.12.4"
parking_lot = "0.12.3"
percent-encoding = "2.3.1"
tokio = { version = "1.45.1", features = [
tokio = { version = "1.44.2", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
serde = { version = "1.0.219", features = ["derive"] }
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] }
reqwest = { version = "0.12.15", features = ["json", "rustls-tls", "cookies"] }
regex = "1.11.1"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
image = "0.25.6"
@@ -56,18 +56,18 @@ tauri = { version = "2.5.1", features = [
] }
network-interface = { version = "2.0.1", features = ["serde"] }
tauri-plugin-shell = "2.2.1"
tauri-plugin-dialog = "2.2.2"
tauri-plugin-fs = "2.3.0"
tauri-plugin-dialog = "2.2.1"
tauri-plugin-fs = "2.2.1"
tauri-plugin-process = "2.2.1"
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-deep-link = "2.3.0"
tauri-plugin-deep-link = "2.2.1"
tauri-plugin-devtools = "2.0.0"
tauri-plugin-window-state = "2.2.2"
zip = "4.0.0"
zip = "2.6.1"
reqwest_dav = "0.2.1"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.3.3"
getrandom = "0.3.2"
tokio-tungstenite = "0.26.2"
futures = "0.3.31"
sys-locale = "0.3.2"
@@ -85,7 +85,7 @@ hex = "0.4.3"
runas = "=1.2.0"
deelevate = "0.2.0"
winreg = "0.55.0"
winapi = { version = "0.3.9", features = [
winapi = { version = "0.3", features = [
"winbase",
"fileapi",
"winnt",
@@ -100,7 +100,7 @@ users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-global-shortcut = "2.2.1"
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-updater = "2.7.1"
[features]
@@ -136,7 +136,7 @@ name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[dev-dependencies]
tempfile = "3.20.0"
tempfile = "3.19.1"
[workspace]
members = ["src_crates/crate_mihomo_api"]
@@ -1,9 +0,0 @@
{
"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,3 +11,4 @@ merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
imports_granularity = "Crate"
+2 -6
View File
@@ -22,7 +22,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
.await;
match profiles_result {
Ok(Ok(profiles)) => Ok(*profiles),
Ok(Ok(profiles)) => Ok(profiles),
Ok(Err(join_err)) => {
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err);
Ok(IProfiles {
@@ -41,7 +41,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await {
Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用latest()成功获取配置");
Ok(*profiles)
Ok(profiles)
}
Err(_) => {
logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置");
@@ -93,10 +93,6 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
// 删除后自动清理冗余文件
let _ = Config::profiles().latest().auto_cleanup();
if should_update {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
+50 -56
View File
@@ -1,77 +1,71 @@
use super::CmdResult;
use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState};
use crate::module::mihomo::MihomoManager;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::{
sync::Mutex,
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
use tauri::Manager;
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3);
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
static LAST_REFRESH_TIME: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None));
static IS_REFRESHING: AtomicBool = AtomicBool::new(false);
const REFRESH_INTERVAL: Duration = Duration::from_secs(5);
#[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global();
let app_handle = handle::Handle::global().app_handle().unwrap();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
let should_refresh = {
let mut state = cmd_proxy_state.lock().unwrap();
let now = Instant::now();
if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL {
state.need_refresh = true;
state.last_refresh_time = now;
}
state.need_refresh
};
if should_refresh {
let proxies = manager.get_refresh_proxies().await?;
{
let mut state = cmd_proxy_state.lock().unwrap();
state.proxies = Box::new(proxies);
state.need_refresh = false;
}
log::debug!(target: "app", "proxies刷新成功");
}
let proxies = {
let state = cmd_proxy_state.lock().unwrap();
state.proxies.clone()
};
Ok(*proxies)
manager
.refresh_proxies()
.await
.map(|_| manager.get_proxies())
.or_else(|_| Ok(manager.get_proxies()))
}
#[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
let manager = MihomoManager::global();
let cached_data = manager.get_providers_proxies();
let should_refresh = {
let mut state = cmd_proxy_state.lock().unwrap();
let now = Instant::now();
if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL {
state.need_refresh = true;
state.last_refresh_time = now;
}
state.need_refresh
let safe_data = if cached_data.is_null() {
serde_json::json!({
"providers": {}
})
} else {
cached_data
};
if should_refresh {
let manager = MihomoManager::global();
let providers = manager.get_providers_proxies().await?;
{
let mut state = cmd_proxy_state.lock().unwrap();
state.providers_proxies = Box::new(providers);
state.need_refresh = false;
// 检查是否需要刷新
let should_refresh = {
let last_refresh = LAST_REFRESH_TIME.lock();
match *last_refresh {
Some(last_time) => last_time.elapsed() > REFRESH_INTERVAL,
None => true,
}
log::debug!(target: "app", "providers_proxies刷新成功");
};
if should_refresh && !IS_REFRESHING.load(Ordering::Acquire) {
IS_REFRESHING.store(true, Ordering::Release);
crate::process::AsyncHandler::spawn(|| async move {
let manager = MihomoManager::global();
match manager.refresh_providers_proxies().await {
Ok(_) => {
log::debug!(target: "app", "providers_proxies静默后台刷新成功");
}
Err(e) => {
log::warn!(target: "app", "providers_proxies后台刷新失败: {}", e);
}
}
{
let mut last_refresh = LAST_REFRESH_TIME.lock();
*last_refresh = Some(Instant::now());
}
IS_REFRESHING.store(false, Ordering::Release);
});
}
let providers_proxies = {
let state = cmd_proxy_state.lock().unwrap();
state.providers_proxies.clone()
};
Ok(*providers_proxies)
Ok(safe_data)
}
+1 -1
View File
@@ -6,7 +6,7 @@ use crate::{config::*, feat, wrap_err};
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge();
let verge_data = verge.data().clone();
Ok(IVergeResponse::from(*verge_data))
Ok(IVergeResponse::from(verge_data))
}
/// 修改Verge配置
-4
View File
@@ -63,8 +63,6 @@ impl IClashTemp {
vec![
"tauri://localhost",
"http://tauri.localhost",
// Only enable this in dev mode
#[cfg(feature = "verge-dev")]
"http://localhost:3000",
"https://yacd.metacubex.one",
"https://metacubex.github.io",
@@ -105,8 +103,6 @@ impl IClashTemp {
vec![
"tauri://localhost",
"http://tauri.localhost",
// Only enable this in dev mode
#[cfg(feature = "verge-dev")]
"http://localhost:3000",
"https://yacd.metacubex.one",
"https://metacubex.github.io",
+14 -53
View File
@@ -15,10 +15,10 @@ pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
pub struct Config {
clash_config: Draft<Box<IClashTemp>>,
verge_config: Draft<Box<IVerge>>,
profiles_config: Draft<Box<IProfiles>>,
runtime_config: Draft<Box<IRuntime>>,
clash_config: Draft<IClashTemp>,
verge_config: Draft<IVerge>,
profiles_config: Draft<IProfiles>,
runtime_config: Draft<IRuntime>,
}
impl Config {
@@ -26,26 +26,26 @@ impl Config {
static CONFIG: OnceCell<Config> = OnceCell::new();
CONFIG.get_or_init(|| Config {
clash_config: Draft::from(Box::new(IClashTemp::new())),
verge_config: Draft::from(Box::new(IVerge::new())),
profiles_config: Draft::from(Box::new(IProfiles::new())),
runtime_config: Draft::from(Box::new(IRuntime::new())),
clash_config: Draft::from(IClashTemp::new()),
verge_config: Draft::from(IVerge::new()),
profiles_config: Draft::from(IProfiles::new()),
runtime_config: Draft::from(IRuntime::new()),
})
}
pub fn clash() -> Draft<Box<IClashTemp>> {
pub fn clash() -> Draft<IClashTemp> {
Self::global().clash_config.clone()
}
pub fn verge() -> Draft<Box<IVerge>> {
pub fn verge() -> Draft<IVerge> {
Self::global().verge_config.clone()
}
pub fn profiles() -> Draft<Box<IProfiles>> {
pub fn profiles() -> Draft<IProfiles> {
Self::global().profiles_config.clone()
}
pub fn runtime() -> Draft<Box<IRuntime>> {
pub fn runtime() -> Draft<IRuntime> {
Self::global().runtime_config.clone()
}
@@ -149,11 +149,11 @@ impl Config {
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
*Config::runtime().draft() = Box::new(IRuntime {
*Config::runtime().draft() = IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
});
};
Ok(())
}
@@ -164,42 +164,3 @@ pub enum ConfigType {
Run,
Check,
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem;
#[test]
fn test_prfitem_from_merge_size() {
let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap();
dbg!(&merge_item);
let prfitem_size = mem::size_of_val(&merge_item);
dbg!(prfitem_size);
// Boxed version
let boxed_merge_item = Box::new(merge_item);
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
dbg!(box_prfitem_size);
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
assert!(box_prfitem_size < prfitem_size);
}
#[test]
fn test_draft_size_non_boxed() {
let draft = Draft::from(IRuntime::new());
let iruntime_size = std::mem::size_of_val(&draft);
dbg!(iruntime_size);
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
}
#[test]
fn test_draft_size_boxed() {
let draft = Draft::from(Box::new(IRuntime::new()));
let box_iruntime_size = std::mem::size_of_val(&draft);
dbg!(box_iruntime_size);
assert_eq!(
box_iruntime_size,
std::mem::size_of::<Draft<Box<IRuntime>>>()
);
}
}
+20 -28
View File
@@ -9,21 +9,13 @@ pub struct Draft<T: Clone + ToOwned> {
macro_rules! draft_define {
($id: ident) => {
impl From<$id> for Draft<$id> {
fn from(data: $id) -> Self {
Draft {
inner: Arc::new(Mutex::new((data, None))),
}
}
}
impl Draft<Box<$id>> {
impl Draft<$id> {
#[allow(unused)]
pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
pub fn data(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
}
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
pub fn latest(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
&mut inner.0
@@ -33,7 +25,7 @@ macro_rules! draft_define {
})
}
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
pub fn draft(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
inner.1 = Some(inner.0.clone());
@@ -43,7 +35,7 @@ macro_rules! draft_define {
})
}
pub fn apply(&self) -> Option<Box<$id>> {
pub fn apply(&self) -> Option<$id> {
let mut inner = self.inner.lock();
match inner.1.take() {
@@ -56,14 +48,14 @@ macro_rules! draft_define {
}
}
pub fn discard(&self) -> Option<Box<$id>> {
pub fn discard(&self) -> Option<$id> {
let mut inner = self.inner.lock();
inner.1.take()
}
}
impl From<Box<$id>> for Draft<Box<$id>> {
fn from(data: Box<$id>) -> Self {
impl From<$id> for Draft<$id> {
fn from(data: $id) -> Self {
Draft {
inner: Arc::new(Mutex::new((data, None))),
}
@@ -79,12 +71,12 @@ draft_define!(IRuntime);
draft_define!(IVerge);
#[test]
fn test_draft_box() {
let verge = Box::new(IVerge {
fn test_draft() {
let verge = IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..IVerge::default()
});
};
let draft = Draft::from(verge);
@@ -94,11 +86,10 @@ fn test_draft_box() {
assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert_eq!(draft.draft().enable_tun_mode, Some(false));
{
let mut d = draft.draft();
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
}
let mut d = draft.draft();
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
drop(d);
assert_eq!(draft.data().enable_auto_launch, Some(true));
assert_eq!(draft.data().enable_tun_mode, Some(false));
@@ -118,17 +109,18 @@ fn test_draft_box() {
assert_eq!(draft.draft().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_tun_mode, Some(true));
{
let mut d = draft.draft();
d.enable_auto_launch = Some(true);
}
let mut d = draft.draft();
d.enable_auto_launch = Some(true);
drop(d);
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert!(draft.discard().is_some());
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert!(draft.discard().is_none());
assert_eq!(draft.draft().enable_auto_launch, Some(false));
+1 -202
View File
@@ -3,7 +3,7 @@ use crate::utils::{dirs, help};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::{collections::HashSet, fs, io::Write};
use std::{fs, io::Write};
/// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -15,14 +15,6 @@ pub struct IProfiles {
pub items: Option<Vec<PrfItem>>,
}
/// 清理结果
#[derive(Debug, Clone)]
pub struct CleanupResult {
pub total_files: usize,
pub deleted_files: Vec<String>,
pub failed_deletions: Vec<String>,
}
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
@@ -493,197 +485,4 @@ impl IProfiles {
.collect()
})
}
/// 以 app 中的 profile 列表为准,删除不再需要的文件
pub fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
let profiles_dir = dirs::app_profiles_dir()?;
if !profiles_dir.exists() {
return Ok(CleanupResult {
total_files: 0,
deleted_files: vec![],
failed_deletions: vec![],
});
}
// 获取所有 active profile 的文件名集合
let active_files = self.get_all_active_files();
// 添加全局扩展配置文件到保护列表
let protected_files = self.get_protected_global_files();
// 扫描 profiles 目录下的所有文件
let mut total_files = 0;
let mut deleted_files = vec![];
let mut failed_deletions = vec![];
for entry in std::fs::read_dir(&profiles_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
total_files += 1;
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if Self::is_profile_file(file_name) {
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {}", file_name);
continue;
}
// 检查是否为活跃文件
if !active_files.contains(file_name) {
match std::fs::remove_file(&path) {
Ok(_) => {
deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {}", file_name);
}
Err(e) => {
failed_deletions.push(format!("{}: {}", file_name, e));
log::warn!(target: "app", "清理文件失败: {} - {}", file_name, e);
}
}
}
}
}
}
let result = CleanupResult {
total_files,
deleted_files,
failed_deletions,
};
log::info!(
target: "app",
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
result.total_files,
result.deleted_files.len(),
result.failed_deletions.len()
);
Ok(result)
}
/// 不删除全局扩展配置
fn get_protected_global_files(&self) -> HashSet<String> {
let mut protected_files = HashSet::new();
protected_files.insert("Merge.yaml".to_string());
protected_files.insert("Script.js".to_string());
protected_files
}
/// 获取所有 active profile 关联的文件名
fn get_all_active_files(&self) -> HashSet<String> {
let mut active_files = HashSet::new();
if let Some(items) = &self.items {
for item in items {
// 收集所有类型 profile 的文件
if let Some(file) = &item.file {
active_files.insert(file.clone());
}
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
if let Some(itype) = &item.itype {
if itype == "remote" || itype == "local" {
if let Some(option) = &item.option {
// 收集关联的扩展文件
if let Some(merge_uid) = &option.merge {
if let Ok(merge_item) = self.get_item(merge_uid) {
if let Some(file) = &merge_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(script_uid) = &option.script {
if let Ok(script_item) = self.get_item(script_uid) {
if let Some(file) = &script_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(rules_uid) = &option.rules {
if let Ok(rules_item) = self.get_item(rules_uid) {
if let Some(file) = &rules_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(proxies_uid) = &option.proxies {
if let Ok(proxies_item) = self.get_item(proxies_uid) {
if let Some(file) = &proxies_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(groups_uid) = &option.groups {
if let Ok(groups_item) = self.get_item(groups_uid) {
if let Some(file) = &groups_item.file {
active_files.insert(file.clone());
}
}
}
}
}
}
}
}
active_files
}
/// 检查文件名是否符合 profile 文件的命名规则
fn is_profile_file(filename: &str) -> bool {
// 匹配各种 profile 文件格式
// R12345678.yaml (remote)
// L12345678.yaml (local)
// m12345678.yaml (merge)
// s12345678.js (script)
// r12345678.yaml (rules)
// p12345678.yaml (proxies)
// g12345678.yaml (groups)
let patterns = [
r"^[RL][a-zA-Z0-9]+\.yaml$", // Remote/Local profiles
r"^m[a-zA-Z0-9]+\.yaml$", // Merge files
r"^s[a-zA-Z0-9]+\.js$", // Script files
r"^[rpg][a-zA-Z0-9]+\.yaml$", // Rules/Proxies/Groups files
];
patterns.iter().any(|pattern| {
regex::Regex::new(pattern)
.map(|re| re.is_match(filename))
.unwrap_or(false)
})
}
pub fn auto_cleanup(&self) -> Result<()> {
match self.cleanup_orphaned_files() {
Ok(result) => {
if !result.deleted_files.is_empty() {
log::info!(
target: "app",
"自动清理完成,删除了 {} 个冗余文件",
result.deleted_files.len()
);
}
Ok(())
}
Err(e) => {
log::warn!(target: "app", "自动清理失败: {}", e);
Ok(())
}
}
}
}
-7
View File
@@ -200,9 +200,6 @@ pub struct IVerge {
/// 自动进入轻量模式的延迟(分钟)
pub auto_light_weight_minutes: Option<u64>,
/// 启用代理页面自动滚动
pub enable_hover_jump_navigator: Option<bool>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
}
@@ -278,7 +275,6 @@ impl IVerge {
tun_tray_icon: Some(false),
enable_auto_launch: Some(false),
enable_silent_start: Some(false),
enable_hover_jump_navigator: Some(true),
enable_system_proxy: Some(false),
proxy_auto_config: Some(false),
pac_file_content: Some(DEFAULT_PAC.into()),
@@ -355,7 +351,6 @@ impl IVerge {
patch!(enable_tun_mode);
patch!(enable_auto_launch);
patch!(enable_silent_start);
patch!(enable_hover_jump_navigator);
patch!(enable_random_port);
#[cfg(not(target_os = "windows"))]
patch!(verge_redir_port);
@@ -497,7 +492,6 @@ pub struct IVergeResponse {
pub auto_light_weight_minutes: Option<u64>,
pub enable_dns_settings: Option<bool>,
pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>,
}
@@ -567,7 +561,6 @@ impl From<IVerge> for IVergeResponse {
auto_light_weight_minutes: verge.auto_light_weight_minutes,
enable_dns_settings: verge.enable_dns_settings,
home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
service_state: verge.service_state,
}
}
+2 -2
View File
@@ -140,11 +140,11 @@ impl CoreManager {
/// 使用默认配置
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
*Config::runtime().draft() = Box::new(IRuntime {
*Config::runtime().draft() = IRuntime {
config: Some(Config::clash().latest().0.clone()),
exists_keys: vec![],
chain_logs: Default::default(),
});
};
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
+26 -67
View File
@@ -153,76 +153,35 @@ impl Hotkey {
"=== Hotkey Dashboard Window Operation Start ==="
);
// 检查是否在轻量模式下,如果是,需要同步处理
if crate::module::lightweight::is_in_lightweight_mode() {
logging!(
info,
Type::Hotkey,
true,
"In lightweight mode, calling open_or_close_dashboard directly"
);
crate::feat::open_or_close_dashboard();
} else {
AsyncHandler::spawn(move || async move {
logging!(
debug,
Type::Hotkey,
true,
"Toggle dashboard window visibility (async)"
);
// 使用 spawn_blocking 来确保在正确的线程上执行
AsyncHandler::spawn_blocking(|| {
logging!(debug, Type::Hotkey, "Toggle dashboard window visibility");
// 检查窗口是否存在
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();
}
}
// 检查窗口是否存在
if let Some(window) = handle::Handle::global().get_window() {
// 如果窗口可见,则隐藏它
if window.is_visible().unwrap_or(false) {
logging!(info, Type::Window, "Window is visible, hiding it");
let _ = window.hide();
} else {
// 如果窗口不存在,创建一个新窗口
logging!(
info,
Type::Window,
true,
"Window does not exist, creating a new one"
);
resolve::create_window(true);
// 如果窗口不可见,则显示它
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!(
info,
Type::Window,
"Window does not exist, creating a new one"
);
resolve::create_window(true);
}
});
logging!(
debug,
+5 -3
View File
@@ -845,9 +845,11 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
}
};
if version_check && is_service_available().await.is_ok() {
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
return start_with_existing_service(config_file).await;
if version_check {
if is_service_available().await.is_ok() {
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
return start_with_existing_service(config_file).await;
}
}
if !version_check {
+6 -4
View File
@@ -235,11 +235,13 @@ impl Sysopt {
} else {
return Ok(());
}
} else if let Err(e) = startup_shortcut::remove_shortcut() {
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
if let Err(e) = startup_shortcut::remove_shortcut() {
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
}
}
}
+43 -75
View File
@@ -270,7 +270,7 @@ impl Tray {
match app_handle.tray_by_id("main") {
Some(tray) => {
let _ = tray.set_menu(Some(create_tray_menu(
app_handle,
&app_handle,
Some(mode.as_str()),
*system_proxy,
*tun_mode,
@@ -288,7 +288,6 @@ impl Tray {
}
/// 更新托盘图标
#[cfg(target_os = "macos")]
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
@@ -316,83 +315,58 @@ impl Tray {
(false, true) => TrayState::get_tun_tray_icon(),
(false, false) => TrayState::get_common_tray_icon(),
};
#[cfg(target_os = "macos")]
{
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false);
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let is_colorful = colorful == "colorful";
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false);
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let is_colorful = colorful == "colorful";
if !enable_tray_speed {
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
let _ = tray.set_icon_as_template(!is_colorful);
return Ok(());
}
if !enable_tray_speed {
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
let _ = tray.set_icon_as_template(!is_colorful);
return Ok(());
}
let rate = if let Some(rate) = rate {
Some(rate)
} else {
let guard = self.speed_rate.lock();
if let Some(guard) = guard.as_ref() {
if let Some(rate) = guard.get_curent_rate() {
Some(rate)
let rate = if let Some(rate) = rate {
Some(rate)
} else {
let guard = self.speed_rate.lock();
if let Some(guard) = guard.as_ref() {
if let Some(rate) = guard.get_curent_rate() {
Some(rate)
} else {
Some(Rate::default())
}
} else {
Some(Rate::default())
}
} 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 rate = rate_guard.as_ref();
if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) {
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
let mut rate_guard = self.rate_cache.lock();
if *rate_guard != rate {
*rate_guard = rate;
let bytes = if enable_tray_icon {
Some(icon_bytes)
} else {
None
};
let rate = rate_guard.as_ref();
if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) {
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
}
}
Ok(())
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
return Ok(());
}
};
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
return Ok(());
}
};
let verge = Config::verge().latest().clone();
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
(true, true) => TrayState::get_tun_tray_icon(),
(true, false) => TrayState::get_sysproxy_tray_icon(),
(false, true) => TrayState::get_tun_tray_icon(),
(false, false) => TrayState::get_common_tray_icon(),
};
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
Ok(())
#[cfg(not(target_os = "macos"))]
{
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
Ok(())
}
}
/// 更新托盘提示
@@ -617,12 +591,6 @@ impl Tray {
let icon_bytes = TrayState::get_common_tray_icon().1;
let icon = tauri::image::Image::from_bytes(&icon_bytes)?;
#[cfg(target_os = "linux")]
let builder = TrayIconBuilder::with_id("main")
.icon(icon)
.icon_as_template(false);
#[cfg(not(target_os = "linux"))]
let mut builder = TrayIconBuilder::with_id("main")
.icon(icon)
.icon_as_template(false);
+10 -117
View File
@@ -33,50 +33,37 @@ pub fn use_script(
}
let _ = context.eval(Source::from_bytes(
r#"var console = Object.freeze({
log(data){__verge_log__("log",JSON.stringify(data, null, 2))},
info(data){__verge_log__("info",JSON.stringify(data, null, 2))},
error(data){__verge_log__("error",JSON.stringify(data, null, 2))},
debug(data){__verge_log__("debug",JSON.stringify(data, null, 2))},
warn(data){__verge_log__("warn",JSON.stringify(data, null, 2))},
table(data){__verge_log__("table",JSON.stringify(data, null, 2))},
log(data){__verge_log__("log",JSON.stringify(data))},
info(data){__verge_log__("info",JSON.stringify(data))},
error(data){__verge_log__("error",JSON.stringify(data))},
debug(data){__verge_log__("debug",JSON.stringify(data))},
});"#,
));
let config = use_lowercase(config.clone());
let config_str = serde_json::to_string(&config)?;
// 处理 name 参数中的特殊字符
let safe_name = escape_js_string(&name);
let code = format!(
r#"try{{
{script};
JSON.stringify(main({config_str},'{safe_name}')||'')
JSON.stringify(main({config_str},'{name}')||'')
}} catch(err) {{
`__error_flag__ ${{err.toString()}}`
}}"#
);
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
if !result.is_string() {
anyhow::bail!("main function should return object");
}
let result = result.to_string(&mut context).unwrap();
let result = result.to_std_string().unwrap();
// 处理 JS 执行结果中的特殊字符
let unescaped_result = unescape_js_string(&result);
if unescaped_result.starts_with("__error_flag__") {
anyhow::bail!(unescaped_result[15..].to_owned());
if result.starts_with("__error_flag__") {
anyhow::bail!(result[15..].to_owned());
}
if unescaped_result == "\"\"" {
if result == "\"\"" {
anyhow::bail!("main function should return object");
}
// 安全地解析 JSON 结果
let res: Result<Mapping, Error> = parse_json_safely(&unescaped_result);
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
let mut out = outputs.lock().unwrap();
match res {
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
@@ -90,74 +77,6 @@ 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]
fn test_script() {
let script = r#"
@@ -185,32 +104,6 @@ fn test_script() {
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
let _ = serde_yaml::to_string(&config).unwrap();
let yaml_config_size = std::mem::size_of_val(&config);
dbg!(yaml_config_size);
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
dbg!(box_yaml_config_size);
dbg!(results);
assert!(box_yaml_config_size < yaml_config_size);
}
// 测试特殊字符转义功能
#[test]
fn test_escape_unescape() {
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
let escaped = escape_js_string(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
@@ -0,0 +1 @@
pub mod service;
+1
View File
@@ -0,0 +1 @@
+27 -130
View File
@@ -13,19 +13,6 @@ pub fn open_or_close_dashboard() {
println!("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() {
println!("Found existing window");
log::info!(target: "app", "Found existing window");
@@ -62,9 +49,8 @@ pub fn open_or_close_dashboard() {
}
}
/// 异步优化的应用退出函数
/// 优化的应用退出函数
pub fn quit() {
use crate::process::AsyncHandler;
log::debug!(target: "app", "启动退出流程");
// 获取应用句柄并设置退出标志
@@ -74,144 +60,55 @@ pub fn quit() {
// 优先关闭窗口,提供立即反馈
if let Some(window) = handle::Handle::global().get_window() {
let _ = window.hide();
log::info!(target: "app", "窗口已隐藏");
}
// 使用异步任务处理资源清理,避免阻塞
AsyncHandler::spawn(move || async move {
log::info!(target: "app", "开始异步清理资源");
let cleanup_result = clean_async().await;
log::info!(target: "app", "资源清理完成,退出代码: {}", if cleanup_result { 0 } else { 1 });
app_handle.exit(if cleanup_result { 0 } else { 1 });
// 在单独线程中处理资源清理,避免阻塞主线程
std::thread::spawn(move || {
let cleanup_result = clean();
app_handle.exit(match cleanup_result {
true => 0,
false => 1,
});
});
}
async fn clean_async() -> bool {
pub fn clean() -> bool {
use tokio::time::{timeout, Duration};
log::info!(target: "app", "开始执行异步清理操作...");
// 1. 处理TUN模式
let tun_task = async {
if Config::verge().data().enable_tun_mode.unwrap_or(false) {
let rt = tokio::runtime::Runtime::new().unwrap();
let cleanup_result = rt.block_on(async {
// 1. 处理TUN模式
let tun_success = if Config::verge().data().enable_tun_mode.unwrap_or(false) {
let disable_tun = serde_json::json!({
"tun": {
"enable": false
}
});
match timeout(
Duration::from_secs(2),
timeout(
Duration::from_secs(1),
MihomoManager::global().patch_configs(disable_tun),
)
.await
{
Ok(_) => {
log::info!(target: "app", "TUN模式已禁用");
true
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时");
false
}
}
.is_ok()
} else {
true
}
};
};
// 2. 系统代理重置
let proxy_task = async {
match timeout(
Duration::from_secs(3),
// 2. 顺序执行关键清理
let proxy_res = timeout(
Duration::from_secs(1),
sysopt::Sysopt::global().reset_sysproxy(),
)
.await
{
Ok(_) => {
log::info!(target: "app", "系统代理已重置");
true
}
Err(_) => {
log::warn!(target: "app", "重置系统代理超时");
false
}
}
};
.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
}
}
};
let core_res = timeout(Duration::from_secs(1), CoreManager::global().stop_core()).await;
// 4. DNS恢复(仅macOS)
#[cfg(target_os = "macos")]
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
}
}
};
// 3. 平台特定清理
#[cfg(target_os = "macos")]
let _dns_res = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await;
// 并行执行所有清理任务
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);
tun_success && proxy_res.is_ok() && core_res.is_ok()
});
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
Ok(result) => {
log::info!(target: "app", "清理操作完成,结果: {}", result);
result
}
Err(_) => {
log::warn!(target: "app", "清理操作超时,返回成功状态避免阻塞");
true
}
}
cleanup_result
}
#[cfg(target_os = "macos")]
+10 -12
View File
@@ -2,10 +2,10 @@ mod cmd;
mod config;
mod core;
mod enhance;
mod error;
mod feat;
mod module;
mod process;
mod state;
mod utils;
use crate::{
core::hotkey,
@@ -15,8 +15,8 @@ use crate::{
use config::Config;
use std::sync::{Mutex, Once};
use tauri::AppHandle;
use tauri::Manager;
#[cfg(target_os = "macos")]
use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{timeout, Duration};
@@ -90,20 +90,17 @@ pub fn run() {
let _ = utils::dirs::init_portable_flag();
// 异步单例检测
AsyncHandler::spawn(move || async move {
// 单例检测
let app_exists: bool = AsyncHandler::block_on(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if let Some(app_handle) = AppHandleManager::global().get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
true
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
false
}
}
Err(_) => {
@@ -113,9 +110,13 @@ pub fn run() {
true,
"单例检查超时,假定没有其他实例运行"
);
false
}
}
});
if app_exists {
return;
}
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
@@ -204,9 +205,6 @@ pub fn run() {
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
}
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(())
})
+67 -91
View File
@@ -1,82 +1,47 @@
use crate::{
config::Config,
core::{handle, timer::Timer},
log_err, logging,
state::lightweight::LightWeightState,
log_err, logging, logging_error,
utils::logging::Type,
AppHandleManager,
};
#[cfg(target_os = "macos")]
use crate::logging_error;
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder;
use std::sync::Mutex;
use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock};
use std::{
sync::Arc,
time::{Duration, Instant},
};
use tauri::{Listener, Manager};
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
fn with_lightweight_status<F, R>(f: F) -> R
where
F: FnOnce(&mut LightWeightState) -> R,
{
let app_handle = handle::Handle::global().app_handle().unwrap();
let state = app_handle.state::<Mutex<LightWeightState>>();
let mut guard = state.lock().unwrap();
f(&mut guard)
// 轻量模式状态标志
static IS_LIGHTWEIGHT_MODE: OnceCell<Arc<RwLock<bool>>> = OnceCell::new();
// 添加一个锁来防止并发退出轻量模式
static EXIT_LOCK: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
fn get_lightweight_mode() -> &'static Arc<RwLock<bool>> {
IS_LIGHTWEIGHT_MODE.get_or_init(|| Arc::new(RwLock::new(false)))
}
pub fn run_once_auto_lightweight() {
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();
}
}
fn get_exit_lock() -> &'static Mutex<(bool, Instant)> {
EXIT_LOCK.get_or_init(|| Mutex::new((false, Instant::now())))
}
// 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool {
with_lightweight_status(|state| state.is_lightweight)
*get_lightweight_mode().read()
}
// 设置轻量模式状态
fn set_lightweight_mode(value: bool) {
with_lightweight_status(|state| {
state.set_lightweight_mode(value);
});
let mut mode = get_lightweight_mode().write();
*mode = value;
logging!(info, Type::Lightweight, true, "轻量模式状态: {}", value);
}
pub fn enable_auto_light_weight_mode() {
@@ -104,30 +69,49 @@ pub fn entry_lightweight_mode() {
AppHandleManager::global().set_activation_policy_accessory();
logging!(info, Type::Lightweight, true, "轻量模式已开启");
}
// 标记已进入轻量模式
set_lightweight_mode(true);
let _ = cancel_light_weight_timer();
}
// 添加从轻量模式恢复的函数
pub fn exit_lightweight_mode() {
// 确保当前确实处于轻量模式才执行退出操作
if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
// 获取锁,检查是否已经有退出操作在进行中
let mut exit_lock = get_exit_lock().lock();
let (is_exiting, last_exit_time) = *exit_lock;
let now = Instant::now();
// 如果已经有一个退出操作在进行,并且距离上次退出时间不超过2秒,跳过本次退出
if is_exiting && now.duration_since(last_exit_time) < Duration::from_secs(2) {
logging!(
warn,
Type::Lightweight,
true,
"已有退出轻量模式操作正在进行中,跳过本次请求"
);
return;
}
*exit_lock = (true, now);
// 确保当前确实处于轻量模式才执行退出操作
if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
exit_lock.0 = false;
return;
}
// 标记退出轻量模式
set_lightweight_mode(false);
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
// macOS激活策略
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_regular();
// 重置UI就绪状态
crate::utils::resolve::reset_ui_ready();
// 释放锁
exit_lock.0 = false;
}
#[cfg(target_os = "macos")]
pub fn add_light_weight_timer() {
logging_error!(Type::Lightweight, setup_light_weight_timer());
}
@@ -172,20 +156,19 @@ fn cancel_window_close_listener() {
fn setup_light_weight_timer() -> Result<()> {
Timer::global().init()?;
let mut timer_map = Timer::global().timer_map.write();
let delay_timer = Timer::global().delay_timer.write();
let mut timer_count = Timer::global().timer_count.lock();
let task_id = *timer_count;
*timer_count += 1;
let once_by_minutes = Config::verge()
.latest()
.auto_light_weight_minutes
.unwrap_or(10);
// 获取task_id
let task_id = {
let mut timer_count = Timer::global().timer_count.lock();
let id = *timer_count;
*timer_count += 1;
id
};
// 创建任务
let task = TaskBuilder::default()
.set_task_id(task_id)
.set_maximum_parallel_runnable_num(1)
@@ -196,24 +179,17 @@ fn setup_light_weight_timer() -> Result<()> {
})
.context("failed to create timer task")?;
// 添加任务到定时器
{
let delay_timer = Timer::global().delay_timer.write();
delay_timer
.add_task(task)
.context("failed to add timer task")?;
}
delay_timer
.add_task(task)
.context("failed to add timer task")?;
// 更新任务映射
{
let mut timer_map = Timer::global().timer_map.write();
let timer_task = crate::core::timer::TimerTask {
task_id,
interval_minutes: once_by_minutes,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
}
let timer_task = crate::core::timer::TimerTask {
task_id,
interval_minutes: once_by_minutes,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
logging!(
info,
+1 -4
View File
@@ -3,9 +3,7 @@ use mihomo_api;
use once_cell::sync::Lazy;
use parking_lot::{Mutex, RwLock};
use std::time::{Duration, Instant};
use tauri::http::HeaderMap;
#[cfg(target_os = "macos")]
use tauri::http::HeaderValue;
use tauri::http::{HeaderMap, HeaderValue};
// 缓存的最大有效期(5秒)
const CACHE_TTL: Duration = Duration::from_secs(5);
@@ -107,7 +105,6 @@ impl MihomoManager {
}
// 提供默认值的版本,避免在connection_info为None时panic
#[cfg(target_os = "macos")]
fn get_clash_client_info_or_default() -> (String, HeaderMap) {
Self::get_clash_client_info().unwrap_or_else(|| {
let mut headers = HeaderMap::new();
+16
View File
@@ -11,4 +11,20 @@ impl AsyncHandler {
{
async_runtime::spawn(f())
}
pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
async_runtime::spawn_blocking(f)
}
pub fn block_on<F, Fut, R>(f: F) -> R
where
F: FnOnce() -> Fut,
Fut: Future<Output = R>,
{
async_runtime::block_on(f())
}
}
-44
View File
@@ -1,44 +0,0 @@
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
@@ -1,5 +0,0 @@
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
// https://tauri.app/develop/state-management/#do-you-need-arc
pub mod lightweight;
pub mod proxy;
-19
View File
@@ -1,19 +0,0 @@
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")]
#[test]
fn test_format_bytes_speed() {
assert_eq!(format_bytes_speed(0), "0.0B/s");
assert_eq!(format_bytes_speed(1023), "1.0KB/s");
assert_eq!(format_bytes_speed(0), "0B/s");
assert_eq!(format_bytes_speed(1023), "1023B/s");
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
+47 -189
View File
@@ -1,10 +1,8 @@
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use crate::{
config::{Config, IVerge, PrfItem},
core::*,
logging, logging_error,
module::lightweight::{self, auto_lightweight_mode_init},
module::lightweight,
process::AsyncHandler,
utils::{init, logging::Type, server},
wrap_err,
@@ -144,8 +142,7 @@ pub async fn find_unused_port() -> Result<u16> {
/// 异步方式处理启动后的额外任务
pub async fn resolve_setup_async(app_handle: &AppHandle) {
let start_time = std::time::Instant::now();
logging!(info, Type::Setup, true, "开始执行异步设置任务...");
logging!(info, Type::Setup, true, "执行异步设置任务...");
if VERSION.get().is_none() {
let version = app_handle.package_info().version.to_string();
@@ -172,15 +169,6 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
logging!(trace, Type::Config, true, "初始化配置...");
logging_error!(Type::Config, true, Config::init_config().await);
// 启动时清理冗余的 Profile 文件
logging!(info, Type::Setup, true, "清理冗余的Profile文件...");
let profiles = Config::profiles();
if let Err(e) = profiles.latest().auto_cleanup() {
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
} else {
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
}
logging!(trace, Type::Core, true, "启动核心管理器...");
logging_error!(Type::Core, true, CoreManager::global().init().await);
@@ -220,46 +208,24 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
// 创建窗口
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")]
{
if is_silent_start {
use crate::AppHandleManager;
AppHandleManager::global().set_activation_policy_accessory();
}
}
create_window(!is_silent_start);
// 初始化定时器
logging_error!(Type::System, true, timer::Timer::global().init());
// 自动进入轻量模式
auto_lightweight_mode_init();
let enable_auto_light_weight_mode =
{ Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
if enable_auto_light_weight_mode && !is_silent_start {
lightweight::enable_auto_light_weight_mode();
}
logging_error!(Type::Tray, true, tray::Tray::global().update_part());
logging!(trace, Type::System, true, "初始化热键...");
logging_error!(Type::System, true, hotkey::Hotkey::global().init());
let elapsed = start_time.elapsed();
logging!(
info,
Type::Setup,
true,
"异步设置任务完成,耗时: {:?}",
elapsed
);
// 如果初始化时间过长,记录警告
if elapsed.as_secs() > 10 {
logging!(
warn,
Type::Setup,
true,
"异步设置任务耗时较长({:?})",
elapsed
);
}
logging!(info, Type::Setup, true, "异步设置任务完成");
}
/// reset system proxy (异步)
@@ -292,17 +258,6 @@ pub fn create_window(is_show: bool) -> bool {
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(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
@@ -344,72 +299,7 @@ pub fn create_window(is_show: bool) -> bool {
.fullscreen(false)
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
.min_inner_size(520.0, 520.0)
.visible(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] 窗口初始化脚本执行完成');
"#,
)
.visible(false)
.build()
{
Ok(newly_created_window) => {
@@ -428,102 +318,70 @@ pub fn create_window(is_show: bool) -> bool {
"异步窗口任务开始 (启动已标记完成)"
);
// 先运行轻量模式检测
lightweight::run_once_auto_lightweight();
// 发送启动完成事件,触发前端开始加载
logging!(
debug,
Type::Window,
true,
"发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
if is_show {
let window_clone = newly_created_window.clone();
// 立即显示窗口
// Attempt to show and focus the window first.
let _ = window_clone.show();
let _ = window_clone.set_focus();
logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
logging!(debug, Type::Window, true, "窗口已尝试显示和聚焦");
tokio::time::sleep(Duration::from_millis(100)).await; // Crucial delay
logging!(
debug,
Type::Window,
true,
"延时后,尝试发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
3
2
} else {
8
5
};
logging!(
info,
Type::Window,
true,
"开始监控UI加载状态 (最多{}秒)...",
"等待UI就绪 (最多{}秒)...",
timeout_seconds
);
// 异步监控UI状态,不影响窗口显示
tokio::spawn(async move {
let wait_result =
tokio::time::timeout(Duration::from_secs(timeout_seconds), async {
let mut check_count = 0;
while !*get_ui_ready().read() {
tokio::time::sleep(Duration::from_millis(100)).await;
check_count += 1;
// 每2秒记录一次等待状态
if check_count % 20 == 0 {
logging!(
debug,
Type::Window,
true,
"UI加载状态检查... ({}秒)",
check_count / 10
);
}
}
})
.await;
match wait_result {
Ok(_) => {
logging!(info, Type::Window, true, "UI已完全加载就绪");
// 移除初始加载指示器
if let Some(window) = handle::Handle::global().get_window() {
let _ = window.eval(r#"
const overlay = document.getElementById('initial-loading-overlay');
if (overlay) {
overlay.style.opacity = '0';
setTimeout(() => overlay.remove(), 300);
}
"#);
}
}
Err(_) => {
logging!(
warn,
Type::Window,
true,
"UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds
);
*get_ui_ready().write() = true;
let wait_result =
tokio::time::timeout(Duration::from_secs(timeout_seconds), async {
while !*get_ui_ready().read() {
tokio::time::sleep(Duration::from_millis(100)).await;
}
})
.await;
match wait_result {
Ok(_) => {
logging!(info, Type::Window, true, "UI就绪");
}
});
Err(_) => {
logging!(
warn,
Type::Window,
true,
"等待UI就绪超时({}秒),强制标记就绪",
timeout_seconds
);
*get_ui_ready().write() = true;
}
}
logging!(info, Type::Window, true, "窗口显示流程完成");
} else {
logging!(
debug,
Type::Window,
true,
"is_show为false,窗口保持隐藏状态"
"is_show为false,窗口保持隐藏。尝试发送启动事件。"
);
handle::Handle::notify_startup_completed();
}
});
true
@@ -2,10 +2,13 @@
name = "mihomo_api"
edition = "2024"
[features]
debug = []
[dependencies]
reqwest = { version = "0.12.20", features = ["json"] }
reqwest = { version = "0.12.15", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tokio = { version = "1.45.1", features = ["rt", "macros", "time"] }
tokio = { version = "1.44.1", features = ["rt", "macros", "time"] }
[dev-dependencies]
@@ -1,8 +1,11 @@
use reqwest::{Method, header::HeaderMap};
use serde_json::{Value, json};
use std::time::Duration;
use serde_json::json;
use std::{
sync::{Arc, Mutex},
time::Duration,
};
pub mod model;
pub use model::MihomoManager;
pub use model::{MihomoData, MihomoManager};
impl MihomoManager {
pub fn new(mihomo_server: String, headers: HeaderMap) -> Self {
@@ -17,10 +20,38 @@ impl MihomoManager {
Self {
mihomo_server,
data: Arc::new(Mutex::new(MihomoData {
proxies: serde_json::Value::Null,
providers_proxies: serde_json::Value::Null,
})),
client,
}
}
fn update_proxies(&self, proxies: serde_json::Value) {
let mut data = self.data.lock().expect("Mutex poisoned");
data.proxies = proxies;
}
fn update_providers_proxies(&self, providers_proxies: serde_json::Value) {
let mut data = self.data.lock().expect("Mutex poisoned");
data.providers_proxies = providers_proxies;
}
pub fn get_mihomo_server(&self) -> String {
self.mihomo_server.clone()
}
pub fn get_proxies(&self) -> serde_json::Value {
let data = self.data.lock().expect("Mutex poisoned");
data.proxies.clone()
}
pub fn get_providers_proxies(&self) -> serde_json::Value {
let data = self.data.lock().expect("Mutex poisoned");
data.providers_proxies.clone()
}
async fn send_request(
&self,
method: Method,
@@ -56,16 +87,18 @@ impl MihomoManager {
Ok(response)
}
pub async fn get_refresh_proxies(&self) -> Result<Value, String> {
pub async fn refresh_proxies(&self) -> Result<&Self, String> {
let url = format!("{}/proxies", self.mihomo_server);
let proxies = self.send_request(Method::GET, url, None).await?;
Ok(proxies)
self.update_proxies(proxies);
Ok(self)
}
pub async fn get_providers_proxies(&self) -> Result<Value, String> {
pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> {
let url = format!("{}/providers/proxies", self.mihomo_server);
let providers_proxies = self.send_request(Method::GET, url, None).await?;
Ok(providers_proxies)
self.update_providers_proxies(providers_proxies);
Ok(self)
}
pub async fn close_all_connections(&self) -> Result<(), String> {
@@ -1,5 +1,27 @@
use std::sync::{Arc, Mutex};
pub struct MihomoData {
pub(crate) proxies: serde_json::Value,
pub(crate) providers_proxies: serde_json::Value,
}
#[derive(Clone)]
pub struct MihomoManager {
pub(crate) mihomo_server: String,
pub(crate) data: Arc<Mutex<MihomoData>>,
pub(crate) client: reqwest::Client,
}
#[cfg(feature = "debug")]
impl Drop for MihomoData {
fn drop(&mut self) {
println!("Dropping MihomoData");
}
}
#[cfg(feature = "debug")]
impl Drop for MihomoManager {
fn drop(&mut self) {
println!("Dropping MihomoManager");
}
}
@@ -1,7 +1,29 @@
use mihomo_api;
use reqwest::header::HeaderMap;
#[test]
fn test_mihomo_manager_init() {
let _ = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
assert_eq!(true, true);
let manager = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
assert_eq!(manager.get_proxies(), serde_json::Value::Null);
assert_eq!(manager.get_providers_proxies(), serde_json::Value::Null);
}
#[tokio::test]
async fn test_refresh_proxies() {
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
let manager = manager.refresh_proxies().await.unwrap();
let proxies = manager.get_proxies();
let providers = manager.get_providers_proxies();
assert_ne!(proxies, serde_json::Value::Null);
assert_eq!(providers, serde_json::Value::Null);
}
#[tokio::test]
async fn test_refresh_providers_proxies() {
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
let manager = manager.refresh_providers_proxies().await.unwrap();
let proxies = manager.get_proxies();
let providers = manager.get_providers_proxies();
assert_eq!(proxies, serde_json::Value::Null);
assert_ne!(providers, serde_json::Value::Null);
}
+22 -6
View File
@@ -11,9 +11,15 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": ["resources", "resources/locales/*"],
"resources": [
"resources",
"resources/locales/*"
],
"publisher": "Clash Verge Rev",
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"externalBin": [
"sidecar/verge-mihomo",
"sidecar/verge-mihomo-alpha"
],
"copyright": "GNU General Public License v3.0",
"category": "DeveloperTool",
"shortDescription": "Clash Verge Rev",
@@ -44,18 +50,28 @@
},
"deep-link": {
"desktop": {
"schemes": ["clash", "clash-verge"]
"schemes": [
"clash",
"clash-verge"
]
}
}
},
"app": {
"security": {
"capabilities": ["desktop-capability", "migrated"],
"capabilities": [
"desktop-capability",
"migrated"
],
"assetProtocol": {
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
"scope": [
"$APPDATA/**",
"$RESOURCE/../**",
"**"
],
"enable": true
},
"csp": null
}
}
}
}
+1 -8
View File
@@ -21,13 +21,6 @@
}
},
"app": {
"windows": [],
"security": {
"capabilities": [
"desktop-capability",
"desktop-windows-capability",
"migrated"
]
}
"windows": []
}
}
+3 -3
View File
@@ -4,9 +4,9 @@
body {
margin: 0;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
user-select: none;
+47 -44
View File
@@ -1,17 +1,20 @@
import React, { useSyncExternalStore } from "react";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import { CloseRounded } from "@mui/icons-material";
import {
subscribeNotices,
hideNotice,
getSnapshotNotices,
} from "@/services/noticeService";
import React, { useState, useEffect } from 'react';
import { Snackbar, Alert, IconButton, Box } from '@mui/material';
import { CloseRounded } from '@mui/icons-material';
import { subscribeNotices, hideNotice, NoticeItem } from '@/services/noticeService';
export const NoticeManager: React.FC = () => {
const currentNotices = useSyncExternalStore(
subscribeNotices,
getSnapshotNotices,
);
const [currentNotices, setCurrentNotices] = useState<NoticeItem[]>([]);
useEffect(() => {
const unsubscribe = subscribeNotices((notices) => {
setCurrentNotices(notices);
});
return () => {
unsubscribe();
};
}, []);
const handleClose = (id: number) => {
hideNotice(id);
@@ -20,49 +23,49 @@ export const NoticeManager: React.FC = () => {
return (
<Box
sx={{
position: "fixed",
top: "20px",
right: "20px",
position: 'fixed',
top: '20px',
right: '20px',
zIndex: 1500,
display: "flex",
flexDirection: "column",
gap: "10px",
maxWidth: "360px",
display: 'flex',
flexDirection: 'column',
gap: '10px',
maxWidth: '360px',
}}
>
{currentNotices.map((notice) => (
<Snackbar
key={notice.id}
open={true}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
sx={{
position: "relative",
transform: "none",
top: "auto",
right: "auto",
bottom: "auto",
left: "auto",
width: "100%",
position: 'relative',
transform: 'none',
top: 'auto',
right: 'auto',
bottom: 'auto',
left: 'auto',
width: '100%',
}}
>
<Alert
severity={notice.type}
variant="filled"
sx={{ width: "100%" }}
action={
<IconButton
size="small"
color="inherit"
onClick={() => handleClose(notice.id)}
>
<CloseRounded fontSize="inherit" />
</IconButton>
}
>
{notice.message}
</Alert>
<Alert
severity={notice.type}
variant="filled"
sx={{ width: '100%' }}
action={
<IconButton
size="small"
color="inherit"
onClick={() => handleClose(notice.id)}
>
<CloseRounded fontSize="inherit" />
</IconButton>
}
>
{notice.message}
</Alert>
</Snackbar>
))}
</Box>
);
};
};
+1 -1
View File
@@ -157,7 +157,7 @@ export const BaseSearchBox = (props: SearchProps) => {
</Tooltip>
</Box>
),
},
}
}}
/>
</Tooltip>
@@ -107,14 +107,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{information.map((each) => (
<div key={each.label}>
<b>{each.label}</b>
<span
style={{
wordBreak: "break-all",
color: theme.palette.text.primary,
}}
>
: {each.value}
</span>
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span>
</div>
))}
+1 -1
View File
@@ -25,7 +25,7 @@ export const ClashInfoCard = () => {
// 使用备忘录组件内容,减少重新渲染
const cardContent = useMemo(() => {
if (!clashConfig) return null;
return (
<Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between">
+23 -32
View File
@@ -23,24 +23,12 @@ export const ClashModeCard = () => {
// 直接使用API返回的模式,不维护本地状态
const currentMode = clashConfig?.mode?.toLowerCase();
const modeDescription = useMemo(() => {
if (typeof currentMode === "string" && currentMode.length > 0) {
return t(
`${currentMode[0].toLocaleUpperCase()}${currentMode.slice(1)} Mode Description`,
);
}
return t("Mode Description Not Available");
}, [currentMode]);
// 模式图标映射
const modeIcons = useMemo(
() => ({
rule: <MultipleStopRounded fontSize="small" />,
global: <LanguageRounded fontSize="small" />,
direct: <DirectionsRounded fontSize="small" />,
}),
[],
);
const modeIcons = useMemo(() => ({
rule: <MultipleStopRounded fontSize="small" />,
global: <LanguageRounded fontSize="small" />,
direct: <DirectionsRounded fontSize="small" />
}), []);
// 切换模式的处理函数
const onChangeMode = useLockFn(async (mode: string) => {
@@ -80,19 +68,18 @@ export const ClashModeCard = () => {
"&:active": {
transform: "translateY(1px)",
},
"&::after":
mode === currentMode
? {
content: '""',
position: "absolute",
bottom: -16,
left: "50%",
width: 2,
height: 16,
bgcolor: "primary.main",
transform: "translateX(-50%)",
}
: {},
"&::after": mode === currentMode
? {
content: '""',
position: "absolute",
bottom: -16,
left: "50%",
width: 2,
height: 16,
bgcolor: "primary.main",
transform: "translateX(-50%)",
}
: {},
});
// 描述样式
@@ -156,8 +143,12 @@ export const ClashModeCard = () => {
overflow: "visible",
}}
>
<Typography variant="caption" component="div" sx={descriptionStyles}>
{modeDescription}
<Typography
variant="caption"
component="div"
sx={descriptionStyles}
>
{t(`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`)}
</Typography>
</Box>
</Box>
+32 -48
View File
@@ -105,7 +105,7 @@ export const CurrentProxyCard = () => {
// 添加排序类型状态
const [sortType, setSortType] = useState<ProxySortType>(() => {
const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE);
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0;
return savedSortType ? Number(savedSortType) as ProxySortType : 0;
});
// 定义状态类型
@@ -156,8 +156,7 @@ export const CurrentProxyCard = () => {
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) ||
proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
return primaryGroup?.name || "";
};
@@ -201,13 +200,11 @@ export const CurrentProxyCard = () => {
// 只保留 Selector 类型的组用于选择
const filteredGroups = proxies.groups
.filter((g: { name: string; type?: string }) => g.type === "Selector")
.map(
(g: { name: string; now: string; all: Array<{ name: string }> }) => ({
name: g.name,
now: g.now || "",
all: g.all.map((p: { name: string }) => p.name),
}),
);
.map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({
name: g.name,
now: g.now || "",
all: g.all.map((p: { name: string }) => p.name),
}));
let newProxy = "";
let newDisplayProxy = null;
@@ -233,12 +230,12 @@ export const CurrentProxyCard = () => {
if (selectorGroup) {
newGroup = selectorGroup.name;
newProxy = selectorGroup.now || selectorGroup.all[0] || "";
newDisplayProxy = proxies.records?.[newProxy] || null;
newDisplayProxy = proxies.records?.[newProxy] || null;
if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
}
}
}
@@ -283,9 +280,7 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
setState((prev) => {
const group = prev.proxyData.groups.find(
(g: { name: string }) => g.name === newGroup,
);
const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup);
if (group) {
return {
...prev,
@@ -373,16 +368,14 @@ export const CurrentProxyCard = () => {
}, [state.displayProxy]);
// 获取当前节点的延迟(增加非空校验)
const currentDelay =
currentProxy && state.selection.group
? delayManager.getDelayFix(currentProxy, state.selection.group)
: -1;
const currentDelay = currentProxy && state.selection.group
? delayManager.getDelayFix(currentProxy, state.selection.group)
: -1;
// 信号图标(增加非空校验)
const signalInfo =
currentProxy && state.selection.group
? getSignalIcon(currentDelay)
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
const signalInfo = currentProxy && state.selection.group
? getSignalIcon(currentDelay)
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
// 自定义渲染选择框中的值
const renderProxyValue = useCallback(
@@ -391,7 +384,7 @@ export const CurrentProxyCard = () => {
const delayValue = delayManager.getDelayFix(
state.proxyData.records[selected],
state.selection.group,
state.selection.group
);
return (
@@ -448,7 +441,7 @@ export const CurrentProxyCard = () => {
return list;
},
[sortType, state.proxyData.records, state.selection.group],
[sortType, state.proxyData.records, state.selection.group]
);
// 计算要显示的代理选项(增加非空校验)
@@ -459,11 +452,11 @@ export const CurrentProxyCard = () => {
if (isGlobalMode && proxies?.global) {
const options = proxies.global.all
.filter((p: any) => {
const name = typeof p === "string" ? p : p.name;
const name = typeof p === 'string' ? p : p.name;
return name !== "DIRECT" && name !== "REJECT";
})
.map((p: any) => ({
name: typeof p === "string" ? p : p.name,
name: typeof p === 'string' ? p : p.name
}));
return sortProxies(options);
@@ -471,7 +464,7 @@ export const CurrentProxyCard = () => {
// 规则模式
const group = state.selection.group
? state.proxyData.groups.find((g) => g.name === state.selection.group)
? state.proxyData.groups.find(g => g.name === state.selection.group)
: null;
if (group) {
@@ -480,14 +473,7 @@ export const CurrentProxyCard = () => {
}
return [];
}, [
isDirectMode,
isGlobalMode,
proxies,
state.proxyData,
state.selection.group,
sortProxies,
]);
}, [isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortProxies]);
// 获取排序图标
const getSortIcon = () => {
@@ -674,14 +660,12 @@ export const CurrentProxyCard = () => {
{isDirectMode
? null
: proxyOptions.map((proxy, index) => {
const delayValue =
state.proxyData.records[proxy.name] &&
state.selection.group
? delayManager.getDelayFix(
state.proxyData.records[proxy.name],
state.selection.group,
)
: -1;
const delayValue = state.proxyData.records[proxy.name] && state.selection.group
? delayManager.getDelayFix(
state.proxyData.records[proxy.name],
state.selection.group,
)
: -1;
return (
<MenuItem
key={`${proxy.name}-${index}`}
@@ -722,4 +706,4 @@ export const CurrentProxyCard = () => {
)}
</EnhancedCard>
);
};
};
+14 -14
View File
@@ -38,7 +38,7 @@ export const EnhancedCard = ({
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "block",
display: "block"
};
return (
@@ -62,15 +62,13 @@ export const EnhancedCard = ({
borderColor: "divider",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
minWidth: 0,
flex: 1,
overflow: "hidden",
}}
>
<Box sx={{
display: "flex",
alignItems: "center",
minWidth: 0,
flex: 1,
overflow: "hidden"
}}>
<Box
sx={{
display: "flex",
@@ -89,9 +87,9 @@ export const EnhancedCard = ({
</Box>
<Box sx={{ minWidth: 0, flex: 1 }}>
{typeof title === "string" ? (
<Typography
variant="h6"
fontWeight="medium"
<Typography
variant="h6"
fontWeight="medium"
fontSize={18}
sx={titleTruncateStyle}
title={title}
@@ -99,7 +97,9 @@ export const EnhancedCard = ({
{title}
</Typography>
) : (
<Box sx={titleTruncateStyle}>{title}</Box>
<Box sx={titleTruncateStyle}>
{title}
</Box>
)}
</Box>
</Box>
+179 -231
View File
@@ -30,7 +30,7 @@ ChartJS.register(
PointElement,
LineElement,
Tooltip,
Filler,
Filler
);
// 流量数据项接口
@@ -54,8 +54,8 @@ type DataPoint = ITrafficItem & { name: string; timestamp: number };
/**
* 增强型流量图表组件
*/
export const EnhancedTrafficGraph = memo(
forwardRef<EnhancedTrafficGraphRef>((props, ref) => {
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
(props, ref) => {
const theme = useTheme();
const { t } = useTranslation();
@@ -63,20 +63,20 @@ export const EnhancedTrafficGraph = memo(
const [timeRange, setTimeRange] = useState<TimeRange>(10);
const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
const [displayData, setDisplayData] = useState<DataPoint[]>([]);
// 数据缓冲区
const dataBufferRef = useRef<DataPoint[]>([]);
// 根据时间范围计算保留的数据点数量
const getMaxPointsByTimeRange = useCallback(
(minutes: TimeRange): number => minutes * 60,
[],
[]
);
// 最大数据点数量
const MAX_BUFFER_SIZE = useMemo(
() => getMaxPointsByTimeRange(10),
[getMaxPointsByTimeRange],
[getMaxPointsByTimeRange]
);
// 颜色配置
@@ -89,28 +89,23 @@ export const EnhancedTrafficGraph = memo(
text: theme.palette.text.primary,
tooltipBorder: theme.palette.divider,
}),
[theme],
[theme]
);
// 切换时间范围
const handleTimeRangeClick = useCallback(
(event: React.MouseEvent<SVGTextElement>) => {
event.stopPropagation();
setTimeRange((prevRange) => {
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
});
},
[],
);
const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => {
event.stopPropagation();
setTimeRange((prevRange) => {
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
});
}, []);
// 点击图表主体或图例时切换样式
const handleToggleStyleClick = useCallback(
(event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
event.stopPropagation();
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
},
[],
);
const handleToggleStyleClick = useCallback((event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
event.stopPropagation();
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
}, []);
// 初始化数据缓冲区
useEffect(() => {
@@ -126,9 +121,7 @@ export const EnhancedTrafficGraph = memo(
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(
`Initial data generation: Invalid date for timestamp ${pointTime}`,
);
console.warn(`Initial data generation: Invalid date for timestamp ${pointTime}`);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
@@ -139,14 +132,7 @@ export const EnhancedTrafficGraph = memo(
});
}
} 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";
}
@@ -156,66 +142,55 @@ export const EnhancedTrafficGraph = memo(
timestamp: pointTime,
name: nameValue,
};
},
}
);
dataBufferRef.current = initialBuffer;
// 更新显示数据
const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(initialBuffer.slice(-pointsToShow));
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
// 添加数据点方法
const appendData = useCallback(
(data: ITrafficItem) => {
const safeData = {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
down:
typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
};
const appendData = useCallback((data: ITrafficItem) => {
const safeData = {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
};
const timestamp = data.timestamp || Date.now();
const date = new Date(timestamp);
const timestamp = data.timestamp || Date.now();
const date = new Date(timestamp);
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
} catch (e) {
console.error(
"Error in toLocaleTimeString in appendData:",
e,
"Date:",
date,
"Timestamp:",
timestamp,
);
nameValue = "Err:Time";
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
// 带时间标签的新数据点
const newPoint: DataPoint = {
...safeData,
name: nameValue,
timestamp: timestamp,
};
} catch (e) {
console.error("Error in toLocaleTimeString in appendData:", e, "Date:", date, "Timestamp:", timestamp);
nameValue = "Err:Time";
}
// 带时间标签的新数据点
const newPoint: DataPoint = {
...safeData,
name: nameValue,
timestamp: timestamp,
};
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer;
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer;
const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow));
},
[timeRange, getMaxPointsByTimeRange],
);
const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow));
}, [timeRange, getMaxPointsByTimeRange]);
// 监听时间范围变化
useEffect(() => {
@@ -227,7 +202,7 @@ export const EnhancedTrafficGraph = memo(
// 切换图表样式
const toggleStyle = useCallback(() => {
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
setChartStyle((prev) => prev === "line" ? "area" : "line");
}, []);
// 暴露方法给父组件
@@ -237,31 +212,30 @@ export const EnhancedTrafficGraph = memo(
appendData,
toggleStyle,
}),
[appendData, toggleStyle],
[appendData, toggleStyle]
);
const formatYAxis = useCallback((value: number | string): string => {
if (typeof value !== "number") return String(value);
if (typeof value !== 'number') return String(value);
const [num, unit] = parseTraffic(value);
return `${num}${unit}`;
}, []);
const formatXLabel = useCallback(
(tickValue: string | number, index: number, ticks: any[]) => {
const dataPoint = displayData[index as number];
if (dataPoint && dataPoint.name) {
const parts = dataPoint.name.split(":");
return `${parts[0]}:${parts[1]}`;
}
if (typeof tickValue === "string") {
const parts = tickValue.split(":");
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
return tickValue;
}
return "";
},
[displayData],
);
const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => {
const dataPoint = displayData[index as number];
if (dataPoint && dataPoint.name) {
const parts = dataPoint.name.split(":");
return `${parts[0]}:${parts[1]}`;
}
if(typeof tickValue === 'string') {
const parts = tickValue.split(":");
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
return tickValue;
}
return '';
}, [displayData]);
// 获取当前时间范围文本
const getTimeRangeText = useCallback(() => {
@@ -269,13 +243,13 @@ export const EnhancedTrafficGraph = memo(
}, [timeRange, t]);
const chartData = useMemo(() => {
const labels = displayData.map((d) => d.name);
const labels = displayData.map(d => d.name);
return {
labels,
datasets: [
{
label: t("Upload"),
data: displayData.map((d) => d.up),
data: displayData.map(d => d.up),
borderColor: colors.up,
backgroundColor: chartStyle === "area" ? colors.up : colors.up,
fill: chartStyle === "area",
@@ -286,7 +260,7 @@ export const EnhancedTrafficGraph = memo(
},
{
label: t("Download"),
data: displayData.map((d) => d.down),
data: displayData.map(d => d.down),
borderColor: colors.down,
backgroundColor: chartStyle === "area" ? colors.down : colors.down,
fill: chartStyle === "area",
@@ -294,130 +268,113 @@ export const EnhancedTrafficGraph = memo(
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2,
},
],
}
]
};
}, [displayData, colors.up, colors.down, t, chartStyle]);
const chartOptions = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
animation: false as false,
scales: {
x: {
const chartOptions = useMemo(() => ({
responsive: true,
maintainAspectRatio: false,
animation: false as false,
scales: {
x: {
display: true,
type: 'category' as const,
labels: displayData.map(d => d.name),
ticks: {
display: true,
type: "category" as const,
labels: displayData.map((d) => d.name),
ticks: {
display: true,
color: colors.text,
font: { size: 10 },
callback: function (
this: Scale,
tickValue: string | number,
index: number,
ticks: Tick[],
): string | undefined {
let labelToFormat: string | undefined = undefined;
color: colors.text,
font: { size: 10 },
callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined {
let labelToFormat: string | undefined = undefined;
const currentDisplayTick = ticks[index];
if (
currentDisplayTick &&
typeof currentDisplayTick.label === "string"
) {
labelToFormat = currentDisplayTick.label;
} else {
const sourceLabels = displayData.map((d) => d.name);
if (
typeof tickValue === "number" &&
tickValue >= 0 &&
tickValue < sourceLabels.length
) {
labelToFormat = sourceLabels[tickValue];
} else if (typeof tickValue === "string") {
labelToFormat = tickValue;
}
const currentDisplayTick = ticks[index];
if (currentDisplayTick && typeof currentDisplayTick.label === 'string') {
labelToFormat = currentDisplayTick.label;
} else {
const sourceLabels = displayData.map(d => d.name);
if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) {
labelToFormat = sourceLabels[tickValue];
} else if (typeof tickValue === 'string') {
labelToFormat = tickValue;
}
}
if (typeof labelToFormat !== "string") {
return undefined;
}
if (typeof labelToFormat !== 'string') {
return undefined;
}
const parts: string[] = labelToFormat.split(":");
return parts.length >= 2
? `${parts[0]}:${parts[1]}`
: labelToFormat;
},
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,
const parts: string[] = labelToFormat.split(':');
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat;
},
autoSkip: true,
maxTicksLimit: Math.max(5, Math.floor(displayData.length / (timeRange * 2))),
minRotation: 0,
maxRotation: 0,
},
y: {
beginAtZero: true,
ticks: {
color: colors.text,
font: { size: 10 },
callback: formatYAxis,
},
grid: {
display: true,
drawTicks: true,
tickLength: 3,
color: colors.grid,
},
grid: {
display: true,
drawOnChartArea: false,
drawTicks: true,
tickLength: 2,
color: colors.text,
},
},
plugins: {
tooltip: {
enabled: true,
mode: "index" as const,
intersect: false,
backgroundColor: colors.tooltipBg,
titleColor: colors.text,
bodyColor: colors.text,
borderColor: colors.tooltipBorder,
borderWidth: 1,
cornerRadius: 4,
padding: 8,
callbacks: {
title: (tooltipItems: any[]) => {
return `${t("Time")}: ${tooltipItems[0].label}`;
},
label: (context: any): string => {
const label = context.dataset.label || "";
const value = context.parsed.y;
const [num, unit] = parseTraffic(value);
return `${label}: ${num} ${unit}/s`;
},
y: {
beginAtZero: true,
ticks: {
color: colors.text,
font: { size: 10 },
callback: formatYAxis,
},
grid: {
display: true,
drawTicks: true,
tickLength: 3,
color: colors.grid,
},
}
},
plugins: {
tooltip: {
enabled: true,
mode: 'index' as const,
intersect: false,
backgroundColor: colors.tooltipBg,
titleColor: colors.text,
bodyColor: colors.text,
borderColor: colors.tooltipBorder,
borderWidth: 1,
cornerRadius: 4,
padding: 8,
callbacks: {
title: (tooltipItems: any[]) => {
return `${t("Time")}: ${tooltipItems[0].label}`;
},
},
legend: {
display: false,
},
label: (context: any): string => {
const label = context.dataset.label || '';
const value = context.parsed.y;
const [num, unit] = parseTraffic(value);
return `${label}: ${num} ${unit}/s`;
}
}
},
layout: {
padding: {
top: 16,
right: 7,
left: 3,
},
},
}),
[colors, t, formatYAxis, timeRange, displayData],
);
legend: {
display: false
}
},
layout: {
padding: {
top: 16,
right: 7,
left: 3,
}
}
}), [colors, t, formatYAxis, timeRange, displayData]);
return (
<Box
@@ -435,17 +392,8 @@ export const EnhancedTrafficGraph = memo(
{displayData.length > 0 && (
<ChartJsLine data={chartData} options={chartOptions} />
)}
<svg
width="100%"
height="100%"
style={{
position: "absolute",
top: 0,
left: 0,
pointerEvents: "none",
}}
>
<svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}>
<text
x="3.5%"
y="10%"
@@ -454,11 +402,11 @@ export const EnhancedTrafficGraph = memo(
fontSize={11}
fontWeight="bold"
onClick={handleTimeRangeClick}
style={{ cursor: "pointer", pointerEvents: "all" }}
style={{ cursor: "pointer", pointerEvents: 'all' }}
>
{getTimeRangeText()}
</text>
<text
x="99%"
y="10%"
@@ -467,7 +415,7 @@ export const EnhancedTrafficGraph = memo(
fontSize={12}
fontWeight="bold"
onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: "all" }}
style={{ cursor: "pointer", pointerEvents: 'all' }}
>
{t("Upload")}
</text>
@@ -480,7 +428,7 @@ export const EnhancedTrafficGraph = memo(
fontSize={12}
fontWeight="bold"
onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: "all" }}
style={{ cursor: "pointer", pointerEvents: 'all' }}
>
{t("Download")}
</text>
@@ -488,7 +436,7 @@ export const EnhancedTrafficGraph = memo(
</div>
</Box>
);
}),
);
},
));
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";
+153 -211
View File
@@ -66,90 +66,85 @@ const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
// 统计卡片组件 - 使用memo优化
const CompactStatCard = memo(
({ icon, title, value, unit, color, onClick }: StatCardProps) => {
const theme = useTheme();
const CompactStatCard = memo(({
icon,
title,
value,
unit,
color,
onClick,
}: StatCardProps) => {
const theme = useTheme();
// 获取调色板颜色 - 使用useMemo避免重复计算
const colorValue = useMemo(() => {
const palette = theme.palette;
if (
color in palette &&
palette[color as keyof typeof palette] &&
"main" in (palette[color as keyof typeof palette] as PaletteColor)
) {
return (palette[color as keyof typeof palette] as PaletteColor).main;
}
return palette.primary.main;
}, [theme.palette, color]);
// 获取调色板颜色 - 使用useMemo避免重复计算
const colorValue = useMemo(() => {
const palette = theme.palette;
if (
color in palette &&
palette[color as keyof typeof palette] &&
"main" in (palette[color as keyof typeof palette] as PaletteColor)
) {
return (palette[color as keyof typeof palette] as PaletteColor).main;
}
return palette.primary.main;
}, [theme.palette, color]);
return (
<Paper
elevation={0}
return (
<Paper
elevation={0}
sx={{
display: "flex",
alignItems: "center",
borderRadius: 2,
bgcolor: alpha(colorValue, 0.05),
border: `1px solid ${alpha(colorValue, 0.15)}`,
padding: "8px",
transition: "all 0.2s ease-in-out",
cursor: onClick ? "pointer" : "default",
"&:hover": onClick ? {
bgcolor: alpha(colorValue, 0.1),
border: `1px solid ${alpha(colorValue, 0.3)}`,
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
} : {},
}}
onClick={onClick}
>
{/* 图标容器 */}
<Grid
component="div"
sx={{
mr: 1,
ml: "2px",
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)`,
}
: {},
justifyContent: "center",
width: 32,
height: 32,
borderRadius: "50%",
bgcolor: alpha(colorValue, 0.1),
color: colorValue,
}}
onClick={onClick}
>
{/* 图标容器 */}
<Grid
component="div"
sx={{
mr: 1,
ml: "2px",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 32,
height: 32,
borderRadius: "50%",
bgcolor: alpha(colorValue, 0.1),
color: colorValue,
}}
>
{icon}
</Grid>
{icon}
</Grid>
{/* 文本内容 */}
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" noWrap>
{title}
{/* 文本内容 */}
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" noWrap>
{title}
</Typography>
<Grid component="div" sx={{ display: "flex", alignItems: "baseline" }}>
<Typography variant="body1" fontWeight="bold" noWrap sx={{ mr: 0.5 }}>
{value}
</Typography>
<Typography variant="caption" color="text.secondary">
{unit}
</Typography>
<Grid
component="div"
sx={{ 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>
);
},
);
</Grid>
</Paper>
);
});
// 添加显示名称
CompactStatCard.displayName = "CompactStatCard";
@@ -210,25 +205,25 @@ export const EnhancedTrafficStats = () => {
down: data.down,
timestamp: now,
});
} catch {}
} catch { }
return;
}
lastUpdateRef.current.traffic = now;
const safeUp = isNaN(data.up) ? 0 : data.up;
const safeDown = isNaN(data.down) ? 0 : data.down;
try {
setStats((prev) => ({
setStats(prev => ({
...prev,
traffic: { up: safeUp, down: safeDown },
traffic: { up: safeUp, down: safeDown }
}));
} catch {}
} catch { }
try {
trafficRef.current?.appendData({
up: safeUp,
down: safeDown,
timestamp: now,
});
} catch {}
} catch { }
}
} catch (err) {
console.error("[Traffic] 解析数据错误:", err, event.data);
@@ -240,12 +235,12 @@ export const EnhancedTrafficStats = () => {
try {
const data = JSON.parse(event.data) as MemoryUsage;
if (data && typeof data.inuse === "number") {
setStats((prev) => ({
setStats(prev => ({
...prev,
memory: {
inuse: isNaN(data.inuse) ? 0 : data.inuse,
oslimit: data.oslimit,
},
}
}));
}
} catch (err) {
@@ -262,7 +257,7 @@ export const EnhancedTrafficStats = () => {
// 清理现有连接的函数
const cleanupSockets = () => {
Object.values(socketRefs.current).forEach((socket) => {
Object.values(socketRefs.current).forEach(socket => {
if (socket) {
socket.close();
}
@@ -274,78 +269,40 @@ export const EnhancedTrafficStats = () => {
cleanupSockets();
// 创建新连接
console.log(
`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
);
socketRefs.current.traffic = createAuthSockette(
`${server}/traffic`,
secret,
{
onmessage: handleTrafficUpdate,
onopen: (event) => {
console.log(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
},
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(`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`);
socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, {
onmessage: handleTrafficUpdate,
onopen: (event) => {
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event);
},
);
onerror: (event) => {
console.error(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
},
onclose: (event) => {
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
}
},
});
console.log(
`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`,
);
console.log(`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`);
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
onmessage: handleMemoryUpdate,
onopen: (event) => {
console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event);
},
onerror: (event) => {
console.error(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
console.error(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } }));
},
onclose: (event) => {
console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
console.warn(`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } }));
}
},
});
@@ -357,11 +314,11 @@ export const EnhancedTrafficStats = () => {
useEffect(() => {
return () => {
try {
Object.values(socketRefs.current).forEach((socket) => {
Object.values(socketRefs.current).forEach(socket => {
if (socket) socket.close();
});
socketRefs.current = { traffic: null, memory: null };
} catch {}
} catch { }
};
}, []);
@@ -382,25 +339,13 @@ export const EnhancedTrafficStats = () => {
const [up, upUnit] = parseTraffic(stats.traffic.up);
const [down, downUnit] = parseTraffic(stats.traffic.down);
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
const [uploadTotal, uploadTotalUnit] = parseTraffic(
connections.uploadTotal,
);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
connections.downloadTotal,
);
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal);
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal);
return {
up,
upUnit,
down,
downUnit,
inuse,
inuseUnit,
uploadTotal,
uploadTotalUnit,
downloadTotal,
downloadTotalUnit,
connectionsCount: connections.count,
up, upUnit, down, downUnit, inuse, inuseUnit,
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit,
connectionsCount: connections.count
};
}, [stats, connections]);
@@ -447,54 +392,51 @@ export const EnhancedTrafficStats = () => {
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
// 使用useMemo计算统计卡片配置
const statCards = useMemo(
() => [
{
icon: <ArrowUpwardRounded fontSize="small" />,
title: t("Upload Speed"),
value: parsedData.up,
unit: `${parsedData.upUnit}/s`,
color: "secondary" as const,
},
{
icon: <ArrowDownwardRounded fontSize="small" />,
title: t("Download Speed"),
value: parsedData.down,
unit: `${parsedData.downUnit}/s`,
color: "primary" as const,
},
{
icon: <LinkRounded fontSize="small" />,
title: t("Active Connections"),
value: parsedData.connectionsCount,
unit: "",
color: "success" as const,
},
{
icon: <CloudUploadRounded fontSize="small" />,
title: t("Uploaded"),
value: parsedData.uploadTotal,
unit: parsedData.uploadTotalUnit,
color: "secondary" as const,
},
{
icon: <CloudDownloadRounded fontSize="small" />,
title: t("Downloaded"),
value: parsedData.downloadTotal,
unit: parsedData.downloadTotalUnit,
color: "primary" as const,
},
{
icon: <MemoryRounded fontSize="small" />,
title: t("Memory Usage"),
value: parsedData.inuse,
unit: parsedData.inuseUnit,
color: "error" as const,
onClick: isDebug ? handleGarbageCollection : undefined,
},
],
[t, parsedData, isDebug, handleGarbageCollection],
);
const statCards = useMemo(() => [
{
icon: <ArrowUpwardRounded fontSize="small" />,
title: t("Upload Speed"),
value: parsedData.up,
unit: `${parsedData.upUnit}/s`,
color: "secondary" as const,
},
{
icon: <ArrowDownwardRounded fontSize="small" />,
title: t("Download Speed"),
value: parsedData.down,
unit: `${parsedData.downUnit}/s`,
color: "primary" as const,
},
{
icon: <LinkRounded fontSize="small" />,
title: t("Active Connections"),
value: parsedData.connectionsCount,
unit: "",
color: "success" as const,
},
{
icon: <CloudUploadRounded fontSize="small" />,
title: t("Uploaded"),
value: parsedData.uploadTotal,
unit: parsedData.uploadTotalUnit,
color: "secondary" as const,
},
{
icon: <CloudDownloadRounded fontSize="small" />,
title: t("Downloaded"),
value: parsedData.downloadTotal,
unit: parsedData.downloadTotalUnit,
color: "primary" as const,
},
{
icon: <MemoryRounded fontSize="small" />,
title: t("Memory Usage"),
value: parsedData.inuse,
unit: parsedData.inuseUnit,
color: "error" as const,
onClick: isDebug ? handleGarbageCollection : undefined,
},
], [t, parsedData, isDebug, handleGarbageCollection]);
return (
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
+30 -48
View File
@@ -78,16 +78,12 @@ const truncateStyle = {
maxWidth: "calc(100% - 28px)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
whiteSpace: "nowrap"
};
// 提取独立组件减少主组件复杂度
const ProfileDetails = ({
current,
onUpdateProfile,
updating,
}: {
current: ProfileItem;
const ProfileDetails = ({ current, onUpdateProfile, updating }: {
current: ProfileItem;
onUpdateProfile: () => void;
updating: boolean;
}) => {
@@ -103,7 +99,7 @@ const ProfileDetails = ({
if (!current.extra || !current.extra.total) return 1;
return Math.min(
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
100,
100
);
}, [current.extra, usedTraffic]);
@@ -113,24 +109,19 @@ const ProfileDetails = ({
{current.url && (
<Stack direction="row" alignItems="center" spacing={1}>
<DnsOutlined fontSize="small" color="action" />
<Typography
variant="body2"
color="text.secondary"
noWrap
sx={{ display: "flex", alignItems: "center" }}
>
<Typography variant="body2" color="text.secondary" noWrap sx={{ display: "flex", alignItems: "center" }}>
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
{current.home ? (
<Link
component="button"
fontWeight="medium"
onClick={() => current.home && openWebUrl(current.home)}
sx={{
sx={{
display: "inline-flex",
alignItems: "center",
minWidth: 0,
maxWidth: "calc(100% - 40px)",
ml: 0.5,
ml: 0.5
}}
title={parseUrl(current.url)}
>
@@ -141,19 +132,14 @@ const ProfileDetails = ({
textOverflow: "ellipsis",
whiteSpace: "nowrap",
minWidth: 0,
flex: 1,
flex: 1
}}
>
{parseUrl(current.url)}
</Typography>
<LaunchOutlined
fontSize="inherit"
sx={{
ml: 0.5,
fontSize: "0.8rem",
opacity: 0.7,
flexShrink: 0,
}}
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }}
/>
</Link>
) : (
@@ -166,7 +152,7 @@ const ProfileDetails = ({
whiteSpace: "nowrap",
minWidth: 0,
flex: 1,
ml: 0.5,
ml: 0.5
}}
title={parseUrl(current.url)}
>
@@ -209,8 +195,7 @@ const ProfileDetails = ({
<Typography variant="body2" color="text.secondary">
{t("Used / Total")}:{" "}
<Box component="span" fontWeight="medium">
{parseTraffic(usedTraffic)} /{" "}
{parseTraffic(current.extra.total)}
{parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)}
</Box>
</Typography>
</Stack>
@@ -255,7 +240,7 @@ const ProfileDetails = ({
// 提取空配置组件
const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
const { t } = useTranslation();
return (
<Box
sx={{
@@ -283,30 +268,27 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
);
};
export const HomeProfileCard = ({
current,
onProfileUpdated,
}: HomeProfileCardProps) => {
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { refreshAll } = useAppData();
// 更新当前订阅
const [updating, setUpdating] = useState(false);
const onUpdateProfile = useLockFn(async () => {
if (!current?.uid) return;
setUpdating(true);
try {
await updateProfile(current.uid, current.option);
showNotice("success", t("Update subscription successfully"), 1000);
showNotice('success', t("Update subscription successfully"), 1000);
onProfileUpdated?.();
// 刷新首页数据
refreshAll();
} catch (err: any) {
showNotice("error", err.message || err.toString(), 3000);
showNotice('error', err.message || err.toString(), 3000);
} finally {
setUpdating(false);
}
@@ -320,9 +302,9 @@ export const HomeProfileCard = ({
// 卡片标题
const cardTitle = useMemo(() => {
if (!current) return t("Profiles");
if (!current.home) return current.name;
return (
<Link
component="button"
@@ -341,19 +323,19 @@ export const HomeProfileCard = ({
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flex: 1,
},
flex: 1
}
}}
title={current.name}
>
<span>{current.name}</span>
<LaunchOutlined
fontSize="inherit"
sx={{
ml: 0.5,
fontSize: "0.8rem",
sx={{
ml: 0.5,
fontSize: "0.8rem",
opacity: 0.7,
flexShrink: 0,
flexShrink: 0
}}
/>
</Link>
@@ -363,7 +345,7 @@ export const HomeProfileCard = ({
// 卡片操作按钮
const cardAction = useMemo(() => {
if (!current) return null;
return (
<Button
variant="outlined"
@@ -385,10 +367,10 @@ export const HomeProfileCard = ({
action={cardAction}
>
{current ? (
<ProfileDetails
current={current}
onUpdateProfile={onUpdateProfile}
updating={updating}
<ProfileDetails
current={current}
onUpdateProfile={onUpdateProfile}
updating={updating}
/>
) : (
<EmptyProfile onClick={goToProfiles} />
+9 -7
View File
@@ -83,28 +83,28 @@ export const IpInfoCard = () => {
// 组件加载时获取IP信息
useEffect(() => {
fetchIpInfo();
// 倒计时实现优化,减少不必要的重渲染
let timer: number | null = null;
let currentCount = IP_REFRESH_SECONDS;
// 只在必要时更新状态,减少重渲染次数
const startCountdown = () => {
timer = window.setInterval(() => {
currentCount -= 1;
if (currentCount <= 0) {
fetchIpInfo();
currentCount = IP_REFRESH_SECONDS;
}
// 每5秒或倒计时结束时才更新UI
if (currentCount % 5 === 0 || currentCount <= 0) {
setCountdown(currentCount);
}
}, 1000);
};
startCountdown();
return () => {
if (timer) clearInterval(timer);
@@ -112,7 +112,7 @@ export const IpInfoCard = () => {
}, [fetchIpInfo]);
const toggleShowIp = useCallback(() => {
setShowIp((prev) => !prev);
setShowIp(prev => !prev);
}, []);
// 渲染加载状态
@@ -282,7 +282,9 @@ export const IpInfoCard = () => {
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
<InfoItem
label={t("Location")}
value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
value={[ipInfo?.city, ipInfo?.region]
.filter(Boolean)
.join(", ")}
/>
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
</Box>
+52 -71
View File
@@ -1,24 +1,19 @@
import { useTranslation } from "react-i18next";
import {
Typography,
Stack,
Divider,
Chip,
IconButton,
Tooltip,
} from "@mui/material";
import {
InfoOutlined,
SettingsOutlined,
WarningOutlined,
import { Typography, Stack, Divider, Chip, IconButton, Tooltip } from "@mui/material";
import {
InfoOutlined,
SettingsOutlined,
WarningOutlined,
AdminPanelSettingsOutlined,
DnsOutlined,
ExtensionOutlined,
ExtensionOutlined
} from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge";
import { EnhancedCard } from "./enhanced-card";
import useSWR from "swr";
import { getSystemInfo } from "@/services/cmds";
import {
getSystemInfo,
} from "@/services/cmds";
import { useNavigate } from "react-router-dom";
import { version as appVersion } from "@root/package.json";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -35,35 +30,32 @@ export const SystemInfoCard = () => {
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
const { installServiceAndRestartCore } = useServiceInstaller();
// 系统信息状态
const [systemState, setSystemState] = useState({
osInfo: "",
lastCheckUpdate: "-",
});
// 系统信息状态
const [systemState, setSystemState] = useState({
osInfo: "",
lastCheckUpdate: "-",
});
// 初始化系统信息
useEffect(() => {
getSystemInfo()
.then((info) => {
const lines = info.split("\n");
if (lines.length > 0) {
const sysName = lines[0].split(": ")[1] || "";
let sysVersion = lines[1].split(": ")[1] || "";
// 初始化系统信息
useEffect(() => {
getSystemInfo()
.then((info) => {
const lines = info.split("\n");
if (lines.length > 0) {
const sysName = lines[0].split(": ")[1] || "";
let sysVersion = lines[1].split(": ")[1] || "";
if (
sysName &&
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
) {
sysVersion = sysVersion.substring(sysName.length).trim();
}
setSystemState((prev) => ({
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) {
sysVersion = sysVersion.substring(sysName.length).trim();
}
})
.catch(console.error);
setSystemState((prev) => ({
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
}
})
.catch(console.error);
// 获取最后检查更新时间
const lastCheck = localStorage.getItem("last_check_update");
@@ -130,6 +122,7 @@ export const SystemInfoCard = () => {
}
}, [verge, patchVerge]);
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
const handleRunningModeClick = useCallback(() => {
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
@@ -142,13 +135,13 @@ export const SystemInfoCard = () => {
try {
const info = await checkUpdate();
if (!info?.available) {
showNotice("success", t("Currently on the Latest Version"));
showNotice('success', t("Currently on the Latest Version"));
} else {
showNotice("info", t("Update Available"), 2000);
showNotice('info', t("Update Available"), 2000);
goToSettings();
}
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice('error', err.message || err.toString());
}
});
@@ -162,15 +155,13 @@ export const SystemInfoCard = () => {
const runningModeStyle = useMemo(
() => ({
// Sidecar或纯管理员模式允许安装服务
cursor:
isSidecarMode || (isAdminMode && isSidecarMode) ? "pointer" : "default",
textDecoration:
isSidecarMode || (isAdminMode && isSidecarMode) ? "underline" : "none",
cursor: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "pointer" : "default",
textDecoration: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "underline" : "none",
display: "flex",
alignItems: "center",
gap: 0.5,
"&:hover": {
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
opacity: (isSidecarMode || (isAdminMode && isSidecarMode)) ? 0.7 : 1,
},
}),
[isSidecarMode, isAdminMode],
@@ -183,34 +174,34 @@ export const SystemInfoCard = () => {
if (!isSidecarMode) {
return (
<>
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")}
/>
<DnsOutlined
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
<DnsOutlined
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
titleAccess={t("Service Mode")}
/>
</>
);
}
return (
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")}
/>
);
} else if (isSidecarMode) {
return (
<ExtensionOutlined
sx={{ color: "info.main", fontSize: 16 }}
<ExtensionOutlined
sx={{ color: "info.main", fontSize: 16 }}
titleAccess={t("Sidecar Mode")}
/>
);
} else {
return (
<DnsOutlined
sx={{ color: "success.main", fontSize: 16 }}
<DnsOutlined
sx={{ color: "success.main", fontSize: 16 }}
titleAccess={t("Service Mode")}
/>
);
@@ -256,19 +247,13 @@ export const SystemInfoCard = () => {
</Typography>
</Stack>
<Divider />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="body2" color="text.secondary">
{t("Auto Launch")}
</Typography>
<Stack direction="row" spacing={1} alignItems="center">
{isAdminMode && (
<Tooltip
title={t("Administrator mode may not support auto launch")}
>
<Tooltip title={t("Administrator mode may not support auto launch")}>
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
</Tooltip>
)}
@@ -283,11 +268,7 @@ export const SystemInfoCard = () => {
</Stack>
</Stack>
<Divider />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="body2" color="text.secondary">
{t("Running Mode")}
</Typography>
+21 -24
View File
@@ -87,12 +87,12 @@ export const TestCard = () => {
}
const newList = testList.map((x) =>
x.uid === uid ? { ...x, ...patch } : x,
x.uid === uid ? { ...x, ...patch } : x
);
mutateVerge({ ...verge, test_list: newList }, false);
},
[testList, verge, mutateVerge],
[testList, verge, mutateVerge]
);
const onDeleteTestListItem = useCallback(
@@ -101,7 +101,7 @@ export const TestCard = () => {
patchVerge({ test_list: newList });
mutateVerge({ ...verge, test_list: newList }, false);
},
[testList, verge, patchVerge, mutateVerge],
[testList, verge, patchVerge, mutateVerge]
);
const onDragEnd = useCallback(
@@ -122,7 +122,7 @@ export const TestCard = () => {
const patchFn = () => {
try {
patchVerge({ test_list: newList });
} catch {}
} catch { }
};
if (window.requestIdleCallback) {
window.requestIdleCallback(patchFn);
@@ -131,7 +131,7 @@ export const TestCard = () => {
}
}
},
[testList, verge, mutateVerge, patchVerge],
[testList, verge, mutateVerge, patchVerge]
);
// 仅在verge首次加载时初始化测试列表
@@ -142,25 +142,22 @@ export const TestCard = () => {
}, [verge, patchVerge]);
// 使用useMemo优化UI内容,减少渲染计算
const renderTestItems = useMemo(
() => (
<Grid container spacing={1} columns={12}>
<SortableContext items={testList.map((x) => x.uid)}>
{testList.map((item) => (
<Grid key={item.uid} size={3}>
<TestItem
id={item.uid}
itemData={item}
onEdit={() => viewerRef.current?.edit(item)}
onDelete={onDeleteTestListItem}
/>
</Grid>
))}
</SortableContext>
</Grid>
),
[testList, onDeleteTestListItem],
);
const renderTestItems = useMemo(() => (
<Grid container spacing={1} columns={12}>
<SortableContext items={testList.map((x) => x.uid)}>
{testList.map((item) => (
<Grid key={item.uid} size={3}>
<TestItem
id={item.uid}
itemData={item}
onEdit={() => viewerRef.current?.edit(item)}
onDelete={onDeleteTestListItem}
/>
</Grid>
))}
</SortableContext>
</Grid>
), [testList, onDeleteTestListItem]);
const handleTestAll = useCallback(() => {
emit("verge://test-all");
+1 -1
View File
@@ -24,7 +24,7 @@ export const UpdateButton = (props: Props) => {
errorRetryCount: 2,
revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour
},
}
);
if (!updateInfo?.available) return null;
+27 -41
View File
@@ -1,9 +1,6 @@
import { useEffect, useMemo } from "react";
import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material";
import {
getCurrentWebviewWindow,
WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { getCurrentWebviewWindow, WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { useSetThemeMode, useThemeMode } from "@/services/states";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { useVerge } from "@/hooks/use-verge";
@@ -54,16 +51,13 @@ export const useCustomTheme = () => {
const timerId = setTimeout(() => {
if (!isMounted) return;
appWindow
.theme()
.then((systemTheme) => {
if (isMounted && systemTheme) {
setMode(systemTheme);
}
})
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
appWindow.theme().then((systemTheme) => {
if (isMounted && systemTheme) {
setMode(systemTheme);
}
}).catch(err => {
console.error("Failed to get initial system theme:", err);
});
}, 0);
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
@@ -75,15 +69,13 @@ export const useCustomTheme = () => {
return () => {
isMounted = false;
clearTimeout(timerId);
unlistenPromise
.then((unlistenFn) => {
if (typeof unlistenFn === "function") {
unlistenFn();
}
})
.catch((err) => {
console.error("Failed to unlisten from theme changes:", err);
});
unlistenPromise.then((unlistenFn) => {
if (typeof unlistenFn === 'function') {
unlistenFn();
}
}).catch(err => {
console.error("Failed to unlisten from theme changes:", err);
});
};
}, [theme_mode, appWindow, setMode]);
@@ -94,10 +86,7 @@ export const useCustomTheme = () => {
if (theme_mode === "system") {
appWindow.setTheme(null).catch((err) => {
console.error(
"Failed to set window theme to follow system (setTheme(null)):",
err,
);
console.error("Failed to set window theme to follow system (setTheme(null)):", err);
});
} else if (mode) {
appWindow.setTheme(mode as TauriOsTheme).catch((err) => {
@@ -164,24 +153,21 @@ export const useCustomTheme = () => {
const rootEle = document.documentElement;
if (rootEle) {
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const dividerColor =
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const dividerColor =
mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";
rootEle.style.setProperty("--divider-color", dividerColor);
rootEle.style.setProperty("--background-color", backgroundColor);
rootEle.style.setProperty("--selection-color", selectColor);
rootEle.style.setProperty("--scroller-color", scrollColor);
rootEle.style.setProperty(
"--primary-main",
muiTheme.palette.primary.main,
);
rootEle.style.setProperty(
rootEle.style.setProperty("--divider-color", dividerColor);
rootEle.style.setProperty("--background-color", backgroundColor);
rootEle.style.setProperty("--selection-color", selectColor);
rootEle.style.setProperty("--scroller-color", scrollColor);
rootEle.style.setProperty("--primary-main", muiTheme.palette.primary.main);
rootEle.style.setProperty(
"--background-color-alpha",
alpha(muiTheme.palette.primary.main, 0.1),
);
);
}
// inject css
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;
onChange?.(prevData.current, currData.current);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice('error', err.message || err.toString());
}
});
@@ -136,7 +136,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
!readOnly && onSave?.(prevData.current, currData.current);
onClose();
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice('error', err.message || err.toString());
}
});
@@ -144,7 +144,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
try {
onClose();
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice('error', err.message || err.toString());
}
});
+2 -2
View File
@@ -70,8 +70,8 @@ export const GroupItem = (props: Props) => {
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete"
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
height: "100%",
margin: "8px 0",
borderRadius: "8px",
+25 -26
View File
@@ -90,27 +90,27 @@ export const GroupsEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo(
() => prependSeq.filter((group) => match(group.name)),
[prependSeq, match],
[prependSeq, match]
);
const filteredGroupList = useMemo(
() => groupList.filter((group) => match(group.name)),
[groupList, match],
[groupList, match]
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((group) => match(group.name)),
[appendSeq, match],
[appendSeq, match]
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
})
);
const reorder = (
list: IProxyGroupConfig[],
startIndex: number,
endIndex: number,
endIndex: number
) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
@@ -188,8 +188,8 @@ export const GroupsEditorViewer = (props: Props) => {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true },
),
{ forceQuotes: true }
)
);
} catch (e) {
// 防止异常导致UI卡死
@@ -226,7 +226,7 @@ export const GroupsEditorViewer = (props: Props) => {
return !moreDeleteProxies.includes(proxy);
}
}),
moreAppendProxies,
moreAppendProxies
);
setProxyPolicyList(
@@ -236,8 +236,8 @@ export const GroupsEditorViewer = (props: Props) => {
.map((group: IProxyGroupConfig) => group.name)
.filter((name) => !deleteSeq.includes(name)) || [],
appendSeq.map((group: IProxyGroupConfig) => group.name),
proxies.map((proxy: any) => proxy.name),
),
proxies.map((proxy: any) => proxy.name)
)
);
};
const fetchProfile = async () => {
@@ -266,7 +266,7 @@ export const GroupsEditorViewer = (props: Props) => {
{},
originProvider,
moreProvider,
globalProvider,
globalProvider
);
setProxyProviderList(Object.keys(provider));
@@ -297,11 +297,11 @@ export const GroupsEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice("success", t("Saved Successfully"));
showNotice('success', t("Saved Successfully"));
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice("error", err.toString());
showNotice('error', err.toString());
}
});
@@ -502,7 +502,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("seconds")}
</InputAdornment>
),
},
}
}}
/>
</Item>
@@ -530,7 +530,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("millis")}
</InputAdornment>
),
},
}
}}
/>
</Item>
@@ -742,7 +742,7 @@ export const GroupsEditorViewer = (props: Props) => {
}
setPrependSeq([formIns.getValues(), ...prependSeq]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice('error', err.message || err.toString());
}
}}
>
@@ -764,7 +764,7 @@ export const GroupsEditorViewer = (props: Props) => {
}
setAppendSeq([...appendSeq, formIns.getValues()]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice('error', err.message || err.toString());
}
}}
>
@@ -811,8 +811,8 @@ export const GroupsEditorViewer = (props: Props) => {
onDelete={() => {
setPrependSeq(
prependSeq.filter(
(v) => v.name !== item.name,
),
(v) => v.name !== item.name
)
);
}}
/>
@@ -838,8 +838,8 @@ export const GroupsEditorViewer = (props: Props) => {
) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredGroupList[newIndex].name,
),
(v) => v !== filteredGroupList[newIndex].name
)
);
} else {
setDeleteSeq((prev) => [
@@ -871,8 +871,8 @@ export const GroupsEditorViewer = (props: Props) => {
onDelete={() => {
setAppendSeq(
appendSeq.filter(
(v) => v.name !== item.name,
),
(v) => v.name !== item.name
)
);
}}
/>
@@ -906,9 +906,8 @@ export const GroupsEditorViewer = (props: Props) => {
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动
}}
+50 -51
View File
@@ -1,58 +1,57 @@
import { alpha, Box, styled } from "@mui/material";
export const ProfileBox = styled(Box)(({
theme,
"aria-selected": selected,
}) => {
const { mode, primary, text } = theme.palette;
const key = `${mode}-${!!selected}`;
export const ProfileBox = styled(Box)(
({ theme, "aria-selected": selected }) => {
const { mode, primary, text } = theme.palette;
const key = `${mode}-${!!selected}`;
const backgroundColor = mode === "light" ? "#ffffff" : "#282A36";
const backgroundColor = mode === "light" ? "#ffffff" : "#282A36";
const color = {
"light-true": text.secondary,
"light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.65),
"dark-false": alpha(text.secondary, 0.65),
}[key]!;
const color = {
"light-true": text.secondary,
"light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.65),
"dark-false": alpha(text.secondary, 0.65),
}[key]!;
const h2color = {
"light-true": primary.main,
"light-false": text.primary,
"dark-true": primary.main,
"dark-false": text.primary,
}[key]!;
const h2color = {
"light-true": primary.main,
"light-false": text.primary,
"dark-true": primary.main,
"dark-false": text.primary,
}[key]!;
const borderSelect = {
"light-true": {
borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
},
"light-false": {
width: "100%",
},
"dark-true": {
borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
},
"dark-false": {
width: "100%",
},
}[key];
const borderSelect = {
"light-true": {
borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
},
"light-false": {
width: "100%",
},
"dark-true": {
borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
},
"dark-false": {
width: "100%",
},
}[key];
return {
position: "relative",
display: "block",
cursor: "pointer",
textAlign: "left",
padding: "8px 16px",
boxSizing: "border-box",
backgroundColor,
...borderSelect,
borderRadius: "8px",
color,
"& h2": { color: h2color },
};
});
return {
position: "relative",
display: "block",
cursor: "pointer",
textAlign: "left",
padding: "8px 16px",
boxSizing: "border-box",
backgroundColor,
...borderSelect,
borderRadius: "8px",
color,
"& h2": { color: h2color },
};
}
);
+14 -47
View File
@@ -75,10 +75,7 @@ export const ProfileItem = (props: Props) => {
// 获取下次更新时间的函数
const fetchNextUpdateTime = useLockFn(async (forceRefresh = false) => {
if (
itemData.option?.update_interval &&
itemData.option.update_interval > 0
) {
if (itemData.option?.update_interval && itemData.option.update_interval > 0) {
try {
console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`);
@@ -100,7 +97,7 @@ export const ProfileItem = (props: Props) => {
setNextUpdateTime(t("Last Update failed"));
} else {
// 否则显示剩余时间
const diffMinutes = nextUpdateDate.diff(now, "minute");
const diffMinutes = nextUpdateDate.diff(now, 'minute');
if (diffMinutes < 60) {
if (diffMinutes <= 0) {
@@ -162,17 +159,11 @@ export const ProfileItem = (props: Props) => {
};
// 只注册定时器更新事件监听
window.addEventListener(
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
window.addEventListener('verge://timer-updated', handleTimerUpdate as EventListener);
return () => {
// 清理事件监听
window.removeEventListener(
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
window.removeEventListener('verge://timer-updated', handleTimerUpdate as EventListener);
};
}, [showNextUpdate, itemData.uid]);
@@ -280,7 +271,7 @@ export const ProfileItem = (props: Props) => {
try {
await viewProfile(itemData.uid);
} catch (err: any) {
showNotice("error", err?.message || err.toString());
showNotice('error', err?.message || err.toString());
}
});
@@ -311,7 +302,7 @@ export const ProfileItem = (props: Props) => {
await updateProfile(itemData.uid, option);
// 更新成功,刷新列表
showNotice("success", t("Update subscription successfully"));
showNotice('success', t("Update subscription successfully"));
mutate("getProfiles");
} catch (err: any) {
// 更新完全失败(包括后端的回退尝试)
@@ -430,25 +421,13 @@ export const ProfileItem = (props: Props) => {
};
// 注册事件监听
window.addEventListener(
"profile-update-started",
handleUpdateStarted as EventListener,
);
window.addEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
window.addEventListener('profile-update-started', handleUpdateStarted as EventListener);
window.addEventListener('profile-update-completed', handleUpdateCompleted as EventListener);
return () => {
// 清理事件监听
window.removeEventListener(
"profile-update-started",
handleUpdateStarted as EventListener,
);
window.removeEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
window.removeEventListener('profile-update-started', handleUpdateStarted as EventListener);
window.removeEventListener('profile-update-completed', handleUpdateCompleted as EventListener);
};
}, [itemData.uid, showNextUpdate]);
@@ -562,23 +541,13 @@ export const ProfileItem = (props: Props) => {
)
)}
{hasUrl && (
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
ml: "auto",
}}
>
<Box sx={{ display: "flex", justifyContent: "flex-end", ml: "auto" }}>
<Typography
noWrap
component="span"
fontSize={14}
textAlign="right"
title={
showNextUpdate
? t("Click to show last update time")
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
}
title={showNextUpdate ? t("Click to show last update time") : `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`}
sx={{
cursor: "pointer",
display: "inline-block",
@@ -587,15 +556,13 @@ export const ProfileItem = (props: Props) => {
"&:hover": {
borderBottomColor: "primary.main",
color: "primary.main",
},
}
}}
onClick={toggleUpdateTimeDisplay}
>
{showNextUpdate
? nextUpdateTime
: updated > 0
? dayjs(updated * 1000).fromNow()
: ""}
: (updated > 0 ? dayjs(updated * 1000).fromNow() : "")}
</Typography>
</Box>
)}
+1 -1
View File
@@ -43,7 +43,7 @@ export const ProfileMore = (props: Props) => {
try {
await viewProfile(id);
} catch (err: any) {
showNotice("error", err?.message || err.toString());
showNotice('error', err?.message || err.toString());
}
});
+1 -1
View File
@@ -201,7 +201,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
setOpen(false);
fileDataRef.current = null;
setTimeout(() => formIns.reset(), 500);
} catch {}
} catch { }
};
const text = {
@@ -66,27 +66,27 @@ export const ProxiesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo(
() => prependSeq.filter((proxy) => match(proxy.name)),
[prependSeq, match],
[prependSeq, match]
);
const filteredProxyList = useMemo(
() => proxyList.filter((proxy) => match(proxy.name)),
[proxyList, match],
[proxyList, match]
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((proxy) => match(proxy.name)),
[appendSeq, match],
[appendSeq, match]
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
})
);
const reorder = (
list: IProxyConfig[],
startIndex: number,
endIndex: number,
endIndex: number
) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
@@ -208,8 +208,8 @@ export const ProxiesEditorViewer = (props: Props) => {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true },
),
{ forceQuotes: true }
)
);
} catch (e) {
// 防止异常导致UI卡死
@@ -232,11 +232,11 @@ export const ProxiesEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice("success", t("Saved Successfully"));
showNotice('success', t("Saved Successfully"));
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice("error", err.toString());
showNotice('error', err.toString());
}
});
@@ -358,8 +358,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => {
setPrependSeq(
prependSeq.filter(
(v) => v.name !== item.name,
),
(v) => v.name !== item.name
)
);
}}
/>
@@ -385,8 +385,8 @@ export const ProxiesEditorViewer = (props: Props) => {
) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredProxyList[newIndex].name,
),
(v) => v !== filteredProxyList[newIndex].name
)
);
} else {
setDeleteSeq((prev) => [
@@ -418,8 +418,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => {
setAppendSeq(
appendSeq.filter(
(v) => v.name !== item.name,
),
(v) => v.name !== item.name
)
);
}}
/>
@@ -453,9 +453,8 @@ export const ProxiesEditorViewer = (props: Props) => {
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动
}}
+2 -2
View File
@@ -49,8 +49,8 @@ export const ProxyItem = (props: Props) => {
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete"
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
height: "100%",
margin: "8px 0",
borderRadius: "8px",
+2 -2
View File
@@ -52,8 +52,8 @@ export const RuleItem = (props: Props) => {
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete"
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
height: "100%",
margin: "8px 0",
borderRadius: "8px",
+180 -182
View File
@@ -55,17 +55,17 @@ interface Props {
const portValidator = (value: string): boolean => {
return new RegExp(
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$",
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$"
).test(value);
};
const ipv4CIDRValidator = (value: string): boolean => {
return new RegExp(
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$",
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$"
).test(value);
};
const ipv6CIDRValidator = (value: string): boolean => {
return new RegExp(
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$",
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$"
).test(value);
};
@@ -76,161 +76,161 @@ const rules: {
noResolve?: boolean;
validator?: (value: string) => boolean;
}[] = [
{
name: "DOMAIN",
example: "example.com",
},
{
name: "DOMAIN-SUFFIX",
example: "example.com",
},
{
name: "DOMAIN-KEYWORD",
example: "example",
},
{
name: "DOMAIN-REGEX",
example: "example.*",
},
{
name: "GEOSITE",
example: "youtube",
},
{
name: "GEOIP",
example: "CN",
noResolve: true,
},
{
name: "SRC-GEOIP",
example: "CN",
},
{
name: "IP-ASN",
example: "13335",
noResolve: true,
validator: (value) => (+value ? true : false),
},
{
name: "SRC-IP-ASN",
example: "9808",
validator: (value) => (+value ? true : false),
},
{
name: "IP-CIDR",
example: "127.0.0.0/8",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-CIDR6",
example: "2620:0:2d0:200::7/32",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-CIDR",
example: "192.168.1.201/32",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-SUFFIX",
example: "8.8.8.8/24",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-SUFFIX",
example: "192.168.1.201/8",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-PORT",
example: "7777",
validator: (value) => portValidator(value),
},
{
name: "DST-PORT",
example: "80",
validator: (value) => portValidator(value),
},
{
name: "IN-PORT",
example: "7890",
validator: (value) => portValidator(value),
},
{
name: "DSCP",
example: "4",
},
{
name: "PROCESS-NAME",
example: getSystem() === "windows" ? "chrome.exe" : "curl",
},
{
name: "PROCESS-PATH",
example:
getSystem() === "windows"
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
: "/usr/bin/wget",
},
{
name: "PROCESS-NAME-REGEX",
example: ".*telegram.*",
},
{
name: "PROCESS-PATH-REGEX",
example:
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
},
{
name: "NETWORK",
example: "udp",
validator: (value) => ["tcp", "udp"].includes(value),
},
{
name: "UID",
example: "1001",
validator: (value) => (+value ? true : false),
},
{
name: "IN-TYPE",
example: "SOCKS/HTTP",
},
{
name: "IN-USER",
example: "mihomo",
},
{
name: "IN-NAME",
example: "ss",
},
{
name: "SUB-RULE",
example: "(NETWORK,tcp)",
},
{
name: "RULE-SET",
example: "providername",
noResolve: true,
},
{
name: "AND",
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
},
{
name: "OR",
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
},
{
name: "NOT",
example: "((DOMAIN,baidu.com))",
},
{
name: "MATCH",
required: false,
},
];
{
name: "DOMAIN",
example: "example.com",
},
{
name: "DOMAIN-SUFFIX",
example: "example.com",
},
{
name: "DOMAIN-KEYWORD",
example: "example",
},
{
name: "DOMAIN-REGEX",
example: "example.*",
},
{
name: "GEOSITE",
example: "youtube",
},
{
name: "GEOIP",
example: "CN",
noResolve: true,
},
{
name: "SRC-GEOIP",
example: "CN",
},
{
name: "IP-ASN",
example: "13335",
noResolve: true,
validator: (value) => (+value ? true : false),
},
{
name: "SRC-IP-ASN",
example: "9808",
validator: (value) => (+value ? true : false),
},
{
name: "IP-CIDR",
example: "127.0.0.0/8",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-CIDR6",
example: "2620:0:2d0:200::7/32",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-CIDR",
example: "192.168.1.201/32",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-SUFFIX",
example: "8.8.8.8/24",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-SUFFIX",
example: "192.168.1.201/8",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-PORT",
example: "7777",
validator: (value) => portValidator(value),
},
{
name: "DST-PORT",
example: "80",
validator: (value) => portValidator(value),
},
{
name: "IN-PORT",
example: "7890",
validator: (value) => portValidator(value),
},
{
name: "DSCP",
example: "4",
},
{
name: "PROCESS-NAME",
example: getSystem() === "windows" ? "chrome.exe" : "curl",
},
{
name: "PROCESS-PATH",
example:
getSystem() === "windows"
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
: "/usr/bin/wget",
},
{
name: "PROCESS-NAME-REGEX",
example: ".*telegram.*",
},
{
name: "PROCESS-PATH-REGEX",
example:
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
},
{
name: "NETWORK",
example: "udp",
validator: (value) => ["tcp", "udp"].includes(value),
},
{
name: "UID",
example: "1001",
validator: (value) => (+value ? true : false),
},
{
name: "IN-TYPE",
example: "SOCKS/HTTP",
},
{
name: "IN-USER",
example: "mihomo",
},
{
name: "IN-NAME",
example: "ss",
},
{
name: "SUB-RULE",
example: "(NETWORK,tcp)",
},
{
name: "RULE-SET",
example: "providername",
noResolve: true,
},
{
name: "AND",
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
},
{
name: "OR",
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
},
{
name: "NOT",
example: "((DOMAIN,baidu.com))",
},
{
name: "MATCH",
required: false,
},
];
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
@@ -260,22 +260,22 @@ export const RulesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo(
() => prependSeq.filter((rule) => match(rule)),
[prependSeq, match],
[prependSeq, match]
);
const filteredRuleList = useMemo(
() => ruleList.filter((rule) => match(rule)),
[ruleList, match],
[ruleList, match]
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((rule) => match(rule)),
[appendSeq, match],
[appendSeq, match]
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
})
);
const reorder = (list: string[], startIndex: number, endIndex: number) => {
const result = Array.from(list);
@@ -333,11 +333,11 @@ export const RulesEditorViewer = (props: Props) => {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true },
),
{ forceQuotes: true }
)
);
} catch (e: any) {
showNotice("error", e?.message || e?.toString() || "YAML dump error");
showNotice('error', e?.message || e?.toString() || 'YAML dump error');
}
};
if (window.requestIdleCallback) {
@@ -371,7 +371,7 @@ export const RulesEditorViewer = (props: Props) => {
return !moreDeleteGroups.includes(group);
}
}),
moreAppendGroups,
moreAppendGroups
);
let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null;
@@ -396,7 +396,7 @@ export const RulesEditorViewer = (props: Props) => {
let globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule);
setProxyPolicyList(
builtinProxyPolicies.concat(groups.map((group: any) => group.name)),
builtinProxyPolicies.concat(groups.map((group: any) => group.name))
);
setRuleSetList(Object.keys(ruleSet));
setSubRuleList(Object.keys(subRule));
@@ -417,20 +417,19 @@ export const RulesEditorViewer = (props: Props) => {
throw new Error(t("Invalid Rule"));
}
const condition = (ruleType.required ?? true) ? ruleContent : "";
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`;
const condition = ruleType.required ?? true ? ruleContent : "";
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`;
};
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice("success", t("Saved Successfully"));
showNotice('success', t("Saved Successfully"));
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice("error", err.toString());
showNotice('error', err.toString());
}
});
@@ -558,7 +557,7 @@ export const RulesEditorViewer = (props: Props) => {
if (prependSeq.includes(raw)) return;
setPrependSeq([raw, ...prependSeq]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice('error', err.message || err.toString());
}
}}
>
@@ -576,7 +575,7 @@ export const RulesEditorViewer = (props: Props) => {
if (appendSeq.includes(raw)) return;
setAppendSeq([...appendSeq, raw]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice('error', err.message || err.toString());
}
}}
>
@@ -622,7 +621,7 @@ export const RulesEditorViewer = (props: Props) => {
ruleRaw={item}
onDelete={() => {
setPrependSeq(
prependSeq.filter((v) => v !== item),
prependSeq.filter((v) => v !== item)
);
}}
/>
@@ -646,8 +645,8 @@ export const RulesEditorViewer = (props: Props) => {
if (deleteSeq.includes(filteredRuleList[newIndex])) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredRuleList[newIndex],
),
(v) => v !== filteredRuleList[newIndex]
)
);
} else {
setDeleteSeq((prev) => [
@@ -678,7 +677,7 @@ export const RulesEditorViewer = (props: Props) => {
ruleRaw={item}
onDelete={() => {
setAppendSeq(
appendSeq.filter((v) => v !== item),
appendSeq.filter((v) => v !== item)
);
}}
/>
@@ -712,9 +711,8 @@ export const RulesEditorViewer = (props: Props) => {
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动
}}
+78 -102
View File
@@ -1,5 +1,5 @@
import { useState } from "react";
import {
import {
Button,
Box,
Dialog,
@@ -15,7 +15,7 @@ import {
LinearProgress,
alpha,
styled,
useTheme,
useTheme
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
@@ -65,83 +65,77 @@ export const ProviderButton = () => {
const [open, setOpen] = useState(false);
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
const [updating, setUpdating] = useState<Record<string, boolean>>({});
// 检查是否有提供者
const hasProviders = Object.keys(proxyProviders || {}).length > 0;
// 更新单个代理提供者
const updateProvider = useLockFn(async (name: string) => {
try {
// 设置更新状态
setUpdating((prev) => ({ ...prev, [name]: true }));
setUpdating(prev => ({ ...prev, [name]: true }));
await proxyProviderUpdate(name);
// 刷新数据
await refreshProxy();
await refreshProxyProviders();
showNotice("success", `${name} 更新成功`);
showNotice('success', `${name} 更新成功`);
} catch (err: any) {
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`);
} finally {
// 清除更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
setUpdating(prev => ({ ...prev, [name]: false }));
}
});
// 更新所有代理提供者
const updateAllProviders = useLockFn(async () => {
try {
// 获取所有provider的名称
const allProviders = Object.keys(proxyProviders || {});
if (allProviders.length === 0) {
showNotice("info", "没有可更新的代理提供者");
showNotice('info', "没有可更新的代理提供者");
return;
}
// 设置所有provider为更新中状态
const newUpdating = allProviders.reduce(
(acc, key) => {
acc[key] = true;
return acc;
},
{} as Record<string, boolean>,
);
const newUpdating = allProviders.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as Record<string, boolean>);
setUpdating(newUpdating);
// 改为串行逐个更新所有provider
for (const name of allProviders) {
try {
await proxyProviderUpdate(name);
// 每个更新完成后更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
setUpdating(prev => ({ ...prev, [name]: false }));
} catch (err) {
console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程
}
}
// 刷新数据
await refreshProxy();
await refreshProxyProviders();
showNotice("success", "全部代理提供者更新成功");
showNotice('success', "全部代理提供者更新成功");
} catch (err: any) {
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
showNotice('error', `更新失败: ${err?.message || err.toString()}`);
} finally {
// 清除所有更新状态
setUpdating({});
}
});
const handleClose = () => {
setOpen(false);
};
if (!hasProviders) return null;
return (
@@ -155,14 +149,15 @@ export const ProviderButton = () => {
>
{t("Proxy Provider")}
</Button>
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<DialogTitle>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">{t("Proxy Provider")}</Typography>
<Box>
<Button
@@ -175,14 +170,14 @@ export const ProviderButton = () => {
</Box>
</Box>
</DialogTitle>
<DialogContent>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(proxyProviders || {}).map(([key, item]) => {
const provider = item as ProxyProviderItem;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
// 订阅信息
const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub;
@@ -190,53 +185,46 @@ export const ProviderButton = () => {
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
// 流量使用进度
const progress =
total > 0
? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
const progress = total > 0
? Math.min(Math.round(((download + upload) * 100) / total) + 1, 100)
: 0;
return (
<ListItem
key={key}
sx={[
{
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
transition: "all 0.2s"
},
({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
const hoverColor = mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
},
}
};
},
}
]}
>
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Box sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<Typography
variant="subtitle1"
component="div"
@@ -244,7 +232,7 @@ export const ProviderButton = () => {
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.proxies.length}
</TypeBox>
@@ -252,14 +240,9 @@ export const ProviderButton = () => {
{provider.vehicleType}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
<Typography variant="body2" color="text.secondary" noWrap>
<small>{t("Update At")}: </small>{time.fromNow()}
</Typography>
</Box>
}
@@ -268,29 +251,26 @@ export const ProviderButton = () => {
{/* 订阅信息 */}
{hasSubInfo && (
<>
<Box
sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Box sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}>
<span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
{parseTraffic(upload + download)} / {parseTraffic(total)}
</span>
<span title={t("Expire Time") as string}>
{parseExpire(expire)}
</span>
</Box>
{/* 进度条 */}
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 6,
sx={{
height: 6,
borderRadius: 3,
opacity: total > 0 ? 1 : 0,
}}
@@ -301,14 +281,12 @@ export const ProviderButton = () => {
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Box sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center"
}}>
<IconButton
size="small"
color="primary"
@@ -317,13 +295,11 @@ export const ProviderButton = () => {
}}
disabled={isUpdating}
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
animation: isUpdating ? "spin 1s linear infinite" : "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
"100%": { transform: "rotate(360deg)" }
}
}}
title={t("Update Provider") as string}
>
@@ -335,7 +311,7 @@ export const ProviderButton = () => {
})}
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t("Close")}
+5 -14
View File
@@ -106,12 +106,10 @@ const LetterItem = memo(
name,
onClick,
getFirstChar,
enableAutoScroll = true,
}: {
name: string;
onClick: (name: string) => void;
getFirstChar: (str: string) => string;
enableAutoScroll?: boolean;
}) => {
const [showTooltip, setShowTooltip] = useState(false);
const letterRef = useRef<HTMLDivElement>(null);
@@ -138,14 +136,11 @@ const LetterItem = memo(
const handleMouseEnter = useCallback(() => {
setShowTooltip(true);
// 只有在启用自动滚动时才触发滚动
if (enableAutoScroll) {
// 添加 100ms 的延迟,避免鼠标快速划过时触发滚动
hoverTimeoutRef.current = setTimeout(() => {
onClick(name);
}, 100);
}
}, [name, onClick, enableAutoScroll]);
// 添加 200ms 的延迟,避免鼠标快速划过时触发滚动
hoverTimeoutRef.current = setTimeout(() => {
onClick(name);
}, 100);
}, [name, onClick]);
const handleMouseLeave = useCallback(() => {
setShowTooltip(false);
@@ -203,9 +198,6 @@ export const ProxyGroups = (props: Props) => {
const { verge } = useVerge();
const { current, patchCurrent } = useProfiles();
// 获取自动滚动开关状态,默认为 true
const enableAutoScroll = verge?.enable_hover_jump_navigator ?? true;
const timeout = verge?.default_latency_timeout || 10000;
const virtuosoRef = useRef<VirtuosoHandle>(null);
@@ -549,7 +541,6 @@ export const ProxyGroups = (props: Props) => {
name={name}
onClick={handleLetterClick}
getFirstChar={getFirstChar}
enableAutoScroll={enableAutoScroll}
/>
))}
</div>
+8 -12
View File
@@ -31,10 +31,7 @@ interface RenderProps {
onLocation: (group: IRenderItem["group"]) => void;
onCheckAll: (groupName: string) => void;
onHeadState: (groupName: string, patch: Partial<HeadState>) => void;
onChangeProxy: (
group: IRenderItem["group"],
proxy: IRenderItem["proxy"] & { name: string },
) => void;
onChangeProxy: (group: IRenderItem["group"], proxy: IRenderItem["proxy"] & { name: string }) => void;
}
export const ProxyRender = (props: RenderProps) => {
@@ -132,15 +129,14 @@ export const ProxyRender = (props: RenderProps) => {
/>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Tooltip title={t("Proxy Count")} arrow>
<Chip
size="small"
label={`${group.all.length}`}
sx={{
mr: 1,
backgroundColor: (theme) =>
alpha(theme.palette.primary.main, 0.1),
<Chip
size="small"
label={`${group.all.length}`}
sx={{
mr: 1,
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.1),
color: (theme) => theme.palette.primary.main,
}}
}}
/>
</Tooltip>
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
+4 -4
View File
@@ -8,7 +8,7 @@ export default function useFilterSort(
proxies: IProxyItem[],
groupName: string,
filterText: string,
sortType: ProxySortType,
sortType: ProxySortType
) {
const [refresh, setRefresh] = useState({});
@@ -40,7 +40,7 @@ export function filterSort(
proxies: IProxyItem[],
groupName: string,
filterText: string,
sortType: ProxySortType,
sortType: ProxySortType
) {
const fp = filterProxies(proxies, groupName, filterText);
const sp = sortProxies(fp, groupName, sortType);
@@ -60,7 +60,7 @@ const regex2 = /type=(.*)/i;
function filterProxies(
proxies: IProxyItem[],
groupName: string,
filterText: string,
filterText: string
) {
if (!filterText) return proxies;
@@ -100,7 +100,7 @@ function filterProxies(
function sortProxies(
proxies: IProxyItem[],
groupName: string,
sortType: ProxySortType,
sortType: ProxySortType
) {
if (!proxies) return [];
if (sortType === 0) return proxies;
+2 -2
View File
@@ -37,7 +37,7 @@ export function useHeadStateNew() {
try {
const data = JSON.parse(
localStorage.getItem(HEAD_STATE_KEY)!,
localStorage.getItem(HEAD_STATE_KEY)!
) as HeadStateStorage;
const value = data[current] || {};
@@ -74,7 +74,7 @@ export function useHeadStateNew() {
return ret;
});
},
[current],
[current]
);
return [state, setHeadState] as const;
+71 -86
View File
@@ -1,9 +1,9 @@
import { useState } from "react";
import {
Button,
Box,
Dialog,
DialogTitle,
import {
Button,
Box,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
IconButton,
@@ -14,7 +14,7 @@ import {
Divider,
alpha,
styled,
useTheme,
useTheme
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
@@ -54,80 +54,74 @@ export const ProviderButton = () => {
// 检查是否有提供者
const hasProviders = Object.keys(ruleProviders || {}).length > 0;
// 更新单个规则提供者
const updateProvider = useLockFn(async (name: string) => {
try {
// 设置更新状态
setUpdating((prev) => ({ ...prev, [name]: true }));
setUpdating(prev => ({ ...prev, [name]: true }));
await ruleProviderUpdate(name);
// 刷新数据
await refreshRules();
await refreshRuleProviders();
showNotice("success", `${name} 更新成功`);
showNotice('success', `${name} 更新成功`);
} catch (err: any) {
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`);
} finally {
// 清除更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
setUpdating(prev => ({ ...prev, [name]: false }));
}
});
// 更新所有规则提供者
const updateAllProviders = useLockFn(async () => {
try {
// 获取所有provider的名称
const allProviders = Object.keys(ruleProviders || {});
if (allProviders.length === 0) {
showNotice("info", "没有可更新的规则提供者");
showNotice('info', "没有可更新的规则提供者");
return;
}
// 设置所有provider为更新中状态
const newUpdating = allProviders.reduce(
(acc, key) => {
acc[key] = true;
return acc;
},
{} as Record<string, boolean>,
);
const newUpdating = allProviders.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as Record<string, boolean>);
setUpdating(newUpdating);
// 改为串行逐个更新所有provider
for (const name of allProviders) {
try {
await ruleProviderUpdate(name);
// 每个更新完成后更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
setUpdating(prev => ({ ...prev, [name]: false }));
} catch (err) {
console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程
}
}
// 刷新数据
await refreshRules();
await refreshRuleProviders();
showNotice("success", "全部规则提供者更新成功");
showNotice('success', "全部规则提供者更新成功");
} catch (err: any) {
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
showNotice('error', `更新失败: ${err?.message || err.toString()}`);
} finally {
// 清除所有更新状态
setUpdating({});
}
});
const handleClose = () => {
setOpen(false);
};
if (!hasProviders) return null;
return (
@@ -140,14 +134,15 @@ export const ProviderButton = () => {
>
{t("Rule Provider")}
</Button>
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<DialogTitle>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">{t("Rule Providers")}</Typography>
<Button
variant="contained"
@@ -158,52 +153,49 @@ export const ProviderButton = () => {
</Button>
</Box>
</DialogTitle>
<DialogContent>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(ruleProviders || {}).map(([key, item]) => {
const provider = item as RuleProviderItem;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
return (
<ListItem
key={key}
sx={[
{
p: 0,
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
transition: "all 0.2s"
},
({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
const hoverColor = mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
borderColor: alpha(primary.main, 0.3),
},
borderColor: alpha(primary.main, 0.3)
}
};
},
}
]}
>
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Box sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<Typography
variant="subtitle1"
component="div"
@@ -211,19 +203,14 @@ export const ProviderButton = () => {
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.ruleCount}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
<Typography variant="body2" color="text.secondary" noWrap>
<small>{t("Update At")}: </small>{time.fromNow()}
</Typography>
</Box>
}
@@ -232,32 +219,30 @@ export const ProviderButton = () => {
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
<TypeBox component="span">{provider.behavior}</TypeBox>
<TypeBox component="span">
{provider.behavior}
</TypeBox>
</Box>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Box sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center"
}}>
<IconButton
size="small"
color="primary"
onClick={() => updateProvider(key)}
disabled={isUpdating}
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
animation: isUpdating ? "spin 1s linear infinite" : "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
"100%": { transform: "rotate(360deg)" }
}
}}
title={t("Update Provider") as string}
>
@@ -269,7 +254,7 @@ export const ProviderButton = () => {
})}
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t("Close")}
@@ -82,21 +82,21 @@ export const BackupConfigViewer = memo(
if (!url) {
urlRef.current?.focus();
showNotice("error", t("WebDAV URL Required"));
showNotice('error', t("WebDAV URL Required"));
throw new Error(t("WebDAV URL Required"));
} else if (!isValidUrl(url)) {
urlRef.current?.focus();
showNotice("error", t("Invalid WebDAV URL"));
showNotice('error', t("Invalid WebDAV URL"));
throw new Error(t("Invalid WebDAV URL"));
}
if (!username) {
usernameRef.current?.focus();
showNotice("error", t("WebDAV URL Required"));
showNotice('error', t("WebDAV URL Required"));
throw new Error(t("Username Required"));
}
if (!password) {
passwordRef.current?.focus();
showNotice("error", t("WebDAV URL Required"));
showNotice('error', t("WebDAV URL Required"));
throw new Error(t("Password Required"));
}
};
@@ -110,11 +110,11 @@ export const BackupConfigViewer = memo(
data.username.trim(),
data.password,
).then(() => {
showNotice("success", t("WebDAV Config Saved"));
showNotice('success', t("WebDAV Config Saved"));
onSaveSuccess();
});
} catch (error) {
showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
showNotice('error', t("WebDAV Config Save Failed", { error }), 3000);
} finally {
setLoading(false);
}
@@ -125,11 +125,11 @@ export const BackupConfigViewer = memo(
try {
setLoading(true);
await createWebdavBackup().then(async () => {
showNotice("success", t("Backup Created"));
showNotice('success', t("Backup Created"));
await onBackupSuccess();
});
} catch (error) {
showNotice("error", t("Backup Failed", { error }));
showNotice('error', t("Backup Failed", { error }));
} finally {
setLoading(false);
}
@@ -61,7 +61,7 @@ export const BackupTableViewer = memo(
const handleRestore = useLockFn(async (filename: string) => {
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();
});
@@ -52,7 +52,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const errorMsg = await changeClashCore(core);
if (errorMsg) {
showNotice("error", errorMsg);
showNotice('error', errorMsg);
setChangingCore(null);
return;
}
@@ -65,7 +65,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
}, 500);
} catch (err: any) {
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 {
setRestarting(true);
await restartCore();
showNotice("success", t(`Clash Core Restarted`));
showNotice('success', t(`Clash Core Restarted`));
setRestarting(false);
} catch (err: any) {
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);
await upgradeCore();
setUpgrading(false);
showNotice("success", t(`Core Version Updated`));
showNotice('success', t(`Core Version Updated`));
} catch (err: any) {
setUpgrading(false);
const errMsg = err.response?.data?.message || err.toString();
const showMsg = errMsg.includes("already using latest version")
? "Already Using Latest Core Version"
: errMsg;
showNotice("error", t(showMsg));
showNotice('error', t(showMsg));
}
});
+249 -269
View File
@@ -11,7 +11,7 @@ import {
ListItem,
ListItemText,
Stack,
TextField,
TextField
} from "@mui/material";
import { useLockFn, useRequest } from "ahooks";
import { forwardRef, useImperativeHandle, useState } from "react";
@@ -26,136 +26,127 @@ interface ClashPortViewerRef {
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
>((props, ref) => {
const { t } = useTranslation();
const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerProps>(
(props, ref) => {
const { t } = useTranslation();
const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
// Mixed Port
const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897,
);
// Mixed Port
const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
);
// 其他端口状态
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
const [socksEnabled, setSocksEnabled] = useState(
verge?.verge_socks_enabled ?? false,
);
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
const [httpEnabled, setHttpEnabled] = useState(
verge?.verge_http_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,
);
// 其他端口状态
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
const [socksEnabled, setSocksEnabled] = useState(verge?.verge_socks_enabled ?? false);
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
const [httpEnabled, setHttpEnabled] = useState(verge?.verge_http_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卡死
const { loading, run: saveSettings } = useRequest(
async (params: { clashConfig: any; vergeConfig: any }) => {
const { clashConfig, vergeConfig } = params;
await Promise.all([patchInfo(clashConfig), patchVerge(vergeConfig)]);
},
{
manual: true,
onSuccess: () => {
setOpen(false);
showNotice("success", t("Port settings saved")); // 调用提示函数
// 添加保存请求,防止GUI卡死
const { loading, run: saveSettings } = useRequest(
async (params: {
clashConfig: any;
vergeConfig: any;
}) => {
const { clashConfig, vergeConfig } = params;
await Promise.all([
patchInfo(clashConfig),
patchVerge(vergeConfig)
]);
},
onError: () => {
showNotice("error", t("Failed to save settings")); // 调用提示函数
{
manual: true,
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),
}));
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 () => {
// 端口冲突检测
const portList = [
mixedPort,
socksEnabled ? socksPort : -1,
httpEnabled ? httpPort : -1,
redirEnabled ? redirPort : -1,
tproxyEnabled ? tproxyPort : -1
].filter(p => p !== -1);
const onSave = useLockFn(async () => {
// 端口冲突检测
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) {
return;
}
if (new Set(portList).size !== portList.length) {
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));
// 验证端口范围
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) {
return;
}
if (!allPortsValid) {
return;
}
// 准备配置数据
const clashConfig = {
"mixed-port": mixedPort,
"socks-port": socksPort,
port: httpPort,
"redir-port": redirPort,
"tproxy-port": tproxyPort
};
// 准备配置数据
const clashConfig = {
"mixed-port": mixedPort,
"socks-port": socksPort,
port: httpPort,
"redir-port": redirPort,
"tproxy-port": tproxyPort,
};
const 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 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,
};
// 提交保存请求
await saveSettings({ clashConfig, vergeConfig });
});
// 提交保存请求
await saveSettings({ clashConfig, vergeConfig });
});
// 优化的数字输入处理
const handleNumericChange =
(setter: (value: number) => void) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
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);
return;
}
@@ -166,201 +157,190 @@ export const ClashPortViewer = forwardRef<
}
};
return (
<BaseDialog
open={open}
title={t("Port Configuration")}
contentSx={{
width: 400,
}}
okBtn={
loading ? (
<Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} />
{t("Saving...")}
</Stack>
) : (
t("Save")
)
}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List sx={{ width: "100%" }}>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<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 } }}
/>
<IconButton
size="small"
onClick={() => setMixedPort(generateRandomPort())}
title={t("Random Port")}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<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" && (
return (
<BaseDialog
open={open}
title={t("Port Configuration")}
contentSx={{
width: 400
}}
okBtn={
loading ? (
<Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} />
{t("Saving...")}
</Stack>
) : t("Save")
}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List sx={{ width: "100%" }}>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Redir Port")}
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={redirPort}
onChange={(e) =>
setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!redirEnabled}
value={mixedPort}
onChange={(e) => setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setRedirPort(generateRandomPort())}
onClick={() => setMixedPort(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 }}
checked={true}
disabled={true}
sx={{ ml: 0.5, opacity: 0.7 }}
/>
</div>
</ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Tproxy Port")}
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={tproxyPort}
onChange={(e) =>
setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!tproxyEnabled}
value={socksPort}
onChange={(e) => setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!socksEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setTproxyPort(generateRandomPort())}
onClick={() => setSocksPort(generateRandomPort())}
title={t("Random Port")}
disabled={!tproxyEnabled}
disabled={!socksEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={tproxyEnabled}
onChange={(_, c) => setTproxyEnabled(c)}
checked={socksEnabled}
onChange={(_, c) => setSocksEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
</List>
</BaseDialog>
);
});
<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 }}>
<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,
Snackbar,
TextField,
Tooltip,
Tooltip
} from "@mui/material";
import { useLockFn } from "ahooks";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
@@ -42,72 +42,58 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置
const onSave = useLockFn(async () => {
if (!controller.trim()) {
showNotice("error", t("Controller address cannot be empty"), 3000);
showNotice('error', t("Controller address cannot be empty"), 3000);
return;
}
if (!secret.trim()) {
showNotice("error", t("Secret cannot be empty"), 3000);
showNotice('error', t("Secret cannot be empty"), 3000);
return;
}
try {
setIsSaving(true);
await patchInfo({ "external-controller": controller, secret });
showNotice("success", t("Configuration saved successfully"), 2000);
showNotice('success', t("Configuration saved successfully"), 2000);
setOpen(false);
} catch (err: any) {
showNotice(
"error",
err.message || t("Failed to save configuration"),
4000,
);
showNotice('error', err.message || t("Failed to save configuration"), 4000);
} finally {
setIsSaving(false);
}
});
// 复制到剪贴板
const handleCopyToClipboard = useLockFn(
async (text: string, type: string) => {
try {
await navigator.clipboard.writeText(text);
setCopySuccess(type);
setTimeout(() => setCopySuccess(null), 2000);
} catch (err) {
showNotice("error", t("Failed to copy"), 2000);
}
},
);
const handleCopyToClipboard = useLockFn(async (text: string, type: string) => {
try {
await navigator.clipboard.writeText(text);
setCopySuccess(type);
setTimeout(() => setCopySuccess(null), 2000);
} catch (err) {
showNotice('error', t("Failed to copy"), 2000);
}
});
return (
<BaseDialog
open={open}
title={t("External Controller")}
contentSx={{ width: 400 }}
okBtn={
isSaving ? (
<Box display="flex" alignItems="center" gap={1}>
<CircularProgress size={16} color="inherit" />
{t("Saving...")}
</Box>
) : (
t("Save")
)
}
okBtn={isSaving ? (
<Box display="flex" alignItems="center" gap={1}>
<CircularProgress size={16} color="inherit" />
{t("Saving...")}
</Box>
) : (
t("Save")
)}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<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")} />
<Box display="flex" alignItems="center" gap={1}>
<TextField
@@ -115,11 +101,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{
width: 175,
opacity: 1,
pointerEvents: "auto",
pointerEvents: 'auto'
}}
value={controller}
placeholder="Required"
onChange={(e) => setController(e.target.value)}
onChange={e => setController(e.target.value)}
disabled={isSaving}
/>
<Tooltip title={t("Copy to clipboard")}>
@@ -135,13 +121,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
</Box>
</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")} />
<Box display="flex" alignItems="center" gap={1}>
<TextField
@@ -149,11 +129,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{
width: 175,
opacity: 1,
pointerEvents: "auto",
pointerEvents: 'auto'
}}
value={secret}
placeholder={t("Recommended")}
onChange={(e) => setSecret(e.target.value)}
onChange={e => setSecret(e.target.value)}
disabled={isSaving}
/>
<Tooltip title={t("Copy to clipboard")}>
@@ -173,12 +153,13 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<Snackbar
open={copySuccess !== null}
autoHideDuration={2000}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
>
<Alert severity="success">
{copySuccess === "controller"
? t("Controller address copied to clipboard")
: t("Secret copied to clipboard")}
: t("Secret copied to clipboard")
}
</Alert>
</Snackbar>
</BaseDialog>
+29 -45
View File
@@ -59,13 +59,7 @@ const DEFAULT_DNS_CONFIG = {
"*.msftncsi.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: [
"8.8.8.8",
"https://doh.pub/dns-query",
@@ -76,7 +70,7 @@ const DEFAULT_DNS_CONFIG = {
"proxy-server-nameserver": [
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query",
"tls://223.5.5.5",
"tls://223.5.5.5"
],
"direct-nameserver": [],
"direct-nameserver-follow-policy": false,
@@ -225,7 +219,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
useSystemHosts:
dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
dnsConfig["use-system-hosts"] ??
DEFAULT_DNS_CONFIG["use-system-hosts"],
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter:
dnsConfig["fake-ip-filter"]?.join(", ") ??
@@ -234,8 +229,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig.nameserver?.join(", ") ??
DEFAULT_DNS_CONFIG.nameserver.join(", "),
fallback:
dnsConfig.fallback?.join(", ") ??
DEFAULT_DNS_CONFIG.fallback.join(", "),
dnsConfig.fallback?.join(", ") ?? DEFAULT_DNS_CONFIG.fallback.join(", "),
defaultNameserver:
dnsConfig["default-nameserver"]?.join(", ") ??
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
@@ -305,8 +299,9 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
// 从表单值更新YAML内容
const updateYamlFromValues = () => {
const config: Record<string, any> = {};
const config: Record<string, any> = {};
const dnsConfig = generateDnsConfig();
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig;
@@ -316,7 +311,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
if (Object.keys(hosts).length > 0) {
config.hosts = hosts;
}
setYamlContent(yaml.dump(config, { forceQuotes: true }));
};
@@ -325,10 +320,10 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
try {
const parsedYaml = yaml.load(yamlContent) as any;
if (!parsedYaml) return;
updateValuesFromConfig(parsedYaml);
} catch (err: any) {
showNotice("error", t("Invalid YAML format"));
showNotice('error', t("Invalid YAML format"));
}
};
@@ -510,7 +505,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig;
}
const hosts = parseHosts(values.hosts);
if (Object.keys(hosts).length > 0) {
config.hosts = hosts;
@@ -526,41 +521,30 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置
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) {
let cleanErrorMsg = errorMsg;
// 提取关键错误信息
if (errorMsg.includes("level=error")) {
const errorLines = errorMsg
.split("\n")
.filter(
(line) =>
line.includes("level=error") ||
line.includes("level=fatal") ||
line.includes("failed"),
);
const errorLines = errorMsg.split('\n').filter(line =>
line.includes("level=error") ||
line.includes("level=fatal") ||
line.includes("failed")
);
if (errorLines.length > 0) {
cleanErrorMsg = errorLines
.map((line) => {
const msgMatch = line.match(/msg="([^"]+)"/);
return msgMatch ? msgMatch[1] : line;
})
.join(", ");
cleanErrorMsg = errorLines.map(line => {
const msgMatch = line.match(/msg="([^"]+)"/);
return msgMatch ? msgMatch[1] : line;
}).join(", ");
}
}
showNotice(
"error",
t("DNS configuration error") + ": " + cleanErrorMsg,
);
showNotice('error', t("DNS configuration error") + ": " + cleanErrorMsg);
return;
}
@@ -571,9 +555,9 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
}
setOpen(false);
showNotice("success", t("DNS settings saved"));
showNotice('success', t("DNS settings saved"));
} 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);
} catch (err: any) {
showNotice("error", err.toString());
showNotice('error', err.toString());
}
});
+1 -26
View File
@@ -12,7 +12,6 @@ import {
} from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Switch } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { GuardState } from "./guard-state";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { convertFileSrc } from "@tauri-apps/api/core";
@@ -89,7 +88,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const onSwitchFormat = (_e: any, value: boolean) => value;
const onError = (err: any) => {
showNotice("error", err.message || err.toString());
showNotice('error', err.message || err.toString());
};
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
@@ -148,30 +147,6 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
</GuardState>
</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>
<ListItemText primary={t("Nav Icon")} />
<GuardState

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