feat: add rustfmt configuration and CI workflow for code formatting

refactor: streamline formatting workflow by removing unused taplo steps and clarifying directory change

refactor: remove unnecessary directory change step in formatting workflow
This commit is contained in:
Tunglies
2025-06-06 21:11:14 +08:00
parent 689042df60
commit 09969d95de
89 changed files with 2630 additions and 2008 deletions

View File

@@ -55,4 +55,4 @@ body:
label: 软件版本 / Verge Version label: 软件版本 / Verge Version
description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using
validations: validations:
required: true required: true

51
.github/workflows/fmt.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
name: Check Formatting
on:
pull_request:
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: install Rust stable and rustfmt
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: run cargo fmt
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm i -g --force corepack
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- run: pnpm i --frozen-lockfile
- run: pnpm format:check
# taplo:
# name: taplo (.toml files)
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: install Rust stable
# uses: dtolnay/rust-toolchain@stable
# - name: install taplo-cli
# uses: taiki-e/install-action@v2
# with:
# tool: taplo-cli
# - run: taplo fmt --check --diff

View File

@@ -2,15 +2,23 @@
#pnpm pretty-quick --staged #pnpm pretty-quick --staged
# 运行 clippy fmt if git diff --cached --name-only | grep -q '^src/'; then
cd src-tauri pnpm format:check
cargo fmt if [ $? -ne 0 ]; then
echo "Code format check failed in src/. Please fix formatting issues."
if [ $? -ne 0 ]; then exit 1
echo "rustfmt failed to format the code. Please fix the issues and try again." fi
exit 1 fi
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cd src-tauri
cargo fmt
if [ $? -ne 0 ]; then
echo "rustfmt failed to format the code. Please fix the issues and try again."
exit 1
fi
cd ..
fi fi
cd ..
git add . git add .

7
.prettierignore Normal file
View File

@@ -0,0 +1,7 @@
# README.md
# UPDATELOG.md
# CONTRIBUTING.md
pnpm-lock.yaml
src-tauri/target/

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"experimentalOperatorPosition": "start"
}

View File

@@ -33,12 +33,15 @@ npm install pnpm -g
``` ```
### Install Dependencies ### Install Dependencies
Install node packages Install node packages
```shell ```shell
pnpm install pnpm install
``` ```
Install apt packages ONLY for Ubuntu Install apt packages ONLY for Ubuntu
```shell ```shell
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
``` ```
@@ -105,20 +108,25 @@ pnpm portable
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks. If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
1. Code style formatting 1. Code quailty checks
```bash ```bash
# For rust backend
$ clash-verge-rev: pnpm clippy
# For frontend (not yet).
```
2. Code style formatting
```bash
# For rust backend
$ clash-verge-rev: cd src-tauri $ clash-verge-rev: cd src-tauri
$ clash-verge-rev/src-tauri: cargo fmt $ clash-verge-rev/src-tauri: cargo fmt
# For frontend
$ clash-verge-rev: pnpm format:check
$ clash-verge-rev: pnpm format
``` ```
2. Code quailty checks
```bash
$ clash-verge-rev: pnpm clippy
```
Once you have made your changes: Once you have made your changes:
1. Fork the repository. 1. Fork the repository.

View File

@@ -23,13 +23,13 @@ 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) | | 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)| | 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)| | 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/) 查看
--- ---
@@ -49,11 +49,12 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
- 解锁流媒体及 ChatGPT - 解锁流媒体及 ChatGPT
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6) - 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持, #### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器! 感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
🧩 YXVM 独立服务器优势: 🧩 YXVM 独立服务器优势:
- 🌎 优质网络,回程优化,下载快到飞起 - 🌎 优质网络,回程优化,下载快到飞起
- 🔧 物理机独享资源非VPS可比性能拉满 - 🔧 物理机独享资源非VPS可比性能拉满
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用 - 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用

View File

@@ -3,141 +3,153 @@
尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。 尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。
#### ⚠️ 已知问题 #### ⚠️ 已知问题
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试不保证其他其他Linux发行版可用将在未来做进一步适配和调优
- MacOS 下 墙贴主要为浅色Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡 - 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试不保证其他其他Linux发行版可用将在未来做进一步适配和调优
- 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆。 - MacOS 下 墙贴主要为浅色Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- MacOS 下卸载服务后需手动重启软件才能与内核通信 - 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆
- MacOS 下卸载服务后需手动重启软件才能与内核通信。
### 2.3.0 相对于 2.2.3 ### 2.3.0 相对于 2.2.3
#### 🐞 修复问题 #### 🐞 修复问题
- 首页"代理模式"快速切换导致的卡死问题
- 解锁测试报错信息 - 首页"代理模式"快速切换导致的卡死问题
- Macos 快捷键关闭窗口无法启用自动轻量模式 - 解锁测试报错信息
- 静默启动异常窗口创建和关闭流程 - Macos 快捷键关闭窗口无法启用自动轻量模式
- Windows 错误的全局快捷键 `Ctrl+Q` 注册 - 静默启动异常窗口创建和关闭流程
- Vless URL 解码时网络类型错误 - Windows 错误的全局快捷键 `Ctrl+Q` 注册
- 切换自定义代理地址导致系统代理状态异常 - Vless URL 解码时网络类型错误
- Macos TUN 默认无效网卡名称 - 切换自定义代理地址导致系统代理状态异常
- 托盘更改订阅后 UI 不同步的问题 - Macos TUN 默认无效网卡名称
- 修复提权漏洞,改用带认证的 IPC 通信 - 托盘更改订阅后 UI 不同步的问题
- 编辑器中连字符问题 - 修复提权漏洞,改用带认证的 IPC 通信
- 安装服务模式后无法立即开启 TUN 模式 - 编辑器中连字符问题
- 同步更新多语言翻译 - 安装服务模式后无法立即开启 TUN 模式
- 修复 .window-state.json 无法删除的问题 - 同步更新多语言翻译
- 无法修改配置更新 HTTP 请求超时 - 修复 .window-state.json 无法删除的问题
- 修复 getDelayFix 钩子问题 - 无法修改配置更新 HTTP 请求超时
- 使用外部扩展脚本覆写代理组时首页无法显示代理组 - 修复 getDelayFix 钩子问题
- 导出诊断 Verge 版本与设置页面不同步 - 使用外部扩展脚本覆写代理组时首页无法显示代理组
- 切换语言时可能造成设置页面无法加载 - 导出诊断 Verge 版本与设置页面不同步
- 切换语言时可能造成设置页面无法加载
#### ✨ 新增功能 #### ✨ 新增功能
- Mihomo(Meta)内核升级至 1.19.10
- 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好 - Mihomo(Meta)内核升级至 1.19.10
- 关闭系统代理时关闭已建立的网络连接 - 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好
- 托盘显示当前轻量模式状态 - 关闭系统代理时关闭已建立的网络连接
- Webdav 请求加入 UA - 托盘显示当前轻量模式状态
- Webdav 支持目录重定向 - Webdav 请求加入 UA
- Webdav 备份目录检查和文件上传重试机制 - Webdav 支持目录重定向
- 系统代理守卫可检查意外设置变更并恢复 - Webdav 备份目录检查和文件上传重试机制
- 定时自动订阅更新也能自动回退使用代理 - 系统代理守卫可检查意外设置变更并恢复
- 订阅请求超时机制,防止订阅更新的时候卡死 - 定时自动订阅更新也能自动回退使用代理
- 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示 - 订阅请求超时机制,防止订阅更新的时候卡死
- 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死 - 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示
- 更新依赖,替换弃用元素 - 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死
- 首页当前节点增加排序功能 - 更新依赖,替换弃用元素
- DNS 覆写下增加 Hosts 设置功能 - 首页当前节点增加排序功能
- 修复服务模式安装后无法立即开启 TUN 模式的问题 - DNS 覆写下增加 Hosts 设置功能
- 支持手动卸载服务模式,回退到 Sidecar 模式 - 修复服务模式安装后无法立即开启 TUN 模式的问题
- 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持 - 支持手动卸载服务模式,回退到 Sidecar 模式
- 卸载服务的按钮 - 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持
- 添加了Zashboard的一键跳转URL - 卸载服务的按钮
- 使用操作系统默认的窗口管理器 - 添加了Zashboard的一键跳转URL
- 切换、升级、重启内核的状态管理 - 使用操作系统默认的窗口管理
- 更精细化控制自动日志清理新增1天选项 - 切换、升级、重启内核的状态管理
- Winodws 快捷键名称改为 `Clash Verge` - 更精细化控制自动日志清理新增1天选项
- 配置加载阶段自动补全 external-controller secret 字段。 - Winodws 快捷键名称改为 `Clash Verge`
- 配置加载阶段自动补全 external-controller secret 字段。
#### 🚀 优化改进 #### 🚀 优化改进
- 系统代理 Bypass 设置
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题 - 系统代理 Bypass 设置
- 切换到规则页面时自动刷新规则数据 - Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新 - 切换到规则页面时自动刷新规则数据
- 编辑非激活订阅的时候不在触发当前订阅配置重载 - 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新
- 改进核心功能防止主进程阻塞、改进MihomoManager实现以及优化窗口创建流程 - 编辑非激活订阅的时候不在触发当前订阅配置重载
- 优化系统代理设置更新逻辑 - 改进核心功能防止主进程阻塞、改进MihomoManager实现以及优化窗口创建流程
- 重构前端通知系统分离通知线程防止前端卡死 - 优化系统代理设置更新逻辑
- 优化网络请求和错误处理 - 重构前端通知系统分离通知线程防止前端卡死
- 重构通知系统 - 优化网络请求和错误处理
- 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题 - 重构通知系统
- MacOS 下默认关闭托盘速率显示 - 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题
- 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验 - MacOS 下默认关闭托盘速率显示
- 优化了其他语言的翻译问题 - 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验
- Mihomo 内核默认日志等级为 warn - 优化了其他语言的翻译问题
- Clash Verge Rev 应用默认日志等级为 warn - Mihomo 内核默认日志等级为 warn
- 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题 - Clash Verge Rev 应用默认日志等级为 warn
- 对轮询检测机制进行了优化,引入洗牌算法来增强随机性 - 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题
-获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息 -轮询检测机制进行了优化,引入洗牌算法来增强随机性
- 优化窗口状态初始化逻辑和添加缺失的权限设置 - 对获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息
- 异步化配置:优化端口查找和配置保存逻辑 - 优化窗口状态初始化逻辑和添加缺失的权限设置
- 重构事件通知机制到独立线程,避免前端卡死 - 异步化配置:优化端口查找和配置保存逻辑
- 优化端口设置,每个端口可随机设置端口号 - 重构事件通知机制到独立线程,避免前端卡死
- 优化了保存机制,使用平滑函数,防止客户端卡死 - 优化端口设置,每个端口可随机设置端口号
- 优化端口设置退出和保存机制 - 优化了保存机制,使用平滑函数,防止客户端卡死
- 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖 - 优化端口设置退出和保存机制
- 修改 端口检测范围 1111-65536 - 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖
- 配置文件缺失 secret 字段时自动填充默认值 set-your-secret - 修改 端口检测范围 1111-65536
- 优化异步处理,防止部分组件 UI 阻塞 - 配置文件缺失 secret 字段时自动填充默认值 set-your-secret
- 关闭 DNS 启用 - 优化异步处理,防止部分组件 UI 阻塞
- 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204 - 关闭 DNS 启用
- 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204
#### 🗑️ 移除内容 #### 🗑️ 移除内容
- 窗口状态管理器
- Webdav 跨平台备份恢复限制 - 窗口状态管理器
- Webdav 跨平台备份恢复限制
## v2.2.3 ## v2.2.3
#### 已知问题 #### 已知问题
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试不保证其他其他Linux发行版可用将在未来做进一步适配和调优
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸可能会导致不正常图标和速率间隙 - 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试不保证其他其他Linux发行版可用将在未来做进一步适配和调优
- MacOS 下 墙贴主要为浅色Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡 - MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸可能会导致不正常图标和速率间隙
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS - MacOS 下 墙贴主要为浅色Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
### 2.2.3 相对于 2.2.2 ### 2.2.3 相对于 2.2.2
#### 修复了: #### 修复了:
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- “开机自启”和“DNS覆写”开关跳动问题 - 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- 自定义托盘图标未能应用更改 - “开机自启”和“DNS覆写”开关跳动问题
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大 - 自定义托盘图标未能应用更改
- MacOS 托盘速率显示不全 - MacOS 自定义托盘图标显示速率时图标和文本间隙过大
- Linux 在系统服务模式下无法拉起 Mihomo 内核 - MacOS 托盘速率显示不全
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃 - Linux 在系统服务模式下无法拉起 Mihomo 内核
- 相同节点名称可能导致的页面渲染出错 - 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
- URL Schemes被截断的问题 - 相同节点名称可能导致的页面渲染出错
- 首页流量统计卡更好的时间戳范围 - URL Schemes被截断的问题
- 静默启动无法触发自动轻量化计时器 - 首页流量统计卡更好的时间戳范围
- 静默启动无法触发自动轻量化计时器
#### 新增了: #### 新增了:
- Mihomo(Meta)内核升级至 1.19.4
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限 - Mihomo(Meta)内核升级至 1.19.4
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务 - Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置 - 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启 - 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
- 代理组显示节点数量 - 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
- 统一运行模式检测支持管理员模式下开启TUN模式 - 代理组显示节点数量
- 托盘切换代理模式会根据设置自动断开之前连接 - 统一运行模式检测支持管理员模式下开启TUN模式
- 如订阅获取失败回退使用Clash内核代理再次尝试 - 托盘切换代理模式会根据设置自动断开之前连接
- 如订阅获取失败回退使用Clash内核代理再次尝试
#### 移除了: #### 移除了:
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
#### 优化了: #### 优化了:
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 前端统一刷新应用数据,优化数据获取和刷新逻辑 - 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 优化首页流量图表代码,调整图表文字边距 - 前端统一刷新应用数据,优化数据获取和刷新逻辑
- MacOS 托盘速率更好的显示样式和更新逻辑 - 优化首页流量图表代码,调整图表文字边距
- 首页仅在有流量图表时显示流量图表区域 - MacOS 托盘速率更好的显示样式和更新逻辑
- 更新DNS默认覆写配置 - 首页仅在有流量图表时显示流量图表区域
- 移除测试目录,简化资源初始化逻辑 - 更新DNS默认覆写配置
- 移除测试目录,简化资源初始化逻辑
## v2.2.2 ## v2.2.2
@@ -148,23 +160,29 @@
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验DNS 覆写功能增强网络控制能力解锁测试页面助力内容访问自由度提升轻量模式提供灵活使用选择。此外macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。 代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验DNS 覆写功能增强网络控制能力解锁测试页面助力内容访问自由度提升轻量模式提供灵活使用选择。此外macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
#### 已知问题 #### 已知问题
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试不保证其他其他Linux发行版可用将在未来做进一步适配和调优
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试不保证其他其他Linux发行版可用将在未来做进一步适配和调优
### 2.2.2 相对于 2.2.1(已下架不再提供) ### 2.2.2 相对于 2.2.1(已下架不再提供)
#### 修复了: #### 修复了:
- 弹黑框的问题(原因是服务崩溃触发重装机制)
- MacOS进入轻量模式以后隐藏Dock图标 - 弹黑框的问题(原因是服务崩溃触发重装机制)
- 增加轻量模式缺失的tray翻译 - MacOS进入轻量模式以后隐藏Dock图标
- Linux下的窗口边框被削掉的问题 - 增加轻量模式缺失的tray翻译
- Linux下的窗口边框被削掉的问题
#### 新增了: #### 新增了:
- 加强服务检测和重装逻辑
- 增强内核与服务保活机制 - 加强服务检测和重装逻辑
-加服务模式下的僵尸进程清理机制 -强内核与服务保活机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式 - 增加服务模式下的僵尸进程清理机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
### 2.2.1 相对于 2.2.0(已下架不再提供) ### 2.2.1 相对于 2.2.0(已下架不再提供)
#### 修复了: #### 修复了:
1. **首页** 1. **首页**
- 修复 Direct 模式首页无法渲染 - 修复 Direct 模式首页无法渲染
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出 - 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
@@ -181,6 +199,7 @@
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。 - 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
#### 新增了: #### 新增了:
1. **首页** 1. **首页**
- 首页文本过长自动截断 - 首页文本过长自动截断
2. **轻量模式** 2. **轻量模式**
@@ -197,7 +216,9 @@
## 2.2.0(已下架不再提供) ## 2.2.0(已下架不再提供)
#### 新增功能 #### 新增功能
1. **首页** 1. **首页**
- 新增首页功能,默认启动页面改为首页。 - 新增首页功能,默认启动页面改为首页。
- 首页流量图卡片显示上传/下载名称。 - 首页流量图卡片显示上传/下载名称。
- 首页支持轻量模式切换。 - 首页支持轻量模式切换。
@@ -205,17 +226,21 @@
- 限制首页配置文件卡片URL长度。 - 限制首页配置文件卡片URL长度。
2. **DNS 设置与覆写** 2. **DNS 设置与覆写**
- 新增 DNS 覆写功能。 - 新增 DNS 覆写功能。
- 默认启用 DNS 覆写。 - 默认启用 DNS 覆写。
3. **解锁测试** 3. **解锁测试**
- 新增解锁测试页面。 - 新增解锁测试页面。
4. **轻量模式** 4. **轻量模式**
- 新增轻量模式及设置。 - 新增轻量模式及设置。
- 添加自动轻量模式定时器。 - 添加自动轻量模式定时器。
5. **系统支持** 5. **系统支持**
- Mihomo(meta)内核升级 1.19.3 - Mihomo(meta)内核升级 1.19.3
- macOS 支持 CMD+W 关闭窗口。 - macOS 支持 CMD+W 关闭窗口。
- 新增 macOS 应用菜单。 - 新增 macOS 应用菜单。
@@ -228,7 +253,9 @@
- 新增代理命令。 - 新增代理命令。
#### 修复 #### 修复
1. **系统** 1. **系统**
- 修复 Windows 热键崩溃。 - 修复 Windows 热键崩溃。
- 修复 macOS 无框标题。 - 修复 macOS 无框标题。
- 修复 macOS 静默启动崩溃。 - 修复 macOS 静默启动崩溃。
@@ -241,7 +268,9 @@
- 修复构建失败问题。 - 修复构建失败问题。
#### 优化 #### 优化
1. **性能** 1. **性能**
- 重构后端,巨幅性能优化。 - 重构后端,巨幅性能优化。
- 优化首页组件性能。 - 优化首页组件性能。
- 优化流量图表资源使用。 - 优化流量图表资源使用。
@@ -254,6 +283,7 @@
- 优化修改verge配置性能。 - 优化修改verge配置性能。
2. **重构** 2. **重构**
- 重构后端,巨幅性能优化。 - 重构后端,巨幅性能优化。
- 优化定时器管理。 - 优化定时器管理。
- 重构 MihomoManager 处理流量。 - 重构 MihomoManager 处理流量。

View File

@@ -21,7 +21,9 @@
"publish-version": "node scripts/publish-version.mjs", "publish-version": "node scripts/publish-version.mjs",
"prepare": "husky", "prepare": "husky",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml", "fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml" "clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"format": "prettier --write .",
"format:check": "prettier --check ."
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",

View File

@@ -1,21 +1,21 @@
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.resolve(__dirname, '../src/locales'); const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const SRC_DIRS = [ const SRC_DIRS = [
path.resolve(__dirname, '../src'), path.resolve(__dirname, "../src"),
path.resolve(__dirname, '../src-tauri') path.resolve(__dirname, "../src-tauri"),
]; ];
const exts = ['.js', '.ts', '.tsx', '.jsx', '.vue', '.rs']; const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
// 递归获取所有文件 // 递归获取所有文件
function getAllFiles(dir, exts) { function getAllFiles(dir, exts) {
let files = []; let files = [];
fs.readdirSync(dir).forEach(file => { fs.readdirSync(dir).forEach((file) => {
const full = path.join(dir, file); const full = path.join(dir, file);
if (fs.statSync(full).isDirectory()) { if (fs.statSync(full).isDirectory()) {
files = files.concat(getAllFiles(full, exts)); files = files.concat(getAllFiles(full, exts));
@@ -28,21 +28,21 @@ function getAllFiles(dir, exts) {
// 读取所有源码内容为一个大字符串 // 读取所有源码内容为一个大字符串
function getAllSourceContent() { function getAllSourceContent() {
const files = SRC_DIRS.flatMap(dir => getAllFiles(dir, exts)); const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
return files.map(f => fs.readFileSync(f, 'utf8')).join('\n'); return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
} }
// 白名单 key不检查这些 key 是否被使用 // 白名单 key不检查这些 key 是否被使用
const WHITELIST_KEYS = [ const WHITELIST_KEYS = [
'theme.light', "theme.light",
'theme.dark', "theme.dark",
'theme.system', "theme.system",
"Already Using Latest Core Version" "Already Using Latest Core Version",
]; ];
// 主流程 // 主流程
function processI18nFile(i18nPath, lang, allSource) { function processI18nFile(i18nPath, lang, allSource) {
const i18n = JSON.parse(fs.readFileSync(i18nPath, 'utf8')); const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
const keys = Object.keys(i18n); const keys = Object.keys(i18n);
const used = {}; const used = {};
@@ -50,7 +50,7 @@ function processI18nFile(i18nPath, lang, allSource) {
let checked = 0; let checked = 0;
const total = keys.length; const total = keys.length;
keys.forEach(key => { keys.forEach((key) => {
if (WHITELIST_KEYS.includes(key)) { if (WHITELIST_KEYS.includes(key)) {
used[key] = i18n[key]; used[key] = i18n[key];
} else { } else {
@@ -65,8 +65,10 @@ function processI18nFile(i18nPath, lang, allSource) {
checked++; checked++;
if (checked % 20 === 0 || checked === total) { if (checked % 20 === 0 || checked === total) {
const percent = ((checked / total) * 100).toFixed(1); const percent = ((checked / total) * 100).toFixed(1);
process.stdout.write(`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`); process.stdout.write(
if (checked === total) process.stdout.write('\n'); `\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
);
if (checked === total) process.stdout.write("\n");
} }
}); });
@@ -74,25 +76,27 @@ function processI18nFile(i18nPath, lang, allSource) {
console.log(`\n[${lang}] Unused keys:`, unused); console.log(`\n[${lang}] Unused keys:`, unused);
// 备份原文件 // 备份原文件
const oldPath = i18nPath + '.old'; const oldPath = i18nPath + ".old";
fs.renameSync(i18nPath, oldPath); fs.renameSync(i18nPath, oldPath);
// 写入精简后的 i18n 文件(保留原文件名) // 写入精简后的 i18n 文件(保留原文件名)
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), 'utf8'); fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
console.log(`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`); console.log(
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
);
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`); console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
} }
function main() { function main() {
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等 // 支持 zhtw.json、zh-tw.json、zh_CN.json 等
const files = fs.readdirSync(LOCALES_DIR).filter(f => const files = fs
/^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith('.old') .readdirSync(LOCALES_DIR)
); .filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
const allSource = getAllSourceContent(); const allSource = getAllSourceContent();
files.forEach(file => { files.forEach((file) => {
const lang = path.basename(file, '.json'); const lang = path.basename(file, ".json");
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource); processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
}); });
} }
main(); main();

View File

@@ -38,7 +38,9 @@ async function run() {
let tag = null; let tag = null;
if (versionArg === "alpha") { if (versionArg === "alpha") {
// 读取 package.json 里的主版本 // 读取 package.json 里的主版本
const pkg = await import(path.join(rootDir, "package.json"), { assert: { type: "json" } }); const pkg = await import(path.join(rootDir, "package.json"), {
assert: { type: "json" },
});
tag = `v${pkg.default.version}-alpha`; tag = `v${pkg.default.version}-alpha`;
} else if (isSemver(versionArg)) { } else if (isSemver(versionArg)) {
// 1.2.3 或 v1.2.3 // 1.2.3 或 v1.2.3
@@ -61,4 +63,4 @@ async function run() {
} }
} }
run(); run();

View File

@@ -1,5 +1,3 @@
/** /**
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json. * CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
* *
@@ -51,7 +49,9 @@ function generateShortTimestamp() {
* @returns {boolean} * @returns {boolean}
*/ */
function isValidVersion(version) { function isValidVersion(version) {
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(version); return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
version,
);
} }
/** /**
@@ -69,8 +69,8 @@ function normalizeVersion(version) {
* @returns {string} * @returns {string}
*/ */
function getBaseVersion(version) { function getBaseVersion(version) {
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, ''); let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, ''); base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
return base; return base;
} }
@@ -85,10 +85,21 @@ async function updatePackageVersion(newVersion) {
const data = await fs.readFile(packageJsonPath, "utf8"); const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data); const packageJson = JSON.parse(data);
console.log("[INFO]: Current package.json version is: ", packageJson.version); console.log(
packageJson.version = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; "[INFO]: Current package.json version is: ",
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf8"); packageJson.version,
console.log(`[INFO]: package.json version updated to: ${packageJson.version}`); );
packageJson.version = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
"utf8",
);
console.log(
`[INFO]: package.json version updated to: ${packageJson.version}`,
);
} catch (error) { } catch (error) {
console.error("Error updating package.json version:", error); console.error("Error updating package.json version:", error);
throw error; throw error;
@@ -105,12 +116,17 @@ async function updateCargoVersion(newVersion) {
try { try {
const data = await fs.readFile(cargoTomlPath, "utf8"); const data = await fs.readFile(cargoTomlPath, "utf8");
const lines = data.split("\n"); const lines = data.split("\n");
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV); const baseVersion = getBaseVersion(versionWithoutV);
const updatedLines = lines.map((line) => { const updatedLines = lines.map((line) => {
if (line.trim().startsWith("version =")) { if (line.trim().startsWith("version =")) {
return line.replace(/version\s*=\s*"[^"]+"/, `version = "${baseVersion}"`); return line.replace(
/version\s*=\s*"[^"]+"/,
`version = "${baseVersion}"`,
);
} }
return line; return line;
}); });
@@ -133,12 +149,21 @@ async function updateTauriConfigVersion(newVersion) {
try { try {
const data = await fs.readFile(tauriConfigPath, "utf8"); const data = await fs.readFile(tauriConfigPath, "utf8");
const tauriConfig = JSON.parse(data); const tauriConfig = JSON.parse(data);
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV); const baseVersion = getBaseVersion(versionWithoutV);
console.log("[INFO]: Current tauri.conf.json version is: ", tauriConfig.version); console.log(
"[INFO]: Current tauri.conf.json version is: ",
tauriConfig.version,
);
tauriConfig.version = baseVersion; tauriConfig.version = baseVersion;
await fs.writeFile(tauriConfigPath, JSON.stringify(tauriConfig, null, 2), "utf8"); await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
"utf8",
);
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`); console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
} catch (error) { } catch (error) {
console.error("Error updating tauri.conf.json version:", error); console.error("Error updating tauri.conf.json version:", error);
@@ -210,4 +235,3 @@ program
.argument("<version>", "version tag or full version") .argument("<version>", "version tag or full version")
.action(main) .action(main)
.parse(process.argv); .parse(process.argv);

View File

@@ -11,4 +11,3 @@ merge_derives = true
use_try_shorthand = false use_try_shorthand = false
use_field_init_shorthand = false use_field_init_shorthand = false
force_explicit_abi = true force_explicit_abi = true
imports_granularity = "Crate"

View File

@@ -11,15 +11,9 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": [ "resources": ["resources", "resources/locales/*"],
"resources",
"resources/locales/*"
],
"publisher": "Clash Verge Rev", "publisher": "Clash Verge Rev",
"externalBin": [ "externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"sidecar/verge-mihomo",
"sidecar/verge-mihomo-alpha"
],
"copyright": "GNU General Public License v3.0", "copyright": "GNU General Public License v3.0",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "Clash Verge Rev", "shortDescription": "Clash Verge Rev",
@@ -50,28 +44,18 @@
}, },
"deep-link": { "deep-link": {
"desktop": { "desktop": {
"schemes": [ "schemes": ["clash", "clash-verge"]
"clash",
"clash-verge"
]
} }
} }
}, },
"app": { "app": {
"security": { "security": {
"capabilities": [ "capabilities": ["desktop-capability", "migrated"],
"desktop-capability",
"migrated"
],
"assetProtocol": { "assetProtocol": {
"scope": [ "scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
"$APPDATA/**",
"$RESOURCE/../**",
"**"
],
"enable": true "enable": true
}, },
"csp": null "csp": null
} }
} }
} }

View File

@@ -4,9 +4,9 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", font-family:
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
sans-serif; "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
user-select: none; user-select: none;

View File

@@ -1,7 +1,11 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { Snackbar, Alert, IconButton, Box } from '@mui/material'; import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import { CloseRounded } from '@mui/icons-material'; import { CloseRounded } from "@mui/icons-material";
import { subscribeNotices, hideNotice, NoticeItem } from '@/services/noticeService'; import {
subscribeNotices,
hideNotice,
NoticeItem,
} from "@/services/noticeService";
export const NoticeManager: React.FC = () => { export const NoticeManager: React.FC = () => {
const [currentNotices, setCurrentNotices] = useState<NoticeItem[]>([]); const [currentNotices, setCurrentNotices] = useState<NoticeItem[]>([]);
@@ -23,49 +27,49 @@ export const NoticeManager: React.FC = () => {
return ( return (
<Box <Box
sx={{ sx={{
position: 'fixed', position: "fixed",
top: '20px', top: "20px",
right: '20px', right: "20px",
zIndex: 1500, zIndex: 1500,
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: '10px', gap: "10px",
maxWidth: '360px', maxWidth: "360px",
}} }}
> >
{currentNotices.map((notice) => ( {currentNotices.map((notice) => (
<Snackbar <Snackbar
key={notice.id} key={notice.id}
open={true} open={true}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }} anchorOrigin={{ vertical: "top", horizontal: "right" }}
sx={{ sx={{
position: 'relative', position: "relative",
transform: 'none', transform: "none",
top: 'auto', top: "auto",
right: 'auto', right: "auto",
bottom: 'auto', bottom: "auto",
left: 'auto', left: "auto",
width: '100%', width: "100%",
}} }}
> >
<Alert <Alert
severity={notice.type} severity={notice.type}
variant="filled" variant="filled"
sx={{ width: '100%' }} sx={{ width: "100%" }}
action={ action={
<IconButton <IconButton
size="small" size="small"
color="inherit" color="inherit"
onClick={() => handleClose(notice.id)} onClick={() => handleClose(notice.id)}
> >
<CloseRounded fontSize="inherit" /> <CloseRounded fontSize="inherit" />
</IconButton> </IconButton>
} }
> >
{notice.message} {notice.message}
</Alert> </Alert>
</Snackbar> </Snackbar>
))} ))}
</Box> </Box>
); );
}; };

View File

@@ -157,7 +157,7 @@ export const BaseSearchBox = (props: SearchProps) => {
</Tooltip> </Tooltip>
</Box> </Box>
), ),
} },
}} }}
/> />
</Tooltip> </Tooltip>

View File

@@ -107,7 +107,14 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{information.map((each) => ( {information.map((each) => (
<div key={each.label}> <div key={each.label}>
<b>{each.label}</b> <b>{each.label}</b>
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span> <span
style={{
wordBreak: "break-all",
color: theme.palette.text.primary,
}}
>
: {each.value}
</span>
</div> </div>
))} ))}

View File

@@ -25,7 +25,7 @@ export const ClashInfoCard = () => {
// 使用备忘录组件内容,减少重新渲染 // 使用备忘录组件内容,减少重新渲染
const cardContent = useMemo(() => { const cardContent = useMemo(() => {
if (!clashConfig) return null; if (!clashConfig) return null;
return ( return (
<Stack spacing={1.5}> <Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between"> <Stack direction="row" justifyContent="space-between">

View File

@@ -24,11 +24,14 @@ export const ClashModeCard = () => {
const currentMode = clashConfig?.mode?.toLowerCase(); const currentMode = clashConfig?.mode?.toLowerCase();
// 模式图标映射 // 模式图标映射
const modeIcons = useMemo(() => ({ const modeIcons = useMemo(
rule: <MultipleStopRounded fontSize="small" />, () => ({
global: <LanguageRounded fontSize="small" />, rule: <MultipleStopRounded fontSize="small" />,
direct: <DirectionsRounded fontSize="small" /> global: <LanguageRounded fontSize="small" />,
}), []); direct: <DirectionsRounded fontSize="small" />,
}),
[],
);
// 切换模式的处理函数 // 切换模式的处理函数
const onChangeMode = useLockFn(async (mode: string) => { const onChangeMode = useLockFn(async (mode: string) => {
@@ -68,18 +71,19 @@ export const ClashModeCard = () => {
"&:active": { "&:active": {
transform: "translateY(1px)", transform: "translateY(1px)",
}, },
"&::after": mode === currentMode "&::after":
? { mode === currentMode
content: '""', ? {
position: "absolute", content: '""',
bottom: -16, position: "absolute",
left: "50%", bottom: -16,
width: 2, left: "50%",
height: 16, width: 2,
bgcolor: "primary.main", height: 16,
transform: "translateX(-50%)", bgcolor: "primary.main",
} transform: "translateX(-50%)",
: {}, }
: {},
}); });
// 描述样式 // 描述样式
@@ -143,12 +147,10 @@ export const ClashModeCard = () => {
overflow: "visible", overflow: "visible",
}} }}
> >
<Typography <Typography variant="caption" component="div" sx={descriptionStyles}>
variant="caption" {t(
component="div" `${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`,
sx={descriptionStyles} )}
>
{t(`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`)}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>

View File

@@ -105,7 +105,7 @@ export const CurrentProxyCard = () => {
// 添加排序类型状态 // 添加排序类型状态
const [sortType, setSortType] = useState<ProxySortType>(() => { const [sortType, setSortType] = useState<ProxySortType>(() => {
const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE); const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE);
return savedSortType ? Number(savedSortType) as ProxySortType : 0; return savedSortType ? (Number(savedSortType) as ProxySortType) : 0;
}); });
// 定义状态类型 // 定义状态类型
@@ -156,7 +156,8 @@ export const CurrentProxyCard = () => {
primaryKeywords.some((keyword) => primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()), group.name.toLowerCase().includes(keyword.toLowerCase()),
), ),
) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0]; ) ||
proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
return primaryGroup?.name || ""; return primaryGroup?.name || "";
}; };
@@ -200,11 +201,13 @@ export const CurrentProxyCard = () => {
// 只保留 Selector 类型的组用于选择 // 只保留 Selector 类型的组用于选择
const filteredGroups = proxies.groups const filteredGroups = proxies.groups
.filter((g: { name: string; type?: string }) => g.type === "Selector") .filter((g: { name: string; type?: string }) => g.type === "Selector")
.map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({ .map(
name: g.name, (g: { name: string; now: string; all: Array<{ name: string }> }) => ({
now: g.now || "", name: g.name,
all: g.all.map((p: { name: string }) => p.name), now: g.now || "",
})); all: g.all.map((p: { name: string }) => p.name),
}),
);
let newProxy = ""; let newProxy = "";
let newDisplayProxy = null; let newDisplayProxy = null;
@@ -230,12 +233,12 @@ export const CurrentProxyCard = () => {
if (selectorGroup) { if (selectorGroup) {
newGroup = selectorGroup.name; newGroup = selectorGroup.name;
newProxy = selectorGroup.now || selectorGroup.all[0] || ""; newProxy = selectorGroup.now || selectorGroup.all[0] || "";
newDisplayProxy = proxies.records?.[newProxy] || null; newDisplayProxy = proxies.records?.[newProxy] || null;
if (!isGlobalMode && !isDirectMode) { if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup); localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) { if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy); localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
} }
} }
} }
@@ -280,7 +283,9 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup); localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
setState((prev) => { setState((prev) => {
const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup); const group = prev.proxyData.groups.find(
(g: { name: string }) => g.name === newGroup,
);
if (group) { if (group) {
return { return {
...prev, ...prev,
@@ -368,14 +373,16 @@ export const CurrentProxyCard = () => {
}, [state.displayProxy]); }, [state.displayProxy]);
// 获取当前节点的延迟(增加非空校验) // 获取当前节点的延迟(增加非空校验)
const currentDelay = currentProxy && state.selection.group const currentDelay =
? delayManager.getDelayFix(currentProxy, state.selection.group) currentProxy && state.selection.group
: -1; ? delayManager.getDelayFix(currentProxy, state.selection.group)
: -1;
// 信号图标(增加非空校验) // 信号图标(增加非空校验)
const signalInfo = currentProxy && state.selection.group const signalInfo =
? getSignalIcon(currentDelay) currentProxy && state.selection.group
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" }; ? getSignalIcon(currentDelay)
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
// 自定义渲染选择框中的值 // 自定义渲染选择框中的值
const renderProxyValue = useCallback( const renderProxyValue = useCallback(
@@ -384,7 +391,7 @@ export const CurrentProxyCard = () => {
const delayValue = delayManager.getDelayFix( const delayValue = delayManager.getDelayFix(
state.proxyData.records[selected], state.proxyData.records[selected],
state.selection.group state.selection.group,
); );
return ( return (
@@ -441,7 +448,7 @@ export const CurrentProxyCard = () => {
return list; return list;
}, },
[sortType, state.proxyData.records, state.selection.group] [sortType, state.proxyData.records, state.selection.group],
); );
// 计算要显示的代理选项(增加非空校验) // 计算要显示的代理选项(增加非空校验)
@@ -452,11 +459,11 @@ export const CurrentProxyCard = () => {
if (isGlobalMode && proxies?.global) { if (isGlobalMode && proxies?.global) {
const options = proxies.global.all const options = proxies.global.all
.filter((p: any) => { .filter((p: any) => {
const name = typeof p === 'string' ? p : p.name; const name = typeof p === "string" ? p : p.name;
return name !== "DIRECT" && name !== "REJECT"; return name !== "DIRECT" && name !== "REJECT";
}) })
.map((p: any) => ({ .map((p: any) => ({
name: typeof p === 'string' ? p : p.name name: typeof p === "string" ? p : p.name,
})); }));
return sortProxies(options); return sortProxies(options);
@@ -464,7 +471,7 @@ export const CurrentProxyCard = () => {
// 规则模式 // 规则模式
const group = state.selection.group const group = state.selection.group
? state.proxyData.groups.find(g => g.name === state.selection.group) ? state.proxyData.groups.find((g) => g.name === state.selection.group)
: null; : null;
if (group) { if (group) {
@@ -473,7 +480,14 @@ export const CurrentProxyCard = () => {
} }
return []; return [];
}, [isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortProxies]); }, [
isDirectMode,
isGlobalMode,
proxies,
state.proxyData,
state.selection.group,
sortProxies,
]);
// 获取排序图标 // 获取排序图标
const getSortIcon = () => { const getSortIcon = () => {
@@ -660,12 +674,14 @@ export const CurrentProxyCard = () => {
{isDirectMode {isDirectMode
? null ? null
: proxyOptions.map((proxy, index) => { : proxyOptions.map((proxy, index) => {
const delayValue = state.proxyData.records[proxy.name] && state.selection.group const delayValue =
? delayManager.getDelayFix( state.proxyData.records[proxy.name] &&
state.proxyData.records[proxy.name], state.selection.group
state.selection.group, ? delayManager.getDelayFix(
) state.proxyData.records[proxy.name],
: -1; state.selection.group,
)
: -1;
return ( return (
<MenuItem <MenuItem
key={`${proxy.name}-${index}`} key={`${proxy.name}-${index}`}
@@ -706,4 +722,4 @@ export const CurrentProxyCard = () => {
)} )}
</EnhancedCard> </EnhancedCard>
); );
}; };

View File

@@ -38,7 +38,7 @@ export const EnhancedCard = ({
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
display: "block" display: "block",
}; };
return ( return (
@@ -62,13 +62,15 @@ export const EnhancedCard = ({
borderColor: "divider", borderColor: "divider",
}} }}
> >
<Box sx={{ <Box
display: "flex", sx={{
alignItems: "center", display: "flex",
minWidth: 0, alignItems: "center",
flex: 1, minWidth: 0,
overflow: "hidden" flex: 1,
}}> overflow: "hidden",
}}
>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@@ -87,9 +89,9 @@ export const EnhancedCard = ({
</Box> </Box>
<Box sx={{ minWidth: 0, flex: 1 }}> <Box sx={{ minWidth: 0, flex: 1 }}>
{typeof title === "string" ? ( {typeof title === "string" ? (
<Typography <Typography
variant="h6" variant="h6"
fontWeight="medium" fontWeight="medium"
fontSize={18} fontSize={18}
sx={titleTruncateStyle} sx={titleTruncateStyle}
title={title} title={title}
@@ -97,9 +99,7 @@ export const EnhancedCard = ({
{title} {title}
</Typography> </Typography>
) : ( ) : (
<Box sx={titleTruncateStyle}> <Box sx={titleTruncateStyle}>{title}</Box>
{title}
</Box>
)} )}
</Box> </Box>
</Box> </Box>

View File

@@ -30,7 +30,7 @@ ChartJS.register(
PointElement, PointElement,
LineElement, LineElement,
Tooltip, Tooltip,
Filler Filler,
); );
// 流量数据项接口 // 流量数据项接口
@@ -54,8 +54,8 @@ type DataPoint = ITrafficItem & { name: string; timestamp: number };
/** /**
* 增强型流量图表组件 * 增强型流量图表组件
*/ */
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>( export const EnhancedTrafficGraph = memo(
(props, ref) => { forwardRef<EnhancedTrafficGraphRef>((props, ref) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -63,20 +63,20 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
const [timeRange, setTimeRange] = useState<TimeRange>(10); const [timeRange, setTimeRange] = useState<TimeRange>(10);
const [chartStyle, setChartStyle] = useState<"line" | "area">("area"); const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
const [displayData, setDisplayData] = useState<DataPoint[]>([]); const [displayData, setDisplayData] = useState<DataPoint[]>([]);
// 数据缓冲区 // 数据缓冲区
const dataBufferRef = useRef<DataPoint[]>([]); const dataBufferRef = useRef<DataPoint[]>([]);
// 根据时间范围计算保留的数据点数量 // 根据时间范围计算保留的数据点数量
const getMaxPointsByTimeRange = useCallback( const getMaxPointsByTimeRange = useCallback(
(minutes: TimeRange): number => minutes * 60, (minutes: TimeRange): number => minutes * 60,
[] [],
); );
// 最大数据点数量 // 最大数据点数量
const MAX_BUFFER_SIZE = useMemo( const MAX_BUFFER_SIZE = useMemo(
() => getMaxPointsByTimeRange(10), () => getMaxPointsByTimeRange(10),
[getMaxPointsByTimeRange] [getMaxPointsByTimeRange],
); );
// 颜色配置 // 颜色配置
@@ -89,23 +89,28 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
text: theme.palette.text.primary, text: theme.palette.text.primary,
tooltipBorder: theme.palette.divider, tooltipBorder: theme.palette.divider,
}), }),
[theme] [theme],
); );
// 切换时间范围 // 切换时间范围
const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => { const handleTimeRangeClick = useCallback(
event.stopPropagation(); (event: React.MouseEvent<SVGTextElement>) => {
setTimeRange((prevRange) => { event.stopPropagation();
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1; setTimeRange((prevRange) => {
}); return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
}, []); });
},
// 点击图表主体或图例时切换样式 [],
const handleToggleStyleClick = useCallback((event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => { );
event.stopPropagation();
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
}, []);
// 点击图表主体或图例时切换样式
const handleToggleStyleClick = useCallback(
(event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
event.stopPropagation();
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
},
[],
);
// 初始化数据缓冲区 // 初始化数据缓冲区
useEffect(() => { useEffect(() => {
@@ -121,7 +126,9 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
let nameValue: string; let nameValue: string;
try { try {
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
console.warn(`Initial data generation: Invalid date for timestamp ${pointTime}`); console.warn(
`Initial data generation: Invalid date for timestamp ${pointTime}`,
);
nameValue = "??:??:??"; nameValue = "??:??:??";
} else { } else {
nameValue = date.toLocaleTimeString("en-US", { nameValue = date.toLocaleTimeString("en-US", {
@@ -132,7 +139,14 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}); });
} }
} catch (e) { } catch (e) {
console.error("Error in toLocaleTimeString during initial data gen:", e, "Date:", date, "Timestamp:", pointTime); console.error(
"Error in toLocaleTimeString during initial data gen:",
e,
"Date:",
date,
"Timestamp:",
pointTime,
);
nameValue = "Err:Time"; nameValue = "Err:Time";
} }
@@ -142,55 +156,66 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
timestamp: pointTime, timestamp: pointTime,
name: nameValue, name: nameValue,
}; };
} },
); );
dataBufferRef.current = initialBuffer; dataBufferRef.current = initialBuffer;
// 更新显示数据 // 更新显示数据
const pointsToShow = getMaxPointsByTimeRange(timeRange); const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(initialBuffer.slice(-pointsToShow)); setDisplayData(initialBuffer.slice(-pointsToShow));
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]); }, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
// 添加数据点方法 // 添加数据点方法
const appendData = useCallback((data: ITrafficItem) => { const appendData = useCallback(
const safeData = { (data: ITrafficItem) => {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0, const safeData = {
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0, up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
}; down:
typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
};
const timestamp = data.timestamp || Date.now(); const timestamp = data.timestamp || Date.now();
const date = new Date(timestamp); const date = new Date(timestamp);
let nameValue: string; let nameValue: string;
try { try {
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
console.warn(`appendData: Invalid date for timestamp ${timestamp}`); console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
nameValue = "??:??:??"; nameValue = "??:??:??";
} else { } else {
nameValue = date.toLocaleTimeString("en-US", { nameValue = date.toLocaleTimeString("en-US", {
hour12: false, hour12: false,
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
}); });
}
} catch (e) {
console.error(
"Error in toLocaleTimeString in appendData:",
e,
"Date:",
date,
"Timestamp:",
timestamp,
);
nameValue = "Err:Time";
} }
} catch (e) { // 带时间标签的新数据点
console.error("Error in toLocaleTimeString in appendData:", e, "Date:", date, "Timestamp:", timestamp); const newPoint: DataPoint = {
nameValue = "Err:Time"; ...safeData,
} name: nameValue,
// 带时间标签的新数据点 timestamp: timestamp,
const newPoint: DataPoint = { };
...safeData,
name: nameValue,
timestamp: timestamp,
};
const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer; dataBufferRef.current = newBuffer;
const pointsToShow = getMaxPointsByTimeRange(timeRange); const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow)); setDisplayData(newBuffer.slice(-pointsToShow));
}, [timeRange, getMaxPointsByTimeRange]); },
[timeRange, getMaxPointsByTimeRange],
);
// 监听时间范围变化 // 监听时间范围变化
useEffect(() => { useEffect(() => {
@@ -202,7 +227,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
// 切换图表样式 // 切换图表样式
const toggleStyle = useCallback(() => { const toggleStyle = useCallback(() => {
setChartStyle((prev) => prev === "line" ? "area" : "line"); setChartStyle((prev) => (prev === "line" ? "area" : "line"));
}, []); }, []);
// 暴露方法给父组件 // 暴露方法给父组件
@@ -212,30 +237,31 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
appendData, appendData,
toggleStyle, toggleStyle,
}), }),
[appendData, toggleStyle] [appendData, toggleStyle],
); );
const formatYAxis = useCallback((value: number | string): string => { const formatYAxis = useCallback((value: number | string): string => {
if (typeof value !== 'number') return String(value); if (typeof value !== "number") return String(value);
const [num, unit] = parseTraffic(value); const [num, unit] = parseTraffic(value);
return `${num}${unit}`; return `${num}${unit}`;
}, []); }, []);
const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => { const formatXLabel = useCallback(
const dataPoint = displayData[index as number]; (tickValue: string | number, index: number, ticks: any[]) => {
if (dataPoint && dataPoint.name) { const dataPoint = displayData[index as number];
const parts = dataPoint.name.split(":"); if (dataPoint && dataPoint.name) {
return `${parts[0]}:${parts[1]}`; const parts = dataPoint.name.split(":");
} return `${parts[0]}:${parts[1]}`;
if(typeof tickValue === 'string') { }
const parts = tickValue.split(":"); if (typeof tickValue === "string") {
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`; const parts = tickValue.split(":");
return tickValue; if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
} return tickValue;
return ''; }
}, [displayData]); return "";
},
[displayData],
);
// 获取当前时间范围文本 // 获取当前时间范围文本
const getTimeRangeText = useCallback(() => { const getTimeRangeText = useCallback(() => {
@@ -243,13 +269,13 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}, [timeRange, t]); }, [timeRange, t]);
const chartData = useMemo(() => { const chartData = useMemo(() => {
const labels = displayData.map(d => d.name); const labels = displayData.map((d) => d.name);
return { return {
labels, labels,
datasets: [ datasets: [
{ {
label: t("Upload"), label: t("Upload"),
data: displayData.map(d => d.up), data: displayData.map((d) => d.up),
borderColor: colors.up, borderColor: colors.up,
backgroundColor: chartStyle === "area" ? colors.up : colors.up, backgroundColor: chartStyle === "area" ? colors.up : colors.up,
fill: chartStyle === "area", fill: chartStyle === "area",
@@ -260,7 +286,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}, },
{ {
label: t("Download"), label: t("Download"),
data: displayData.map(d => d.down), data: displayData.map((d) => d.down),
borderColor: colors.down, borderColor: colors.down,
backgroundColor: chartStyle === "area" ? colors.down : colors.down, backgroundColor: chartStyle === "area" ? colors.down : colors.down,
fill: chartStyle === "area", fill: chartStyle === "area",
@@ -268,113 +294,130 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 4, pointHoverRadius: 4,
borderWidth: 2, borderWidth: 2,
} },
] ],
}; };
}, [displayData, colors.up, colors.down, t, chartStyle]); }, [displayData, colors.up, colors.down, t, chartStyle]);
const chartOptions = useMemo(() => ({ const chartOptions = useMemo(
responsive: true, () => ({
maintainAspectRatio: false, responsive: true,
animation: false as false, maintainAspectRatio: false,
scales: { animation: false as false,
x: { scales: {
display: true, x: {
type: 'category' as const,
labels: displayData.map(d => d.name),
ticks: {
display: true, display: true,
color: colors.text, type: "category" as const,
font: { size: 10 }, labels: displayData.map((d) => d.name),
callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined { ticks: {
let labelToFormat: string | undefined = undefined; display: true,
color: colors.text,
font: { size: 10 },
callback: function (
this: Scale,
tickValue: string | number,
index: number,
ticks: Tick[],
): string | undefined {
let labelToFormat: string | undefined = undefined;
const currentDisplayTick = ticks[index]; const currentDisplayTick = ticks[index];
if (currentDisplayTick && typeof currentDisplayTick.label === 'string') { if (
labelToFormat = currentDisplayTick.label; currentDisplayTick &&
} else { typeof currentDisplayTick.label === "string"
const sourceLabels = displayData.map(d => d.name); ) {
if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) { labelToFormat = currentDisplayTick.label;
labelToFormat = sourceLabels[tickValue]; } else {
} else if (typeof tickValue === 'string') { const sourceLabels = displayData.map((d) => d.name);
labelToFormat = tickValue; if (
typeof tickValue === "number" &&
tickValue >= 0 &&
tickValue < sourceLabels.length
) {
labelToFormat = sourceLabels[tickValue];
} else if (typeof tickValue === "string") {
labelToFormat = tickValue;
}
} }
}
if (typeof labelToFormat !== 'string') { if (typeof labelToFormat !== "string") {
return undefined; return undefined;
} }
const parts: string[] = labelToFormat.split(':'); const parts: string[] = labelToFormat.split(":");
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat; return parts.length >= 2
? `${parts[0]}:${parts[1]}`
: labelToFormat;
},
autoSkip: true,
maxTicksLimit: Math.max(
5,
Math.floor(displayData.length / (timeRange * 2)),
),
minRotation: 0,
maxRotation: 0,
},
grid: {
display: true,
drawOnChartArea: false,
drawTicks: true,
tickLength: 2,
color: colors.text,
}, },
autoSkip: true,
maxTicksLimit: Math.max(5, Math.floor(displayData.length / (timeRange * 2))),
minRotation: 0,
maxRotation: 0,
}, },
grid: { y: {
display: true, beginAtZero: true,
drawOnChartArea: false, ticks: {
drawTicks: true, color: colors.text,
tickLength: 2, font: { size: 10 },
color: colors.text, callback: formatYAxis,
},
grid: {
display: true,
drawTicks: true,
tickLength: 3,
color: colors.grid,
},
}, },
}, },
y: { plugins: {
beginAtZero: true, tooltip: {
ticks: { enabled: true,
color: colors.text, mode: "index" as const,
font: { size: 10 }, intersect: false,
callback: formatYAxis, backgroundColor: colors.tooltipBg,
}, titleColor: colors.text,
grid: { bodyColor: colors.text,
display: true, borderColor: colors.tooltipBorder,
drawTicks: true, borderWidth: 1,
tickLength: 3, cornerRadius: 4,
color: colors.grid, padding: 8,
callbacks: {
}, title: (tooltipItems: any[]) => {
} return `${t("Time")}: ${tooltipItems[0].label}`;
}, },
plugins: { label: (context: any): string => {
tooltip: { const label = context.dataset.label || "";
enabled: true, const value = context.parsed.y;
mode: 'index' as const, const [num, unit] = parseTraffic(value);
intersect: false, return `${label}: ${num} ${unit}/s`;
backgroundColor: colors.tooltipBg, },
titleColor: colors.text,
bodyColor: colors.text,
borderColor: colors.tooltipBorder,
borderWidth: 1,
cornerRadius: 4,
padding: 8,
callbacks: {
title: (tooltipItems: any[]) => {
return `${t("Time")}: ${tooltipItems[0].label}`;
}, },
label: (context: any): string => { },
const label = context.dataset.label || ''; legend: {
const value = context.parsed.y; display: false,
const [num, unit] = parseTraffic(value); },
return `${label}: ${num} ${unit}/s`;
}
}
}, },
legend: { layout: {
display: false padding: {
} top: 16,
}, right: 7,
layout: { left: 3,
padding: { },
top: 16, },
right: 7, }),
left: 3, [colors, t, formatYAxis, timeRange, displayData],
} );
}
}), [colors, t, formatYAxis, timeRange, displayData]);
return ( return (
<Box <Box
@@ -392,8 +435,17 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
{displayData.length > 0 && ( {displayData.length > 0 && (
<ChartJsLine data={chartData} options={chartOptions} /> <ChartJsLine data={chartData} options={chartOptions} />
)} )}
<svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}> <svg
width="100%"
height="100%"
style={{
position: "absolute",
top: 0,
left: 0,
pointerEvents: "none",
}}
>
<text <text
x="3.5%" x="3.5%"
y="10%" y="10%"
@@ -402,11 +454,11 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={11} fontSize={11}
fontWeight="bold" fontWeight="bold"
onClick={handleTimeRangeClick} onClick={handleTimeRangeClick}
style={{ cursor: "pointer", pointerEvents: 'all' }} style={{ cursor: "pointer", pointerEvents: "all" }}
> >
{getTimeRangeText()} {getTimeRangeText()}
</text> </text>
<text <text
x="99%" x="99%"
y="10%" y="10%"
@@ -415,7 +467,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={12} fontSize={12}
fontWeight="bold" fontWeight="bold"
onClick={handleToggleStyleClick} onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: 'all' }} style={{ cursor: "pointer", pointerEvents: "all" }}
> >
{t("Upload")} {t("Upload")}
</text> </text>
@@ -428,7 +480,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={12} fontSize={12}
fontWeight="bold" fontWeight="bold"
onClick={handleToggleStyleClick} onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: 'all' }} style={{ cursor: "pointer", pointerEvents: "all" }}
> >
{t("Download")} {t("Download")}
</text> </text>
@@ -436,7 +488,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
</div> </div>
</Box> </Box>
); );
}, }),
)); );
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph"; EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";

View File

@@ -66,85 +66,90 @@ const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新 const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
// 统计卡片组件 - 使用memo优化 // 统计卡片组件 - 使用memo优化
const CompactStatCard = memo(({ const CompactStatCard = memo(
icon, ({ icon, title, value, unit, color, onClick }: StatCardProps) => {
title, const theme = useTheme();
value,
unit,
color,
onClick,
}: StatCardProps) => {
const theme = useTheme();
// 获取调色板颜色 - 使用useMemo避免重复计算 // 获取调色板颜色 - 使用useMemo避免重复计算
const colorValue = useMemo(() => { const colorValue = useMemo(() => {
const palette = theme.palette; const palette = theme.palette;
if ( if (
color in palette && color in palette &&
palette[color as keyof typeof palette] && palette[color as keyof typeof palette] &&
"main" in (palette[color as keyof typeof palette] as PaletteColor) "main" in (palette[color as keyof typeof palette] as PaletteColor)
) { ) {
return (palette[color as keyof typeof palette] as PaletteColor).main; return (palette[color as keyof typeof palette] as PaletteColor).main;
} }
return palette.primary.main; return palette.primary.main;
}, [theme.palette, color]); }, [theme.palette, color]);
return ( return (
<Paper <Paper
elevation={0} elevation={0}
sx={{
display: "flex",
alignItems: "center",
borderRadius: 2,
bgcolor: alpha(colorValue, 0.05),
border: `1px solid ${alpha(colorValue, 0.15)}`,
padding: "8px",
transition: "all 0.2s ease-in-out",
cursor: onClick ? "pointer" : "default",
"&:hover": onClick ? {
bgcolor: alpha(colorValue, 0.1),
border: `1px solid ${alpha(colorValue, 0.3)}`,
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
} : {},
}}
onClick={onClick}
>
{/* 图标容器 */}
<Grid
component="div"
sx={{ sx={{
mr: 1,
ml: "2px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", borderRadius: 2,
width: 32, bgcolor: alpha(colorValue, 0.05),
height: 32, border: `1px solid ${alpha(colorValue, 0.15)}`,
borderRadius: "50%", padding: "8px",
bgcolor: alpha(colorValue, 0.1), transition: "all 0.2s ease-in-out",
color: colorValue, cursor: onClick ? "pointer" : "default",
"&:hover": onClick
? {
bgcolor: alpha(colorValue, 0.1),
border: `1px solid ${alpha(colorValue, 0.3)}`,
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
}
: {},
}} }}
onClick={onClick}
> >
{icon} {/* 图标容器 */}
</Grid> <Grid
component="div"
{/* 文本内容 */} sx={{
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}> mr: 1,
<Typography variant="caption" color="text.secondary" noWrap> ml: "2px",
{title} display: "flex",
</Typography> alignItems: "center",
<Grid component="div" sx={{ display: "flex", alignItems: "baseline" }}> justifyContent: "center",
<Typography variant="body1" fontWeight="bold" noWrap sx={{ mr: 0.5 }}> width: 32,
{value} height: 32,
</Typography> borderRadius: "50%",
<Typography variant="caption" color="text.secondary"> bgcolor: alpha(colorValue, 0.1),
{unit} color: colorValue,
</Typography> }}
>
{icon}
</Grid> </Grid>
</Grid>
</Paper> {/* 文本内容 */}
); <Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
}); <Typography variant="caption" color="text.secondary" noWrap>
{title}
</Typography>
<Grid
component="div"
sx={{ display: "flex", alignItems: "baseline" }}
>
<Typography
variant="body1"
fontWeight="bold"
noWrap
sx={{ mr: 0.5 }}
>
{value}
</Typography>
<Typography variant="caption" color="text.secondary">
{unit}
</Typography>
</Grid>
</Grid>
</Paper>
);
},
);
// 添加显示名称 // 添加显示名称
CompactStatCard.displayName = "CompactStatCard"; CompactStatCard.displayName = "CompactStatCard";
@@ -205,25 +210,25 @@ export const EnhancedTrafficStats = () => {
down: data.down, down: data.down,
timestamp: now, timestamp: now,
}); });
} catch { } } catch {}
return; return;
} }
lastUpdateRef.current.traffic = now; lastUpdateRef.current.traffic = now;
const safeUp = isNaN(data.up) ? 0 : data.up; const safeUp = isNaN(data.up) ? 0 : data.up;
const safeDown = isNaN(data.down) ? 0 : data.down; const safeDown = isNaN(data.down) ? 0 : data.down;
try { try {
setStats(prev => ({ setStats((prev) => ({
...prev, ...prev,
traffic: { up: safeUp, down: safeDown } traffic: { up: safeUp, down: safeDown },
})); }));
} catch { } } catch {}
try { try {
trafficRef.current?.appendData({ trafficRef.current?.appendData({
up: safeUp, up: safeUp,
down: safeDown, down: safeDown,
timestamp: now, timestamp: now,
}); });
} catch { } } catch {}
} }
} catch (err) { } catch (err) {
console.error("[Traffic] 解析数据错误:", err, event.data); console.error("[Traffic] 解析数据错误:", err, event.data);
@@ -235,12 +240,12 @@ export const EnhancedTrafficStats = () => {
try { try {
const data = JSON.parse(event.data) as MemoryUsage; const data = JSON.parse(event.data) as MemoryUsage;
if (data && typeof data.inuse === "number") { if (data && typeof data.inuse === "number") {
setStats(prev => ({ setStats((prev) => ({
...prev, ...prev,
memory: { memory: {
inuse: isNaN(data.inuse) ? 0 : data.inuse, inuse: isNaN(data.inuse) ? 0 : data.inuse,
oslimit: data.oslimit, oslimit: data.oslimit,
} },
})); }));
} }
} catch (err) { } catch (err) {
@@ -257,7 +262,7 @@ export const EnhancedTrafficStats = () => {
// 清理现有连接的函数 // 清理现有连接的函数
const cleanupSockets = () => { const cleanupSockets = () => {
Object.values(socketRefs.current).forEach(socket => { Object.values(socketRefs.current).forEach((socket) => {
if (socket) { if (socket) {
socket.close(); socket.close();
} }
@@ -269,40 +274,78 @@ export const EnhancedTrafficStats = () => {
cleanupSockets(); cleanupSockets();
// 创建新连接 // 创建新连接
console.log(`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`); console.log(
socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, { `[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
onmessage: handleTrafficUpdate, );
onopen: (event) => { socketRefs.current.traffic = createAuthSockette(
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event); `${server}/traffic`,
secret,
{
onmessage: handleTrafficUpdate,
onopen: (event) => {
console.log(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
},
onerror: (event) => {
console.error(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
},
onclose: (event) => {
console.log(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
}
},
}, },
onerror: (event) => { );
console.error(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
},
onclose: (event) => {
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
}
},
});
console.log(`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`); console.log(
`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`,
);
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, { socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
onmessage: handleMemoryUpdate, onmessage: handleMemoryUpdate,
onopen: (event) => { onopen: (event) => {
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event); console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
}, },
onerror: (event) => { onerror: (event) => {
console.error(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event); console.error(
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } })); `[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
}, },
onclose: (event) => { onclose: (event) => {
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason); console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) { if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`); console.warn(
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } })); `[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
} }
}, },
}); });
@@ -314,11 +357,11 @@ export const EnhancedTrafficStats = () => {
useEffect(() => { useEffect(() => {
return () => { return () => {
try { try {
Object.values(socketRefs.current).forEach(socket => { Object.values(socketRefs.current).forEach((socket) => {
if (socket) socket.close(); if (socket) socket.close();
}); });
socketRefs.current = { traffic: null, memory: null }; socketRefs.current = { traffic: null, memory: null };
} catch { } } catch {}
}; };
}, []); }, []);
@@ -339,13 +382,25 @@ export const EnhancedTrafficStats = () => {
const [up, upUnit] = parseTraffic(stats.traffic.up); const [up, upUnit] = parseTraffic(stats.traffic.up);
const [down, downUnit] = parseTraffic(stats.traffic.down); const [down, downUnit] = parseTraffic(stats.traffic.down);
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse); const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal); const [uploadTotal, uploadTotalUnit] = parseTraffic(
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal); connections.uploadTotal,
);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
connections.downloadTotal,
);
return { return {
up, upUnit, down, downUnit, inuse, inuseUnit, up,
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit, upUnit,
connectionsCount: connections.count down,
downUnit,
inuse,
inuseUnit,
uploadTotal,
uploadTotalUnit,
downloadTotal,
downloadTotalUnit,
connectionsCount: connections.count,
}; };
}, [stats, connections]); }, [stats, connections]);
@@ -392,51 +447,54 @@ export const EnhancedTrafficStats = () => {
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]); }, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
// 使用useMemo计算统计卡片配置 // 使用useMemo计算统计卡片配置
const statCards = useMemo(() => [ const statCards = useMemo(
{ () => [
icon: <ArrowUpwardRounded fontSize="small" />, {
title: t("Upload Speed"), icon: <ArrowUpwardRounded fontSize="small" />,
value: parsedData.up, title: t("Upload Speed"),
unit: `${parsedData.upUnit}/s`, value: parsedData.up,
color: "secondary" as const, unit: `${parsedData.upUnit}/s`,
}, color: "secondary" as const,
{ },
icon: <ArrowDownwardRounded fontSize="small" />, {
title: t("Download Speed"), icon: <ArrowDownwardRounded fontSize="small" />,
value: parsedData.down, title: t("Download Speed"),
unit: `${parsedData.downUnit}/s`, value: parsedData.down,
color: "primary" as const, unit: `${parsedData.downUnit}/s`,
}, color: "primary" as const,
{ },
icon: <LinkRounded fontSize="small" />, {
title: t("Active Connections"), icon: <LinkRounded fontSize="small" />,
value: parsedData.connectionsCount, title: t("Active Connections"),
unit: "", value: parsedData.connectionsCount,
color: "success" as const, unit: "",
}, color: "success" as const,
{ },
icon: <CloudUploadRounded fontSize="small" />, {
title: t("Uploaded"), icon: <CloudUploadRounded fontSize="small" />,
value: parsedData.uploadTotal, title: t("Uploaded"),
unit: parsedData.uploadTotalUnit, value: parsedData.uploadTotal,
color: "secondary" as const, unit: parsedData.uploadTotalUnit,
}, color: "secondary" as const,
{ },
icon: <CloudDownloadRounded fontSize="small" />, {
title: t("Downloaded"), icon: <CloudDownloadRounded fontSize="small" />,
value: parsedData.downloadTotal, title: t("Downloaded"),
unit: parsedData.downloadTotalUnit, value: parsedData.downloadTotal,
color: "primary" as const, unit: parsedData.downloadTotalUnit,
}, color: "primary" as const,
{ },
icon: <MemoryRounded fontSize="small" />, {
title: t("Memory Usage"), icon: <MemoryRounded fontSize="small" />,
value: parsedData.inuse, title: t("Memory Usage"),
unit: parsedData.inuseUnit, value: parsedData.inuse,
color: "error" as const, unit: parsedData.inuseUnit,
onClick: isDebug ? handleGarbageCollection : undefined, color: "error" as const,
}, onClick: isDebug ? handleGarbageCollection : undefined,
], [t, parsedData, isDebug, handleGarbageCollection]); },
],
[t, parsedData, isDebug, handleGarbageCollection],
);
return ( return (
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}> <Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>

View File

@@ -78,12 +78,16 @@ const truncateStyle = {
maxWidth: "calc(100% - 28px)", maxWidth: "calc(100% - 28px)",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap" whiteSpace: "nowrap",
}; };
// 提取独立组件减少主组件复杂度 // 提取独立组件减少主组件复杂度
const ProfileDetails = ({ current, onUpdateProfile, updating }: { const ProfileDetails = ({
current: ProfileItem; current,
onUpdateProfile,
updating,
}: {
current: ProfileItem;
onUpdateProfile: () => void; onUpdateProfile: () => void;
updating: boolean; updating: boolean;
}) => { }) => {
@@ -99,7 +103,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
if (!current.extra || !current.extra.total) return 1; if (!current.extra || !current.extra.total) return 1;
return Math.min( return Math.min(
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1, Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
100 100,
); );
}, [current.extra, usedTraffic]); }, [current.extra, usedTraffic]);
@@ -109,19 +113,24 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
{current.url && ( {current.url && (
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
<DnsOutlined fontSize="small" color="action" /> <DnsOutlined fontSize="small" color="action" />
<Typography variant="body2" color="text.secondary" noWrap sx={{ display: "flex", alignItems: "center" }}> <Typography
variant="body2"
color="text.secondary"
noWrap
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ flexShrink: 0 }}>{t("From")}: </span> <span style={{ flexShrink: 0 }}>{t("From")}: </span>
{current.home ? ( {current.home ? (
<Link <Link
component="button" component="button"
fontWeight="medium" fontWeight="medium"
onClick={() => current.home && openWebUrl(current.home)} onClick={() => current.home && openWebUrl(current.home)}
sx={{ sx={{
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
minWidth: 0, minWidth: 0,
maxWidth: "calc(100% - 40px)", maxWidth: "calc(100% - 40px)",
ml: 0.5 ml: 0.5,
}} }}
title={parseUrl(current.url)} title={parseUrl(current.url)}
> >
@@ -132,14 +141,19 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
minWidth: 0, minWidth: 0,
flex: 1 flex: 1,
}} }}
> >
{parseUrl(current.url)} {parseUrl(current.url)}
</Typography> </Typography>
<LaunchOutlined <LaunchOutlined
fontSize="inherit" fontSize="inherit"
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }} sx={{
ml: 0.5,
fontSize: "0.8rem",
opacity: 0.7,
flexShrink: 0,
}}
/> />
</Link> </Link>
) : ( ) : (
@@ -152,7 +166,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
whiteSpace: "nowrap", whiteSpace: "nowrap",
minWidth: 0, minWidth: 0,
flex: 1, flex: 1,
ml: 0.5 ml: 0.5,
}} }}
title={parseUrl(current.url)} title={parseUrl(current.url)}
> >
@@ -195,7 +209,8 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("Used / Total")}:{" "} {t("Used / Total")}:{" "}
<Box component="span" fontWeight="medium"> <Box component="span" fontWeight="medium">
{parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)} {parseTraffic(usedTraffic)} /{" "}
{parseTraffic(current.extra.total)}
</Box> </Box>
</Typography> </Typography>
</Stack> </Stack>
@@ -240,7 +255,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
// 提取空配置组件 // 提取空配置组件
const EmptyProfile = ({ onClick }: { onClick: () => void }) => { const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Box <Box
sx={{ sx={{
@@ -268,27 +283,30 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
); );
}; };
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => { export const HomeProfileCard = ({
current,
onProfileUpdated,
}: HomeProfileCardProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { refreshAll } = useAppData(); const { refreshAll } = useAppData();
// 更新当前订阅 // 更新当前订阅
const [updating, setUpdating] = useState(false); const [updating, setUpdating] = useState(false);
const onUpdateProfile = useLockFn(async () => { const onUpdateProfile = useLockFn(async () => {
if (!current?.uid) return; if (!current?.uid) return;
setUpdating(true); setUpdating(true);
try { try {
await updateProfile(current.uid, current.option); await updateProfile(current.uid, current.option);
showNotice('success', t("Update subscription successfully"), 1000); showNotice("success", t("Update subscription successfully"), 1000);
onProfileUpdated?.(); onProfileUpdated?.();
// 刷新首页数据 // 刷新首页数据
refreshAll(); refreshAll();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString(), 3000); showNotice("error", err.message || err.toString(), 3000);
} finally { } finally {
setUpdating(false); setUpdating(false);
} }
@@ -302,9 +320,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
// 卡片标题 // 卡片标题
const cardTitle = useMemo(() => { const cardTitle = useMemo(() => {
if (!current) return t("Profiles"); if (!current) return t("Profiles");
if (!current.home) return current.name; if (!current.home) return current.name;
return ( return (
<Link <Link
component="button" component="button"
@@ -323,19 +341,19 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
flex: 1 flex: 1,
} },
}} }}
title={current.name} title={current.name}
> >
<span>{current.name}</span> <span>{current.name}</span>
<LaunchOutlined <LaunchOutlined
fontSize="inherit" fontSize="inherit"
sx={{ sx={{
ml: 0.5, ml: 0.5,
fontSize: "0.8rem", fontSize: "0.8rem",
opacity: 0.7, opacity: 0.7,
flexShrink: 0 flexShrink: 0,
}} }}
/> />
</Link> </Link>
@@ -345,7 +363,7 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
// 卡片操作按钮 // 卡片操作按钮
const cardAction = useMemo(() => { const cardAction = useMemo(() => {
if (!current) return null; if (!current) return null;
return ( return (
<Button <Button
variant="outlined" variant="outlined"
@@ -367,10 +385,10 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
action={cardAction} action={cardAction}
> >
{current ? ( {current ? (
<ProfileDetails <ProfileDetails
current={current} current={current}
onUpdateProfile={onUpdateProfile} onUpdateProfile={onUpdateProfile}
updating={updating} updating={updating}
/> />
) : ( ) : (
<EmptyProfile onClick={goToProfiles} /> <EmptyProfile onClick={goToProfiles} />

View File

@@ -83,28 +83,28 @@ export const IpInfoCard = () => {
// 组件加载时获取IP信息 // 组件加载时获取IP信息
useEffect(() => { useEffect(() => {
fetchIpInfo(); fetchIpInfo();
// 倒计时实现优化,减少不必要的重渲染 // 倒计时实现优化,减少不必要的重渲染
let timer: number | null = null; let timer: number | null = null;
let currentCount = IP_REFRESH_SECONDS; let currentCount = IP_REFRESH_SECONDS;
// 只在必要时更新状态,减少重渲染次数 // 只在必要时更新状态,减少重渲染次数
const startCountdown = () => { const startCountdown = () => {
timer = window.setInterval(() => { timer = window.setInterval(() => {
currentCount -= 1; currentCount -= 1;
if (currentCount <= 0) { if (currentCount <= 0) {
fetchIpInfo(); fetchIpInfo();
currentCount = IP_REFRESH_SECONDS; currentCount = IP_REFRESH_SECONDS;
} }
// 每5秒或倒计时结束时才更新UI // 每5秒或倒计时结束时才更新UI
if (currentCount % 5 === 0 || currentCount <= 0) { if (currentCount % 5 === 0 || currentCount <= 0) {
setCountdown(currentCount); setCountdown(currentCount);
} }
}, 1000); }, 1000);
}; };
startCountdown(); startCountdown();
return () => { return () => {
if (timer) clearInterval(timer); if (timer) clearInterval(timer);
@@ -112,7 +112,7 @@ export const IpInfoCard = () => {
}, [fetchIpInfo]); }, [fetchIpInfo]);
const toggleShowIp = useCallback(() => { const toggleShowIp = useCallback(() => {
setShowIp(prev => !prev); setShowIp((prev) => !prev);
}, []); }, []);
// 渲染加载状态 // 渲染加载状态
@@ -282,9 +282,7 @@ export const IpInfoCard = () => {
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} /> <InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
<InfoItem <InfoItem
label={t("Location")} label={t("Location")}
value={[ipInfo?.city, ipInfo?.region] value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
.filter(Boolean)
.join(", ")}
/> />
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} /> <InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
</Box> </Box>

View File

@@ -1,19 +1,24 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Typography, Stack, Divider, Chip, IconButton, Tooltip } from "@mui/material"; import {
import { Typography,
InfoOutlined, Stack,
SettingsOutlined, Divider,
WarningOutlined, Chip,
IconButton,
Tooltip,
} from "@mui/material";
import {
InfoOutlined,
SettingsOutlined,
WarningOutlined,
AdminPanelSettingsOutlined, AdminPanelSettingsOutlined,
DnsOutlined, DnsOutlined,
ExtensionOutlined ExtensionOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { EnhancedCard } from "./enhanced-card"; import { EnhancedCard } from "./enhanced-card";
import useSWR from "swr"; import useSWR from "swr";
import { import { getSystemInfo } from "@/services/cmds";
getSystemInfo,
} from "@/services/cmds";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { version as appVersion } from "@root/package.json"; import { version as appVersion } from "@root/package.json";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
@@ -30,32 +35,35 @@ export const SystemInfoCard = () => {
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState(); const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
const { installServiceAndRestartCore } = useServiceInstaller(); const { installServiceAndRestartCore } = useServiceInstaller();
// 系统信息状态 // 系统信息状态
const [systemState, setSystemState] = useState({ const [systemState, setSystemState] = useState({
osInfo: "", osInfo: "",
lastCheckUpdate: "-", lastCheckUpdate: "-",
}); });
// 初始化系统信息 // 初始化系统信息
useEffect(() => { useEffect(() => {
getSystemInfo() getSystemInfo()
.then((info) => { .then((info) => {
const lines = info.split("\n"); const lines = info.split("\n");
if (lines.length > 0) { if (lines.length > 0) {
const sysName = lines[0].split(": ")[1] || ""; const sysName = lines[0].split(": ")[1] || "";
let sysVersion = lines[1].split(": ")[1] || ""; let sysVersion = lines[1].split(": ")[1] || "";
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) { if (
sysVersion = sysVersion.substring(sysName.length).trim(); sysName &&
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
) {
sysVersion = sysVersion.substring(sysName.length).trim();
}
setSystemState((prev) => ({
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
} }
})
setSystemState((prev) => ({ .catch(console.error);
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
}
})
.catch(console.error);
// 获取最后检查更新时间 // 获取最后检查更新时间
const lastCheck = localStorage.getItem("last_check_update"); const lastCheck = localStorage.getItem("last_check_update");
@@ -122,7 +130,6 @@ useEffect(() => {
} }
}, [verge, patchVerge]); }, [verge, patchVerge]);
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务 // 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
const handleRunningModeClick = useCallback(() => { const handleRunningModeClick = useCallback(() => {
if (isSidecarMode || (isAdminMode && isSidecarMode)) { if (isSidecarMode || (isAdminMode && isSidecarMode)) {
@@ -135,13 +142,13 @@ useEffect(() => {
try { try {
const info = await checkUpdate(); const info = await checkUpdate();
if (!info?.available) { if (!info?.available) {
showNotice('success', t("Currently on the Latest Version")); showNotice("success", t("Currently on the Latest Version"));
} else { } else {
showNotice('info', t("Update Available"), 2000); showNotice("info", t("Update Available"), 2000);
goToSettings(); goToSettings();
} }
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -155,13 +162,15 @@ useEffect(() => {
const runningModeStyle = useMemo( const runningModeStyle = useMemo(
() => ({ () => ({
// Sidecar或纯管理员模式允许安装服务 // Sidecar或纯管理员模式允许安装服务
cursor: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "pointer" : "default", cursor:
textDecoration: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "underline" : "none", isSidecarMode || (isAdminMode && isSidecarMode) ? "pointer" : "default",
textDecoration:
isSidecarMode || (isAdminMode && isSidecarMode) ? "underline" : "none",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: 0.5, gap: 0.5,
"&:hover": { "&:hover": {
opacity: (isSidecarMode || (isAdminMode && isSidecarMode)) ? 0.7 : 1, opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
}, },
}), }),
[isSidecarMode, isAdminMode], [isSidecarMode, isAdminMode],
@@ -174,34 +183,34 @@ useEffect(() => {
if (!isSidecarMode) { if (!isSidecarMode) {
return ( return (
<> <>
<AdminPanelSettingsOutlined <AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }} sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")} titleAccess={t("Administrator Mode")}
/> />
<DnsOutlined <DnsOutlined
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }} sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
titleAccess={t("Service Mode")} titleAccess={t("Service Mode")}
/> />
</> </>
); );
} }
return ( return (
<AdminPanelSettingsOutlined <AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }} sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")} titleAccess={t("Administrator Mode")}
/> />
); );
} else if (isSidecarMode) { } else if (isSidecarMode) {
return ( return (
<ExtensionOutlined <ExtensionOutlined
sx={{ color: "info.main", fontSize: 16 }} sx={{ color: "info.main", fontSize: 16 }}
titleAccess={t("Sidecar Mode")} titleAccess={t("Sidecar Mode")}
/> />
); );
} else { } else {
return ( return (
<DnsOutlined <DnsOutlined
sx={{ color: "success.main", fontSize: 16 }} sx={{ color: "success.main", fontSize: 16 }}
titleAccess={t("Service Mode")} titleAccess={t("Service Mode")}
/> />
); );
@@ -247,13 +256,19 @@ useEffect(() => {
</Typography> </Typography>
</Stack> </Stack>
<Divider /> <Divider />
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("Auto Launch")} {t("Auto Launch")}
</Typography> </Typography>
<Stack direction="row" spacing={1} alignItems="center"> <Stack direction="row" spacing={1} alignItems="center">
{isAdminMode && ( {isAdminMode && (
<Tooltip title={t("Administrator mode may not support auto launch")}> <Tooltip
title={t("Administrator mode may not support auto launch")}
>
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} /> <WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
</Tooltip> </Tooltip>
)} )}
@@ -268,7 +283,11 @@ useEffect(() => {
</Stack> </Stack>
</Stack> </Stack>
<Divider /> <Divider />
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("Running Mode")} {t("Running Mode")}
</Typography> </Typography>

View File

@@ -87,12 +87,12 @@ export const TestCard = () => {
} }
const newList = testList.map((x) => const newList = testList.map((x) =>
x.uid === uid ? { ...x, ...patch } : x x.uid === uid ? { ...x, ...patch } : x,
); );
mutateVerge({ ...verge, test_list: newList }, false); mutateVerge({ ...verge, test_list: newList }, false);
}, },
[testList, verge, mutateVerge] [testList, verge, mutateVerge],
); );
const onDeleteTestListItem = useCallback( const onDeleteTestListItem = useCallback(
@@ -101,7 +101,7 @@ export const TestCard = () => {
patchVerge({ test_list: newList }); patchVerge({ test_list: newList });
mutateVerge({ ...verge, test_list: newList }, false); mutateVerge({ ...verge, test_list: newList }, false);
}, },
[testList, verge, patchVerge, mutateVerge] [testList, verge, patchVerge, mutateVerge],
); );
const onDragEnd = useCallback( const onDragEnd = useCallback(
@@ -122,7 +122,7 @@ export const TestCard = () => {
const patchFn = () => { const patchFn = () => {
try { try {
patchVerge({ test_list: newList }); patchVerge({ test_list: newList });
} catch { } } catch {}
}; };
if (window.requestIdleCallback) { if (window.requestIdleCallback) {
window.requestIdleCallback(patchFn); window.requestIdleCallback(patchFn);
@@ -131,7 +131,7 @@ export const TestCard = () => {
} }
} }
}, },
[testList, verge, mutateVerge, patchVerge] [testList, verge, mutateVerge, patchVerge],
); );
// 仅在verge首次加载时初始化测试列表 // 仅在verge首次加载时初始化测试列表
@@ -142,22 +142,25 @@ export const TestCard = () => {
}, [verge, patchVerge]); }, [verge, patchVerge]);
// 使用useMemo优化UI内容减少渲染计算 // 使用useMemo优化UI内容减少渲染计算
const renderTestItems = useMemo(() => ( const renderTestItems = useMemo(
<Grid container spacing={1} columns={12}> () => (
<SortableContext items={testList.map((x) => x.uid)}> <Grid container spacing={1} columns={12}>
{testList.map((item) => ( <SortableContext items={testList.map((x) => x.uid)}>
<Grid key={item.uid} size={3}> {testList.map((item) => (
<TestItem <Grid key={item.uid} size={3}>
id={item.uid} <TestItem
itemData={item} id={item.uid}
onEdit={() => viewerRef.current?.edit(item)} itemData={item}
onDelete={onDeleteTestListItem} onEdit={() => viewerRef.current?.edit(item)}
/> onDelete={onDeleteTestListItem}
</Grid> />
))} </Grid>
</SortableContext> ))}
</Grid> </SortableContext>
), [testList, onDeleteTestListItem]); </Grid>
),
[testList, onDeleteTestListItem],
);
const handleTestAll = useCallback(() => { const handleTestAll = useCallback(() => {
emit("verge://test-all"); emit("verge://test-all");

View File

@@ -24,7 +24,7 @@ export const UpdateButton = (props: Props) => {
errorRetryCount: 2, errorRetryCount: 2,
revalidateIfStale: false, revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour focusThrottleInterval: 36e5, // 1 hour
} },
); );
if (!updateInfo?.available) return null; if (!updateInfo?.available) return null;

View File

@@ -1,6 +1,9 @@
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material"; import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material";
import { getCurrentWebviewWindow, WebviewWindow } from "@tauri-apps/api/webviewWindow"; import {
getCurrentWebviewWindow,
WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { useSetThemeMode, useThemeMode } from "@/services/states"; import { useSetThemeMode, useThemeMode } from "@/services/states";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -51,13 +54,16 @@ export const useCustomTheme = () => {
const timerId = setTimeout(() => { const timerId = setTimeout(() => {
if (!isMounted) return; if (!isMounted) return;
appWindow.theme().then((systemTheme) => { appWindow
if (isMounted && systemTheme) { .theme()
setMode(systemTheme); .then((systemTheme) => {
} if (isMounted && systemTheme) {
}).catch(err => { setMode(systemTheme);
console.error("Failed to get initial system theme:", err); }
}); })
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
}, 0); }, 0);
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => { const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
@@ -69,13 +75,15 @@ export const useCustomTheme = () => {
return () => { return () => {
isMounted = false; isMounted = false;
clearTimeout(timerId); clearTimeout(timerId);
unlistenPromise.then((unlistenFn) => { unlistenPromise
if (typeof unlistenFn === 'function') { .then((unlistenFn) => {
unlistenFn(); if (typeof unlistenFn === "function") {
} unlistenFn();
}).catch(err => { }
console.error("Failed to unlisten from theme changes:", err); })
}); .catch((err) => {
console.error("Failed to unlisten from theme changes:", err);
});
}; };
}, [theme_mode, appWindow, setMode]); }, [theme_mode, appWindow, setMode]);
@@ -86,7 +94,10 @@ export const useCustomTheme = () => {
if (theme_mode === "system") { if (theme_mode === "system") {
appWindow.setTheme(null).catch((err) => { appWindow.setTheme(null).catch((err) => {
console.error("Failed to set window theme to follow system (setTheme(null)):", err); console.error(
"Failed to set window theme to follow system (setTheme(null)):",
err,
);
}); });
} else if (mode) { } else if (mode) {
appWindow.setTheme(mode as TauriOsTheme).catch((err) => { appWindow.setTheme(mode as TauriOsTheme).catch((err) => {
@@ -153,21 +164,24 @@ export const useCustomTheme = () => {
const rootEle = document.documentElement; const rootEle = document.documentElement;
if (rootEle) { if (rootEle) {
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d"; const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5"; const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee"; const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const dividerColor = const dividerColor =
mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)"; mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";
rootEle.style.setProperty("--divider-color", dividerColor); rootEle.style.setProperty("--divider-color", dividerColor);
rootEle.style.setProperty("--background-color", backgroundColor); rootEle.style.setProperty("--background-color", backgroundColor);
rootEle.style.setProperty("--selection-color", selectColor); rootEle.style.setProperty("--selection-color", selectColor);
rootEle.style.setProperty("--scroller-color", scrollColor); rootEle.style.setProperty("--scroller-color", scrollColor);
rootEle.style.setProperty("--primary-main", muiTheme.palette.primary.main); rootEle.style.setProperty(
rootEle.style.setProperty( "--primary-main",
muiTheme.palette.primary.main,
);
rootEle.style.setProperty(
"--background-color-alpha", "--background-color-alpha",
alpha(muiTheme.palette.primary.main, 0.1), alpha(muiTheme.palette.primary.main, 0.1),
); );
} }
// inject css // inject css
let styleElement = document.querySelector("style#verge-theme"); let styleElement = document.querySelector("style#verge-theme");

View File

@@ -127,7 +127,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
currData.current = value; currData.current = value;
onChange?.(prevData.current, currData.current); onChange?.(prevData.current, currData.current);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -136,7 +136,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
!readOnly && onSave?.(prevData.current, currData.current); !readOnly && onSave?.(prevData.current, currData.current);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -144,7 +144,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
try { try {
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });

View File

@@ -70,8 +70,8 @@ export const GroupItem = (props: Props) => {
? alpha(palette.background.paper, 0.3) ? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3) : alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.3) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3), : alpha(palette.success.main, 0.3),
height: "100%", height: "100%",
margin: "8px 0", margin: "8px 0",
borderRadius: "8px", borderRadius: "8px",

View File

@@ -90,27 +90,27 @@ export const GroupsEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo( const filteredPrependSeq = useMemo(
() => prependSeq.filter((group) => match(group.name)), () => prependSeq.filter((group) => match(group.name)),
[prependSeq, match] [prependSeq, match],
); );
const filteredGroupList = useMemo( const filteredGroupList = useMemo(
() => groupList.filter((group) => match(group.name)), () => groupList.filter((group) => match(group.name)),
[groupList, match] [groupList, match],
); );
const filteredAppendSeq = useMemo( const filteredAppendSeq = useMemo(
() => appendSeq.filter((group) => match(group.name)), () => appendSeq.filter((group) => match(group.name)),
[appendSeq, match] [appendSeq, match],
); );
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const reorder = ( const reorder = (
list: IProxyGroupConfig[], list: IProxyGroupConfig[],
startIndex: number, startIndex: number,
endIndex: number endIndex: number,
) => { ) => {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
@@ -188,8 +188,8 @@ export const GroupsEditorViewer = (props: Props) => {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true } { forceQuotes: true },
) ),
); );
} catch (e) { } catch (e) {
// 防止异常导致UI卡死 // 防止异常导致UI卡死
@@ -226,7 +226,7 @@ export const GroupsEditorViewer = (props: Props) => {
return !moreDeleteProxies.includes(proxy); return !moreDeleteProxies.includes(proxy);
} }
}), }),
moreAppendProxies moreAppendProxies,
); );
setProxyPolicyList( setProxyPolicyList(
@@ -236,8 +236,8 @@ export const GroupsEditorViewer = (props: Props) => {
.map((group: IProxyGroupConfig) => group.name) .map((group: IProxyGroupConfig) => group.name)
.filter((name) => !deleteSeq.includes(name)) || [], .filter((name) => !deleteSeq.includes(name)) || [],
appendSeq.map((group: IProxyGroupConfig) => group.name), appendSeq.map((group: IProxyGroupConfig) => group.name),
proxies.map((proxy: any) => proxy.name) proxies.map((proxy: any) => proxy.name),
) ),
); );
}; };
const fetchProfile = async () => { const fetchProfile = async () => {
@@ -266,7 +266,7 @@ export const GroupsEditorViewer = (props: Props) => {
{}, {},
originProvider, originProvider,
moreProvider, moreProvider,
globalProvider globalProvider,
); );
setProxyProviderList(Object.keys(provider)); setProxyProviderList(Object.keys(provider));
@@ -297,11 +297,11 @@ export const GroupsEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
await saveProfileFile(property, currData); await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully")); showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData); onSave?.(prevData, currData);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -502,7 +502,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("seconds")} {t("seconds")}
</InputAdornment> </InputAdornment>
), ),
} },
}} }}
/> />
</Item> </Item>
@@ -530,7 +530,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("millis")} {t("millis")}
</InputAdornment> </InputAdornment>
), ),
} },
}} }}
/> />
</Item> </Item>
@@ -742,7 +742,7 @@ export const GroupsEditorViewer = (props: Props) => {
} }
setPrependSeq([formIns.getValues(), ...prependSeq]); setPrependSeq([formIns.getValues(), ...prependSeq]);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}} }}
> >
@@ -764,7 +764,7 @@ export const GroupsEditorViewer = (props: Props) => {
} }
setAppendSeq([...appendSeq, formIns.getValues()]); setAppendSeq([...appendSeq, formIns.getValues()]);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}} }}
> >
@@ -811,8 +811,8 @@ export const GroupsEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setPrependSeq( setPrependSeq(
prependSeq.filter( prependSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -838,8 +838,8 @@ export const GroupsEditorViewer = (props: Props) => {
) { ) {
setDeleteSeq( setDeleteSeq(
deleteSeq.filter( deleteSeq.filter(
(v) => v !== filteredGroupList[newIndex].name (v) => v !== filteredGroupList[newIndex].name,
) ),
); );
} else { } else {
setDeleteSeq((prev) => [ setDeleteSeq((prev) => [
@@ -871,8 +871,8 @@ export const GroupsEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setAppendSeq( setAppendSeq(
appendSeq.filter( appendSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -906,8 +906,9 @@ export const GroupsEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets top: 33, // 顶部padding防止遮挡snippets
}, },
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : "" fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
}`, getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}

View File

@@ -1,57 +1,58 @@
import { alpha, Box, styled } from "@mui/material"; import { alpha, Box, styled } from "@mui/material";
export const ProfileBox = styled(Box)( export const ProfileBox = styled(Box)(({
({ theme, "aria-selected": selected }) => { theme,
const { mode, primary, text } = theme.palette; "aria-selected": selected,
const key = `${mode}-${!!selected}`; }) => {
const { mode, primary, text } = theme.palette;
const key = `${mode}-${!!selected}`;
const backgroundColor = mode === "light" ? "#ffffff" : "#282A36"; const backgroundColor = mode === "light" ? "#ffffff" : "#282A36";
const color = { const color = {
"light-true": text.secondary, "light-true": text.secondary,
"light-false": text.secondary, "light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.65), "dark-true": alpha(text.secondary, 0.65),
"dark-false": alpha(text.secondary, 0.65), "dark-false": alpha(text.secondary, 0.65),
}[key]!; }[key]!;
const h2color = { const h2color = {
"light-true": primary.main, "light-true": primary.main,
"light-false": text.primary, "light-false": text.primary,
"dark-true": primary.main, "dark-true": primary.main,
"dark-false": text.primary, "dark-false": text.primary,
}[key]!; }[key]!;
const borderSelect = { const borderSelect = {
"light-true": { "light-true": {
borderLeft: `3px solid ${primary.main}`, borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`, width: `calc(100% + 3px)`,
marginLeft: `-3px`, marginLeft: `-3px`,
}, },
"light-false": { "light-false": {
width: "100%", width: "100%",
}, },
"dark-true": { "dark-true": {
borderLeft: `3px solid ${primary.main}`, borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`, width: `calc(100% + 3px)`,
marginLeft: `-3px`, marginLeft: `-3px`,
}, },
"dark-false": { "dark-false": {
width: "100%", width: "100%",
}, },
}[key]; }[key];
return { return {
position: "relative", position: "relative",
display: "block", display: "block",
cursor: "pointer", cursor: "pointer",
textAlign: "left", textAlign: "left",
padding: "8px 16px", padding: "8px 16px",
boxSizing: "border-box", boxSizing: "border-box",
backgroundColor, backgroundColor,
...borderSelect, ...borderSelect,
borderRadius: "8px", borderRadius: "8px",
color, color,
"& h2": { color: h2color }, "& h2": { color: h2color },
}; };
} });
);

View File

@@ -75,7 +75,10 @@ export const ProfileItem = (props: Props) => {
// 获取下次更新时间的函数 // 获取下次更新时间的函数
const fetchNextUpdateTime = useLockFn(async (forceRefresh = false) => { const fetchNextUpdateTime = useLockFn(async (forceRefresh = false) => {
if (itemData.option?.update_interval && itemData.option.update_interval > 0) { if (
itemData.option?.update_interval &&
itemData.option.update_interval > 0
) {
try { try {
console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`); console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`);
@@ -97,7 +100,7 @@ export const ProfileItem = (props: Props) => {
setNextUpdateTime(t("Last Update failed")); setNextUpdateTime(t("Last Update failed"));
} else { } else {
// 否则显示剩余时间 // 否则显示剩余时间
const diffMinutes = nextUpdateDate.diff(now, 'minute'); const diffMinutes = nextUpdateDate.diff(now, "minute");
if (diffMinutes < 60) { if (diffMinutes < 60) {
if (diffMinutes <= 0) { if (diffMinutes <= 0) {
@@ -159,11 +162,17 @@ export const ProfileItem = (props: Props) => {
}; };
// 只注册定时器更新事件监听 // 只注册定时器更新事件监听
window.addEventListener('verge://timer-updated', handleTimerUpdate as EventListener); window.addEventListener(
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
return () => { return () => {
// 清理事件监听 // 清理事件监听
window.removeEventListener('verge://timer-updated', handleTimerUpdate as EventListener); window.removeEventListener(
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
}; };
}, [showNextUpdate, itemData.uid]); }, [showNextUpdate, itemData.uid]);
@@ -271,7 +280,7 @@ export const ProfileItem = (props: Props) => {
try { try {
await viewProfile(itemData.uid); await viewProfile(itemData.uid);
} catch (err: any) { } catch (err: any) {
showNotice('error', err?.message || err.toString()); showNotice("error", err?.message || err.toString());
} }
}); });
@@ -302,7 +311,7 @@ export const ProfileItem = (props: Props) => {
await updateProfile(itemData.uid, option); await updateProfile(itemData.uid, option);
// 更新成功,刷新列表 // 更新成功,刷新列表
showNotice('success', t("Update subscription successfully")); showNotice("success", t("Update subscription successfully"));
mutate("getProfiles"); mutate("getProfiles");
} catch (err: any) { } catch (err: any) {
// 更新完全失败(包括后端的回退尝试) // 更新完全失败(包括后端的回退尝试)
@@ -421,13 +430,25 @@ export const ProfileItem = (props: Props) => {
}; };
// 注册事件监听 // 注册事件监听
window.addEventListener('profile-update-started', handleUpdateStarted as EventListener); window.addEventListener(
window.addEventListener('profile-update-completed', handleUpdateCompleted as EventListener); "profile-update-started",
handleUpdateStarted as EventListener,
);
window.addEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
return () => { return () => {
// 清理事件监听 // 清理事件监听
window.removeEventListener('profile-update-started', handleUpdateStarted as EventListener); window.removeEventListener(
window.removeEventListener('profile-update-completed', handleUpdateCompleted as EventListener); "profile-update-started",
handleUpdateStarted as EventListener,
);
window.removeEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
}; };
}, [itemData.uid, showNextUpdate]); }, [itemData.uid, showNextUpdate]);
@@ -541,13 +562,23 @@ export const ProfileItem = (props: Props) => {
) )
)} )}
{hasUrl && ( {hasUrl && (
<Box sx={{ display: "flex", justifyContent: "flex-end", ml: "auto" }}> <Box
sx={{
display: "flex",
justifyContent: "flex-end",
ml: "auto",
}}
>
<Typography <Typography
noWrap noWrap
component="span" component="span"
fontSize={14} fontSize={14}
textAlign="right" textAlign="right"
title={showNextUpdate ? t("Click to show last update time") : `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`} title={
showNextUpdate
? t("Click to show last update time")
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
}
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
display: "inline-block", display: "inline-block",
@@ -556,13 +587,15 @@ export const ProfileItem = (props: Props) => {
"&:hover": { "&:hover": {
borderBottomColor: "primary.main", borderBottomColor: "primary.main",
color: "primary.main", color: "primary.main",
} },
}} }}
onClick={toggleUpdateTimeDisplay} onClick={toggleUpdateTimeDisplay}
> >
{showNextUpdate {showNextUpdate
? nextUpdateTime ? nextUpdateTime
: (updated > 0 ? dayjs(updated * 1000).fromNow() : "")} : updated > 0
? dayjs(updated * 1000).fromNow()
: ""}
</Typography> </Typography>
</Box> </Box>
)} )}

View File

@@ -43,7 +43,7 @@ export const ProfileMore = (props: Props) => {
try { try {
await viewProfile(id); await viewProfile(id);
} catch (err: any) { } catch (err: any) {
showNotice('error', err?.message || err.toString()); showNotice("error", err?.message || err.toString());
} }
}); });

View File

@@ -201,7 +201,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
setOpen(false); setOpen(false);
fileDataRef.current = null; fileDataRef.current = null;
setTimeout(() => formIns.reset(), 500); setTimeout(() => formIns.reset(), 500);
} catch { } } catch {}
}; };
const text = { const text = {

View File

@@ -66,27 +66,27 @@ export const ProxiesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo( const filteredPrependSeq = useMemo(
() => prependSeq.filter((proxy) => match(proxy.name)), () => prependSeq.filter((proxy) => match(proxy.name)),
[prependSeq, match] [prependSeq, match],
); );
const filteredProxyList = useMemo( const filteredProxyList = useMemo(
() => proxyList.filter((proxy) => match(proxy.name)), () => proxyList.filter((proxy) => match(proxy.name)),
[proxyList, match] [proxyList, match],
); );
const filteredAppendSeq = useMemo( const filteredAppendSeq = useMemo(
() => appendSeq.filter((proxy) => match(proxy.name)), () => appendSeq.filter((proxy) => match(proxy.name)),
[appendSeq, match] [appendSeq, match],
); );
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const reorder = ( const reorder = (
list: IProxyConfig[], list: IProxyConfig[],
startIndex: number, startIndex: number,
endIndex: number endIndex: number,
) => { ) => {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
@@ -208,8 +208,8 @@ export const ProxiesEditorViewer = (props: Props) => {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true } { forceQuotes: true },
) ),
); );
} catch (e) { } catch (e) {
// 防止异常导致UI卡死 // 防止异常导致UI卡死
@@ -232,11 +232,11 @@ export const ProxiesEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
await saveProfileFile(property, currData); await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully")); showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData); onSave?.(prevData, currData);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -358,8 +358,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setPrependSeq( setPrependSeq(
prependSeq.filter( prependSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -385,8 +385,8 @@ export const ProxiesEditorViewer = (props: Props) => {
) { ) {
setDeleteSeq( setDeleteSeq(
deleteSeq.filter( deleteSeq.filter(
(v) => v !== filteredProxyList[newIndex].name (v) => v !== filteredProxyList[newIndex].name,
) ),
); );
} else { } else {
setDeleteSeq((prev) => [ setDeleteSeq((prev) => [
@@ -418,8 +418,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setAppendSeq( setAppendSeq(
appendSeq.filter( appendSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -453,8 +453,9 @@ export const ProxiesEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets top: 33, // 顶部padding防止遮挡snippets
}, },
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : "" fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
}`, getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}

View File

@@ -49,8 +49,8 @@ export const ProxyItem = (props: Props) => {
? alpha(palette.background.paper, 0.3) ? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3) : alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.3) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3), : alpha(palette.success.main, 0.3),
height: "100%", height: "100%",
margin: "8px 0", margin: "8px 0",
borderRadius: "8px", borderRadius: "8px",

View File

@@ -52,8 +52,8 @@ export const RuleItem = (props: Props) => {
? alpha(palette.background.paper, 0.3) ? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3) : alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.3) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3), : alpha(palette.success.main, 0.3),
height: "100%", height: "100%",
margin: "8px 0", margin: "8px 0",
borderRadius: "8px", borderRadius: "8px",

View File

@@ -55,17 +55,17 @@ interface Props {
const portValidator = (value: string): boolean => { const portValidator = (value: string): boolean => {
return new RegExp( return new RegExp(
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$" "^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$",
).test(value); ).test(value);
}; };
const ipv4CIDRValidator = (value: string): boolean => { const ipv4CIDRValidator = (value: string): boolean => {
return new RegExp( return new RegExp(
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$" "^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$",
).test(value); ).test(value);
}; };
const ipv6CIDRValidator = (value: string): boolean => { const ipv6CIDRValidator = (value: string): boolean => {
return new RegExp( return new RegExp(
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$" "^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$",
).test(value); ).test(value);
}; };
@@ -76,161 +76,161 @@ const rules: {
noResolve?: boolean; noResolve?: boolean;
validator?: (value: string) => boolean; validator?: (value: string) => boolean;
}[] = [ }[] = [
{ {
name: "DOMAIN", name: "DOMAIN",
example: "example.com", example: "example.com",
}, },
{ {
name: "DOMAIN-SUFFIX", name: "DOMAIN-SUFFIX",
example: "example.com", example: "example.com",
}, },
{ {
name: "DOMAIN-KEYWORD", name: "DOMAIN-KEYWORD",
example: "example", example: "example",
}, },
{ {
name: "DOMAIN-REGEX", name: "DOMAIN-REGEX",
example: "example.*", example: "example.*",
}, },
{ {
name: "GEOSITE", name: "GEOSITE",
example: "youtube", example: "youtube",
}, },
{ {
name: "GEOIP", name: "GEOIP",
example: "CN", example: "CN",
noResolve: true, noResolve: true,
}, },
{ {
name: "SRC-GEOIP", name: "SRC-GEOIP",
example: "CN", example: "CN",
}, },
{ {
name: "IP-ASN", name: "IP-ASN",
example: "13335", example: "13335",
noResolve: true, noResolve: true,
validator: (value) => (+value ? true : false), validator: (value) => (+value ? true : false),
}, },
{ {
name: "SRC-IP-ASN", name: "SRC-IP-ASN",
example: "9808", example: "9808",
validator: (value) => (+value ? true : false), validator: (value) => (+value ? true : false),
}, },
{ {
name: "IP-CIDR", name: "IP-CIDR",
example: "127.0.0.0/8", example: "127.0.0.0/8",
noResolve: true, noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "IP-CIDR6", name: "IP-CIDR6",
example: "2620:0:2d0:200::7/32", example: "2620:0:2d0:200::7/32",
noResolve: true, noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "SRC-IP-CIDR", name: "SRC-IP-CIDR",
example: "192.168.1.201/32", example: "192.168.1.201/32",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "IP-SUFFIX", name: "IP-SUFFIX",
example: "8.8.8.8/24", example: "8.8.8.8/24",
noResolve: true, noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "SRC-IP-SUFFIX", name: "SRC-IP-SUFFIX",
example: "192.168.1.201/8", example: "192.168.1.201/8",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "SRC-PORT", name: "SRC-PORT",
example: "7777", example: "7777",
validator: (value) => portValidator(value), validator: (value) => portValidator(value),
}, },
{ {
name: "DST-PORT", name: "DST-PORT",
example: "80", example: "80",
validator: (value) => portValidator(value), validator: (value) => portValidator(value),
}, },
{ {
name: "IN-PORT", name: "IN-PORT",
example: "7890", example: "7890",
validator: (value) => portValidator(value), validator: (value) => portValidator(value),
}, },
{ {
name: "DSCP", name: "DSCP",
example: "4", example: "4",
}, },
{ {
name: "PROCESS-NAME", name: "PROCESS-NAME",
example: getSystem() === "windows" ? "chrome.exe" : "curl", example: getSystem() === "windows" ? "chrome.exe" : "curl",
}, },
{ {
name: "PROCESS-PATH", name: "PROCESS-PATH",
example: example:
getSystem() === "windows" getSystem() === "windows"
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" ? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
: "/usr/bin/wget", : "/usr/bin/wget",
}, },
{ {
name: "PROCESS-NAME-REGEX", name: "PROCESS-NAME-REGEX",
example: ".*telegram.*", example: ".*telegram.*",
}, },
{ {
name: "PROCESS-PATH-REGEX", name: "PROCESS-PATH-REGEX",
example: example:
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget", getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
}, },
{ {
name: "NETWORK", name: "NETWORK",
example: "udp", example: "udp",
validator: (value) => ["tcp", "udp"].includes(value), validator: (value) => ["tcp", "udp"].includes(value),
}, },
{ {
name: "UID", name: "UID",
example: "1001", example: "1001",
validator: (value) => (+value ? true : false), validator: (value) => (+value ? true : false),
}, },
{ {
name: "IN-TYPE", name: "IN-TYPE",
example: "SOCKS/HTTP", example: "SOCKS/HTTP",
}, },
{ {
name: "IN-USER", name: "IN-USER",
example: "mihomo", example: "mihomo",
}, },
{ {
name: "IN-NAME", name: "IN-NAME",
example: "ss", example: "ss",
}, },
{ {
name: "SUB-RULE", name: "SUB-RULE",
example: "(NETWORK,tcp)", example: "(NETWORK,tcp)",
}, },
{ {
name: "RULE-SET", name: "RULE-SET",
example: "providername", example: "providername",
noResolve: true, noResolve: true,
}, },
{ {
name: "AND", name: "AND",
example: "((DOMAIN,baidu.com),(NETWORK,UDP))", example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
}, },
{ {
name: "OR", name: "OR",
example: "((NETWORK,UDP),(DOMAIN,baidu.com))", example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
}, },
{ {
name: "NOT", name: "NOT",
example: "((DOMAIN,baidu.com))", example: "((DOMAIN,baidu.com))",
}, },
{ {
name: "MATCH", name: "MATCH",
required: false, required: false,
}, },
]; ];
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
@@ -260,22 +260,22 @@ export const RulesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo( const filteredPrependSeq = useMemo(
() => prependSeq.filter((rule) => match(rule)), () => prependSeq.filter((rule) => match(rule)),
[prependSeq, match] [prependSeq, match],
); );
const filteredRuleList = useMemo( const filteredRuleList = useMemo(
() => ruleList.filter((rule) => match(rule)), () => ruleList.filter((rule) => match(rule)),
[ruleList, match] [ruleList, match],
); );
const filteredAppendSeq = useMemo( const filteredAppendSeq = useMemo(
() => appendSeq.filter((rule) => match(rule)), () => appendSeq.filter((rule) => match(rule)),
[appendSeq, match] [appendSeq, match],
); );
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const reorder = (list: string[], startIndex: number, endIndex: number) => { const reorder = (list: string[], startIndex: number, endIndex: number) => {
const result = Array.from(list); const result = Array.from(list);
@@ -333,11 +333,11 @@ export const RulesEditorViewer = (props: Props) => {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true } { forceQuotes: true },
) ),
); );
} catch (e: any) { } catch (e: any) {
showNotice('error', e?.message || e?.toString() || 'YAML dump error'); showNotice("error", e?.message || e?.toString() || "YAML dump error");
} }
}; };
if (window.requestIdleCallback) { if (window.requestIdleCallback) {
@@ -371,7 +371,7 @@ export const RulesEditorViewer = (props: Props) => {
return !moreDeleteGroups.includes(group); return !moreDeleteGroups.includes(group);
} }
}), }),
moreAppendGroups moreAppendGroups,
); );
let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null; let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null;
@@ -396,7 +396,7 @@ export const RulesEditorViewer = (props: Props) => {
let globalSubRule = globalSubRuleObj?.["sub-rules"] || {}; let globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule); let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule);
setProxyPolicyList( setProxyPolicyList(
builtinProxyPolicies.concat(groups.map((group: any) => group.name)) builtinProxyPolicies.concat(groups.map((group: any) => group.name)),
); );
setRuleSetList(Object.keys(ruleSet)); setRuleSetList(Object.keys(ruleSet));
setSubRuleList(Object.keys(subRule)); setSubRuleList(Object.keys(subRule));
@@ -417,19 +417,20 @@ export const RulesEditorViewer = (props: Props) => {
throw new Error(t("Invalid Rule")); throw new Error(t("Invalid Rule"));
} }
const condition = ruleType.required ?? true ? ruleContent : ""; const condition = (ruleType.required ?? true) ? ruleContent : "";
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : "" return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
}`; ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`;
}; };
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
await saveProfileFile(property, currData); await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully")); showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData); onSave?.(prevData, currData);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -557,7 +558,7 @@ export const RulesEditorViewer = (props: Props) => {
if (prependSeq.includes(raw)) return; if (prependSeq.includes(raw)) return;
setPrependSeq([raw, ...prependSeq]); setPrependSeq([raw, ...prependSeq]);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}} }}
> >
@@ -575,7 +576,7 @@ export const RulesEditorViewer = (props: Props) => {
if (appendSeq.includes(raw)) return; if (appendSeq.includes(raw)) return;
setAppendSeq([...appendSeq, raw]); setAppendSeq([...appendSeq, raw]);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}} }}
> >
@@ -621,7 +622,7 @@ export const RulesEditorViewer = (props: Props) => {
ruleRaw={item} ruleRaw={item}
onDelete={() => { onDelete={() => {
setPrependSeq( setPrependSeq(
prependSeq.filter((v) => v !== item) prependSeq.filter((v) => v !== item),
); );
}} }}
/> />
@@ -645,8 +646,8 @@ export const RulesEditorViewer = (props: Props) => {
if (deleteSeq.includes(filteredRuleList[newIndex])) { if (deleteSeq.includes(filteredRuleList[newIndex])) {
setDeleteSeq( setDeleteSeq(
deleteSeq.filter( deleteSeq.filter(
(v) => v !== filteredRuleList[newIndex] (v) => v !== filteredRuleList[newIndex],
) ),
); );
} else { } else {
setDeleteSeq((prev) => [ setDeleteSeq((prev) => [
@@ -677,7 +678,7 @@ export const RulesEditorViewer = (props: Props) => {
ruleRaw={item} ruleRaw={item}
onDelete={() => { onDelete={() => {
setAppendSeq( setAppendSeq(
appendSeq.filter((v) => v !== item) appendSeq.filter((v) => v !== item),
); );
}} }}
/> />
@@ -711,8 +712,9 @@ export const RulesEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets top: 33, // 顶部padding防止遮挡snippets
}, },
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : "" fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
}`, getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}

View File

@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Button, Button,
Box, Box,
Dialog, Dialog,
@@ -15,7 +15,7 @@ import {
LinearProgress, LinearProgress,
alpha, alpha,
styled, styled,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
@@ -65,77 +65,83 @@ export const ProviderButton = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData(); const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
const [updating, setUpdating] = useState<Record<string, boolean>>({}); const [updating, setUpdating] = useState<Record<string, boolean>>({});
// 检查是否有提供者 // 检查是否有提供者
const hasProviders = Object.keys(proxyProviders || {}).length > 0; const hasProviders = Object.keys(proxyProviders || {}).length > 0;
// 更新单个代理提供者 // 更新单个代理提供者
const updateProvider = useLockFn(async (name: string) => { const updateProvider = useLockFn(async (name: string) => {
try { try {
// 设置更新状态 // 设置更新状态
setUpdating(prev => ({ ...prev, [name]: true })); setUpdating((prev) => ({ ...prev, [name]: true }));
await proxyProviderUpdate(name); await proxyProviderUpdate(name);
// 刷新数据 // 刷新数据
await refreshProxy(); await refreshProxy();
await refreshProxyProviders(); await refreshProxyProviders();
showNotice('success', `${name} 更新成功`); showNotice("success", `${name} 更新成功`);
} catch (err: any) { } catch (err: any) {
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`); showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
} finally { } finally {
// 清除更新状态 // 清除更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} }
}); });
// 更新所有代理提供者 // 更新所有代理提供者
const updateAllProviders = useLockFn(async () => { const updateAllProviders = useLockFn(async () => {
try { try {
// 获取所有provider的名称 // 获取所有provider的名称
const allProviders = Object.keys(proxyProviders || {}); const allProviders = Object.keys(proxyProviders || {});
if (allProviders.length === 0) { if (allProviders.length === 0) {
showNotice('info', "没有可更新的代理提供者"); showNotice("info", "没有可更新的代理提供者");
return; return;
} }
// 设置所有provider为更新中状态 // 设置所有provider为更新中状态
const newUpdating = allProviders.reduce((acc, key) => { const newUpdating = allProviders.reduce(
acc[key] = true; (acc, key) => {
return acc; acc[key] = true;
}, {} as Record<string, boolean>); return acc;
},
{} as Record<string, boolean>,
);
setUpdating(newUpdating); setUpdating(newUpdating);
// 改为串行逐个更新所有provider // 改为串行逐个更新所有provider
for (const name of allProviders) { for (const name of allProviders) {
try { try {
await proxyProviderUpdate(name); await proxyProviderUpdate(name);
// 每个更新完成后更新状态 // 每个更新完成后更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) { } catch (err) {
console.error(`更新 ${name} 失败`, err); console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程 // 继续执行下一个,不中断整体流程
} }
} }
// 刷新数据 // 刷新数据
await refreshProxy(); await refreshProxy();
await refreshProxyProviders(); await refreshProxyProviders();
showNotice('success', "全部代理提供者更新成功"); showNotice("success", "全部代理提供者更新成功");
} catch (err: any) { } catch (err: any) {
showNotice('error', `更新失败: ${err?.message || err.toString()}`); showNotice("error", `更新失败: ${err?.message || err.toString()}`);
} finally { } finally {
// 清除所有更新状态 // 清除所有更新状态
setUpdating({}); setUpdating({});
} }
}); });
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
}; };
if (!hasProviders) return null; if (!hasProviders) return null;
return ( return (
@@ -149,15 +155,14 @@ export const ProviderButton = () => {
> >
{t("Proxy Provider")} {t("Proxy Provider")}
</Button> </Button>
<Dialog <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<DialogTitle> <DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Proxy Provider")}</Typography> <Typography variant="h6">{t("Proxy Provider")}</Typography>
<Box> <Box>
<Button <Button
@@ -170,14 +175,14 @@ export const ProviderButton = () => {
</Box> </Box>
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<List sx={{ py: 0, minHeight: 250 }}> <List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(proxyProviders || {}).map(([key, item]) => { {Object.entries(proxyProviders || {}).map(([key, item]) => {
const provider = item as ProxyProviderItem; const provider = item as ProxyProviderItem;
const time = dayjs(provider.updatedAt); const time = dayjs(provider.updatedAt);
const isUpdating = updating[key]; const isUpdating = updating[key];
// 订阅信息 // 订阅信息
const sub = provider.subscriptionInfo; const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub; const hasSubInfo = !!sub;
@@ -185,46 +190,53 @@ export const ProviderButton = () => {
const download = sub?.Download || 0; const download = sub?.Download || 0;
const total = sub?.Total || 0; const total = sub?.Total || 0;
const expire = sub?.Expire || 0; const expire = sub?.Expire || 0;
// 流量使用进度 // 流量使用进度
const progress = total > 0 const progress =
? Math.min(Math.round(((download + upload) * 100) / total) + 1, 100) total > 0
: 0; ? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
return ( return (
<ListItem <ListItem
key={key} key={key}
sx={[ sx={[
{ {
p: 0, p: 0,
mb: "8px", mb: "8px",
borderRadius: 2, borderRadius: 2,
overflow: "hidden", overflow: "hidden",
transition: "all 0.2s" transition: "all 0.2s",
}, },
({ palette: { mode, primary } }) => { ({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor = mode === "light" const hoverColor =
? alpha(primary.main, 0.1) mode === "light"
: alpha(primary.main, 0.2); ? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return { return {
backgroundColor: bgcolor, backgroundColor: bgcolor,
"&:hover": { "&:hover": {
backgroundColor: hoverColor, backgroundColor: hoverColor,
} },
}; };
} },
]} ]}
> >
<ListItemText <ListItemText
sx={{ px: 2, py: 1 }} sx={{ px: 2, py: 1 }}
primary={ primary={
<Box sx={{ <Box
display: "flex", sx={{
justifyContent: "space-between", display: "flex",
alignItems: "center", justifyContent: "space-between",
}}> alignItems: "center",
}}
>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
component="div" component="div"
@@ -232,7 +244,7 @@ export const ProviderButton = () => {
title={key} title={key}
sx={{ display: "flex", alignItems: "center" }} sx={{ display: "flex", alignItems: "center" }}
> >
<span style={{ marginRight: "8px" }}>{key}</span> <span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span"> <TypeBox component="span">
{provider.proxies.length} {provider.proxies.length}
</TypeBox> </TypeBox>
@@ -240,9 +252,14 @@ export const ProviderButton = () => {
{provider.vehicleType} {provider.vehicleType}
</TypeBox> </TypeBox>
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" noWrap> <Typography
<small>{t("Update At")}: </small>{time.fromNow()} variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography> </Typography>
</Box> </Box>
} }
@@ -251,26 +268,29 @@ export const ProviderButton = () => {
{/* 订阅信息 */} {/* 订阅信息 */}
{hasSubInfo && ( {hasSubInfo && (
<> <>
<Box sx={{ <Box
mb: 1, sx={{
display: "flex", mb: 1,
alignItems: "center", display: "flex",
justifyContent: "space-between", alignItems: "center",
}}> justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}> <span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} / {parseTraffic(total)} {parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span> </span>
<span title={t("Expire Time") as string}> <span title={t("Expire Time") as string}>
{parseExpire(expire)} {parseExpire(expire)}
</span> </span>
</Box> </Box>
{/* 进度条 */} {/* 进度条 */}
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={progress} value={progress}
sx={{ sx={{
height: 6, height: 6,
borderRadius: 3, borderRadius: 3,
opacity: total > 0 ? 1 : 0, opacity: total > 0 ? 1 : 0,
}} }}
@@ -281,12 +301,14 @@ export const ProviderButton = () => {
} }
/> />
<Divider orientation="vertical" flexItem /> <Divider orientation="vertical" flexItem />
<Box sx={{ <Box
width: 40, sx={{
display: "flex", width: 40,
justifyContent: "center", display: "flex",
alignItems: "center" justifyContent: "center",
}}> alignItems: "center",
}}
>
<IconButton <IconButton
size="small" size="small"
color="primary" color="primary"
@@ -295,11 +317,13 @@ export const ProviderButton = () => {
}} }}
disabled={isUpdating} disabled={isUpdating}
sx={{ sx={{
animation: isUpdating ? "spin 1s linear infinite" : "none", animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": { "@keyframes spin": {
"0%": { transform: "rotate(0deg)" }, "0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" } "100%": { transform: "rotate(360deg)" },
} },
}} }}
title={t("Update Provider") as string} title={t("Update Provider") as string}
> >
@@ -311,7 +335,7 @@ export const ProviderButton = () => {
})} })}
</List> </List>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose} variant="outlined"> <Button onClick={handleClose} variant="outlined">
{t("Close")} {t("Close")}

View File

@@ -31,7 +31,10 @@ interface RenderProps {
onLocation: (group: IRenderItem["group"]) => void; onLocation: (group: IRenderItem["group"]) => void;
onCheckAll: (groupName: string) => void; onCheckAll: (groupName: string) => void;
onHeadState: (groupName: string, patch: Partial<HeadState>) => void; onHeadState: (groupName: string, patch: Partial<HeadState>) => void;
onChangeProxy: (group: IRenderItem["group"], proxy: IRenderItem["proxy"] & { name: string }) => void; onChangeProxy: (
group: IRenderItem["group"],
proxy: IRenderItem["proxy"] & { name: string },
) => void;
} }
export const ProxyRender = (props: RenderProps) => { export const ProxyRender = (props: RenderProps) => {
@@ -129,14 +132,15 @@ export const ProxyRender = (props: RenderProps) => {
/> />
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: "flex", alignItems: "center" }}>
<Tooltip title={t("Proxy Count")} arrow> <Tooltip title={t("Proxy Count")} arrow>
<Chip <Chip
size="small" size="small"
label={`${group.all.length}`} label={`${group.all.length}`}
sx={{ sx={{
mr: 1, mr: 1,
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.1), backgroundColor: (theme) =>
alpha(theme.palette.primary.main, 0.1),
color: (theme) => theme.palette.primary.main, color: (theme) => theme.palette.primary.main,
}} }}
/> />
</Tooltip> </Tooltip>
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />} {headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}

View File

@@ -8,7 +8,7 @@ export default function useFilterSort(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
filterText: string, filterText: string,
sortType: ProxySortType sortType: ProxySortType,
) { ) {
const [refresh, setRefresh] = useState({}); const [refresh, setRefresh] = useState({});
@@ -40,7 +40,7 @@ export function filterSort(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
filterText: string, filterText: string,
sortType: ProxySortType sortType: ProxySortType,
) { ) {
const fp = filterProxies(proxies, groupName, filterText); const fp = filterProxies(proxies, groupName, filterText);
const sp = sortProxies(fp, groupName, sortType); const sp = sortProxies(fp, groupName, sortType);
@@ -60,7 +60,7 @@ const regex2 = /type=(.*)/i;
function filterProxies( function filterProxies(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
filterText: string filterText: string,
) { ) {
if (!filterText) return proxies; if (!filterText) return proxies;
@@ -100,7 +100,7 @@ function filterProxies(
function sortProxies( function sortProxies(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
sortType: ProxySortType sortType: ProxySortType,
) { ) {
if (!proxies) return []; if (!proxies) return [];
if (sortType === 0) return proxies; if (sortType === 0) return proxies;

View File

@@ -37,7 +37,7 @@ export function useHeadStateNew() {
try { try {
const data = JSON.parse( const data = JSON.parse(
localStorage.getItem(HEAD_STATE_KEY)! localStorage.getItem(HEAD_STATE_KEY)!,
) as HeadStateStorage; ) as HeadStateStorage;
const value = data[current] || {}; const value = data[current] || {};
@@ -74,7 +74,7 @@ export function useHeadStateNew() {
return ret; return ret;
}); });
}, },
[current] [current],
); );
return [state, setHeadState] as const; return [state, setHeadState] as const;

View File

@@ -1,9 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Button, Button,
Box, Box,
Dialog, Dialog,
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogActions, DialogActions,
IconButton, IconButton,
@@ -14,7 +14,7 @@ import {
Divider, Divider,
alpha, alpha,
styled, styled,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
@@ -54,74 +54,80 @@ export const ProviderButton = () => {
// 检查是否有提供者 // 检查是否有提供者
const hasProviders = Object.keys(ruleProviders || {}).length > 0; const hasProviders = Object.keys(ruleProviders || {}).length > 0;
// 更新单个规则提供者 // 更新单个规则提供者
const updateProvider = useLockFn(async (name: string) => { const updateProvider = useLockFn(async (name: string) => {
try { try {
// 设置更新状态 // 设置更新状态
setUpdating(prev => ({ ...prev, [name]: true })); setUpdating((prev) => ({ ...prev, [name]: true }));
await ruleProviderUpdate(name); await ruleProviderUpdate(name);
// 刷新数据 // 刷新数据
await refreshRules(); await refreshRules();
await refreshRuleProviders(); await refreshRuleProviders();
showNotice('success', `${name} 更新成功`); showNotice("success", `${name} 更新成功`);
} catch (err: any) { } catch (err: any) {
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`); showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
} finally { } finally {
// 清除更新状态 // 清除更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} }
}); });
// 更新所有规则提供者 // 更新所有规则提供者
const updateAllProviders = useLockFn(async () => { const updateAllProviders = useLockFn(async () => {
try { try {
// 获取所有provider的名称 // 获取所有provider的名称
const allProviders = Object.keys(ruleProviders || {}); const allProviders = Object.keys(ruleProviders || {});
if (allProviders.length === 0) { if (allProviders.length === 0) {
showNotice('info', "没有可更新的规则提供者"); showNotice("info", "没有可更新的规则提供者");
return; return;
} }
// 设置所有provider为更新中状态 // 设置所有provider为更新中状态
const newUpdating = allProviders.reduce((acc, key) => { const newUpdating = allProviders.reduce(
acc[key] = true; (acc, key) => {
return acc; acc[key] = true;
}, {} as Record<string, boolean>); return acc;
},
{} as Record<string, boolean>,
);
setUpdating(newUpdating); setUpdating(newUpdating);
// 改为串行逐个更新所有provider // 改为串行逐个更新所有provider
for (const name of allProviders) { for (const name of allProviders) {
try { try {
await ruleProviderUpdate(name); await ruleProviderUpdate(name);
// 每个更新完成后更新状态 // 每个更新完成后更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) { } catch (err) {
console.error(`更新 ${name} 失败`, err); console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程 // 继续执行下一个,不中断整体流程
} }
} }
// 刷新数据 // 刷新数据
await refreshRules(); await refreshRules();
await refreshRuleProviders(); await refreshRuleProviders();
showNotice('success', "全部规则提供者更新成功"); showNotice("success", "全部规则提供者更新成功");
} catch (err: any) { } catch (err: any) {
showNotice('error', `更新失败: ${err?.message || err.toString()}`); showNotice("error", `更新失败: ${err?.message || err.toString()}`);
} finally { } finally {
// 清除所有更新状态 // 清除所有更新状态
setUpdating({}); setUpdating({});
} }
}); });
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
}; };
if (!hasProviders) return null; if (!hasProviders) return null;
return ( return (
@@ -134,15 +140,14 @@ export const ProviderButton = () => {
> >
{t("Rule Provider")} {t("Rule Provider")}
</Button> </Button>
<Dialog <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<DialogTitle> <DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Rule Providers")}</Typography> <Typography variant="h6">{t("Rule Providers")}</Typography>
<Button <Button
variant="contained" variant="contained"
@@ -153,49 +158,52 @@ export const ProviderButton = () => {
</Button> </Button>
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<List sx={{ py: 0, minHeight: 250 }}> <List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(ruleProviders || {}).map(([key, item]) => { {Object.entries(ruleProviders || {}).map(([key, item]) => {
const provider = item as RuleProviderItem; const provider = item as RuleProviderItem;
const time = dayjs(provider.updatedAt); const time = dayjs(provider.updatedAt);
const isUpdating = updating[key]; const isUpdating = updating[key];
return ( return (
<ListItem <ListItem
key={key} key={key}
sx={[ sx={[
{ {
p: 0, p: 0,
mb: "8px", mb: "8px",
borderRadius: 2, borderRadius: 2,
overflow: "hidden", overflow: "hidden",
transition: "all 0.2s" transition: "all 0.2s",
}, },
({ palette: { mode, primary } }) => { ({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor = mode === "light" const hoverColor =
? alpha(primary.main, 0.1) mode === "light"
: alpha(primary.main, 0.2); ? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return { return {
backgroundColor: bgcolor, backgroundColor: bgcolor,
"&:hover": { "&:hover": {
backgroundColor: hoverColor, backgroundColor: hoverColor,
borderColor: alpha(primary.main, 0.3) borderColor: alpha(primary.main, 0.3),
} },
}; };
} },
]} ]}
> >
<ListItemText <ListItemText
sx={{ px: 2, py: 1 }} sx={{ px: 2, py: 1 }}
primary={ primary={
<Box sx={{ <Box
display: "flex", sx={{
justifyContent: "space-between", display: "flex",
alignItems: "center", justifyContent: "space-between",
}}> alignItems: "center",
}}
>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
component="div" component="div"
@@ -203,14 +211,19 @@ export const ProviderButton = () => {
title={key} title={key}
sx={{ display: "flex", alignItems: "center" }} sx={{ display: "flex", alignItems: "center" }}
> >
<span style={{ marginRight: "8px" }}>{key}</span> <span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span"> <TypeBox component="span">
{provider.ruleCount} {provider.ruleCount}
</TypeBox> </TypeBox>
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" noWrap> <Typography
<small>{t("Update At")}: </small>{time.fromNow()} variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography> </Typography>
</Box> </Box>
} }
@@ -219,30 +232,32 @@ export const ProviderButton = () => {
<TypeBox component="span"> <TypeBox component="span">
{provider.vehicleType} {provider.vehicleType}
</TypeBox> </TypeBox>
<TypeBox component="span"> <TypeBox component="span">{provider.behavior}</TypeBox>
{provider.behavior}
</TypeBox>
</Box> </Box>
} }
/> />
<Divider orientation="vertical" flexItem /> <Divider orientation="vertical" flexItem />
<Box sx={{ <Box
width: 40, sx={{
display: "flex", width: 40,
justifyContent: "center", display: "flex",
alignItems: "center" justifyContent: "center",
}}> alignItems: "center",
}}
>
<IconButton <IconButton
size="small" size="small"
color="primary" color="primary"
onClick={() => updateProvider(key)} onClick={() => updateProvider(key)}
disabled={isUpdating} disabled={isUpdating}
sx={{ sx={{
animation: isUpdating ? "spin 1s linear infinite" : "none", animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": { "@keyframes spin": {
"0%": { transform: "rotate(0deg)" }, "0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" } "100%": { transform: "rotate(360deg)" },
} },
}} }}
title={t("Update Provider") as string} title={t("Update Provider") as string}
> >
@@ -254,7 +269,7 @@ export const ProviderButton = () => {
})} })}
</List> </List>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose} variant="outlined"> <Button onClick={handleClose} variant="outlined">
{t("Close")} {t("Close")}

View File

@@ -82,21 +82,21 @@ export const BackupConfigViewer = memo(
if (!url) { if (!url) {
urlRef.current?.focus(); urlRef.current?.focus();
showNotice('error', t("WebDAV URL Required")); showNotice("error", t("WebDAV URL Required"));
throw new Error(t("WebDAV URL Required")); throw new Error(t("WebDAV URL Required"));
} else if (!isValidUrl(url)) { } else if (!isValidUrl(url)) {
urlRef.current?.focus(); urlRef.current?.focus();
showNotice('error', t("Invalid WebDAV URL")); showNotice("error", t("Invalid WebDAV URL"));
throw new Error(t("Invalid WebDAV URL")); throw new Error(t("Invalid WebDAV URL"));
} }
if (!username) { if (!username) {
usernameRef.current?.focus(); usernameRef.current?.focus();
showNotice('error', t("WebDAV URL Required")); showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Username Required")); throw new Error(t("Username Required"));
} }
if (!password) { if (!password) {
passwordRef.current?.focus(); passwordRef.current?.focus();
showNotice('error', t("WebDAV URL Required")); showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Password Required")); throw new Error(t("Password Required"));
} }
}; };
@@ -110,11 +110,11 @@ export const BackupConfigViewer = memo(
data.username.trim(), data.username.trim(),
data.password, data.password,
).then(() => { ).then(() => {
showNotice('success', t("WebDAV Config Saved")); showNotice("success", t("WebDAV Config Saved"));
onSaveSuccess(); onSaveSuccess();
}); });
} catch (error) { } catch (error) {
showNotice('error', t("WebDAV Config Save Failed", { error }), 3000); showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -125,11 +125,11 @@ export const BackupConfigViewer = memo(
try { try {
setLoading(true); setLoading(true);
await createWebdavBackup().then(async () => { await createWebdavBackup().then(async () => {
showNotice('success', t("Backup Created")); showNotice("success", t("Backup Created"));
await onBackupSuccess(); await onBackupSuccess();
}); });
} catch (error) { } catch (error) {
showNotice('error', t("Backup Failed", { error })); showNotice("error", t("Backup Failed", { error }));
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -61,7 +61,7 @@ export const BackupTableViewer = memo(
const handleRestore = useLockFn(async (filename: string) => { const handleRestore = useLockFn(async (filename: string) => {
await restoreWebDavBackup(filename).then(() => { await restoreWebDavBackup(filename).then(() => {
showNotice('success', t("Restore Success, App will restart in 1s")); showNotice("success", t("Restore Success, App will restart in 1s"));
}); });
await restartApp(); await restartApp();
}); });

View File

@@ -52,7 +52,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const errorMsg = await changeClashCore(core); const errorMsg = await changeClashCore(core);
if (errorMsg) { if (errorMsg) {
showNotice('error', errorMsg); showNotice("error", errorMsg);
setChangingCore(null); setChangingCore(null);
return; return;
} }
@@ -65,7 +65,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
}, 500); }, 500);
} catch (err: any) { } catch (err: any) {
setChangingCore(null); setChangingCore(null);
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -73,11 +73,11 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
try { try {
setRestarting(true); setRestarting(true);
await restartCore(); await restartCore();
showNotice('success', t(`Clash Core Restarted`)); showNotice("success", t(`Clash Core Restarted`));
setRestarting(false); setRestarting(false);
} catch (err: any) { } catch (err: any) {
setRestarting(false); setRestarting(false);
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -86,14 +86,14 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
setUpgrading(true); setUpgrading(true);
await upgradeCore(); await upgradeCore();
setUpgrading(false); setUpgrading(false);
showNotice('success', t(`Core Version Updated`)); showNotice("success", t(`Core Version Updated`));
} catch (err: any) { } catch (err: any) {
setUpgrading(false); setUpgrading(false);
const errMsg = err.response?.data?.message || err.toString(); const errMsg = err.response?.data?.message || err.toString();
const showMsg = errMsg.includes("already using latest version") const showMsg = errMsg.includes("already using latest version")
? "Already Using Latest Core Version" ? "Already Using Latest Core Version"
: errMsg; : errMsg;
showNotice('error', t(showMsg)); showNotice("error", t(showMsg));
} }
}); });

View File

@@ -11,7 +11,7 @@ import {
ListItem, ListItem,
ListItemText, ListItemText,
Stack, Stack,
TextField TextField,
} from "@mui/material"; } from "@mui/material";
import { useLockFn, useRequest } from "ahooks"; import { useLockFn, useRequest } from "ahooks";
import { forwardRef, useImperativeHandle, useState } from "react"; import { forwardRef, useImperativeHandle, useState } from "react";
@@ -26,127 +26,136 @@ interface ClashPortViewerRef {
close: () => void; close: () => void;
} }
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025; const generateRandomPort = () =>
Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerProps>( export const ClashPortViewer = forwardRef<
(props, ref) => { ClashPortViewerRef,
const { t } = useTranslation(); ClashPortViewerProps
const { clashInfo, patchInfo } = useClashInfo(); >((props, ref) => {
const { verge, patchVerge } = useVerge(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
// Mixed Port // Mixed Port
const [mixedPort, setMixedPort] = useState( const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897 verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897,
); );
// 其他端口状态 // 其他端口状态
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898); const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
const [socksEnabled, setSocksEnabled] = useState(verge?.verge_socks_enabled ?? false); const [socksEnabled, setSocksEnabled] = useState(
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899); verge?.verge_socks_enabled ?? false,
const [httpEnabled, setHttpEnabled] = useState(verge?.verge_http_enabled ?? false); );
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895); const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
const [redirEnabled, setRedirEnabled] = useState(verge?.verge_redir_enabled ?? false); const [httpEnabled, setHttpEnabled] = useState(
const [tproxyPort, setTproxyPort] = useState(verge?.verge_tproxy_port ?? 7896); verge?.verge_http_enabled ?? false,
const [tproxyEnabled, setTproxyEnabled] = useState(verge?.verge_tproxy_enabled ?? false); );
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895);
const [redirEnabled, setRedirEnabled] = useState(
verge?.verge_redir_enabled ?? false,
);
const [tproxyPort, setTproxyPort] = useState(
verge?.verge_tproxy_port ?? 7896,
);
const [tproxyEnabled, setTproxyEnabled] = useState(
verge?.verge_tproxy_enabled ?? false,
);
// 添加保存请求防止GUI卡死 // 添加保存请求防止GUI卡死
const { loading, run: saveSettings } = useRequest( const { loading, run: saveSettings } = useRequest(
async (params: { async (params: { clashConfig: any; vergeConfig: any }) => {
clashConfig: any; const { clashConfig, vergeConfig } = params;
vergeConfig: any; await Promise.all([patchInfo(clashConfig), patchVerge(vergeConfig)]);
}) => { },
const { clashConfig, vergeConfig } = params; {
await Promise.all([ manual: true,
patchInfo(clashConfig), onSuccess: () => {
patchVerge(vergeConfig) setOpen(false);
]); showNotice("success", t("Port settings saved")); // 调用提示函数
}, },
{ onError: () => {
manual: true, showNotice("error", t("Failed to save settings")); // 调用提示函数
onSuccess: () => {
setOpen(false);
showNotice("success", t("Port settings saved")); // 调用提示函数
},
onError: () => {
showNotice("error", t("Failed to save settings")); // 调用提示函数
}
}
);
useImperativeHandle(ref, () => ({
open: () => {
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
setSocksPort(verge?.verge_socks_port ?? 7898);
setSocksEnabled(verge?.verge_socks_enabled ?? false);
setHttpPort(verge?.verge_port ?? 7899);
setHttpEnabled(verge?.verge_http_enabled ?? false);
setRedirPort(verge?.verge_redir_port ?? 7895);
setRedirEnabled(verge?.verge_redir_enabled ?? false);
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
setOpen( true);
}, },
close: () => setOpen(false), },
})); );
const onSave = useLockFn(async () => { useImperativeHandle(ref, () => ({
// 端口冲突检测 open: () => {
const portList = [ setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
mixedPort, setSocksPort(verge?.verge_socks_port ?? 7898);
socksEnabled ? socksPort : -1, setSocksEnabled(verge?.verge_socks_enabled ?? false);
httpEnabled ? httpPort : -1, setHttpPort(verge?.verge_port ?? 7899);
redirEnabled ? redirPort : -1, setHttpEnabled(verge?.verge_http_enabled ?? false);
tproxyEnabled ? tproxyPort : -1 setRedirPort(verge?.verge_redir_port ?? 7895);
].filter(p => p !== -1); setRedirEnabled(verge?.verge_redir_enabled ?? false);
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
setOpen(true);
},
close: () => setOpen(false),
}));
if (new Set(portList).size !== portList.length) { const onSave = useLockFn(async () => {
return; // 端口冲突检测
} const portList = [
mixedPort,
socksEnabled ? socksPort : -1,
httpEnabled ? httpPort : -1,
redirEnabled ? redirPort : -1,
tproxyEnabled ? tproxyPort : -1,
].filter((p) => p !== -1);
// 验证端口范围 if (new Set(portList).size !== portList.length) {
const isValidPort = (port: number) => port >= 1 && port <= 65535; return;
const allPortsValid = [ }
mixedPort,
socksEnabled ? socksPort : 0,
httpEnabled ? httpPort : 0,
redirEnabled ? redirPort : 0,
tproxyEnabled ? tproxyPort : 0
].every(port => port === 0 || isValidPort(port));
if (!allPortsValid) { // 验证端口范围
return; const isValidPort = (port: number) => port >= 1 && port <= 65535;
} const allPortsValid = [
mixedPort,
socksEnabled ? socksPort : 0,
httpEnabled ? httpPort : 0,
redirEnabled ? redirPort : 0,
tproxyEnabled ? tproxyPort : 0,
].every((port) => port === 0 || isValidPort(port));
// 准备配置数据 if (!allPortsValid) {
const clashConfig = { return;
"mixed-port": mixedPort, }
"socks-port": socksPort,
port: httpPort,
"redir-port": redirPort,
"tproxy-port": tproxyPort
};
const vergeConfig = { // 准备配置数据
verge_mixed_port: mixedPort, const clashConfig = {
verge_socks_port: socksPort, "mixed-port": mixedPort,
verge_socks_enabled: socksEnabled, "socks-port": socksPort,
verge_port: httpPort, port: httpPort,
verge_http_enabled: httpEnabled, "redir-port": redirPort,
verge_redir_port: redirPort, "tproxy-port": tproxyPort,
verge_redir_enabled: redirEnabled, };
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled
};
// 提交保存请求 const vergeConfig = {
await saveSettings({ clashConfig, vergeConfig }); verge_mixed_port: mixedPort,
}); verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: httpPort,
verge_http_enabled: httpEnabled,
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled,
};
// 优化的数字输入处理 // 提交保存请求
const handleNumericChange = (setter: (value: number) => void) => (e: React.ChangeEvent<HTMLInputElement>) => { await saveSettings({ clashConfig, vergeConfig });
const value = e.target.value.replace(/\D+/, ''); });
if (value === '') {
// 优化的数字输入处理
const handleNumericChange =
(setter: (value: number) => void) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D+/, "");
if (value === "") {
setter(0); setter(0);
return; return;
} }
@@ -157,190 +166,201 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerPro
} }
}; };
return ( return (
<BaseDialog <BaseDialog
open={open} open={open}
title={t("Port Configuration")} title={t("Port Configuration")}
contentSx={{ contentSx={{
width: 400 width: 400,
}} }}
okBtn={ okBtn={
loading ? ( loading ? (
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} /> <CircularProgress size={20} />
{t("Saving...")} {t("Saving...")}
</Stack> </Stack>
) : t("Save") ) : (
} t("Save")
cancelBtn={t("Cancel")} )
onClose={() => setOpen(false)} }
onCancel={() => setOpen(false)} cancelBtn={t("Cancel")}
onOk={onSave} onClose={() => setOpen(false)}
> onCancel={() => setOpen(false)}
<List sx={{ width: "100%" }}> onOk={onSave}
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> >
<ListItemText <List sx={{ width: "100%" }}>
primary={t("Mixed Port")} <ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
primaryTypographyProps={{ fontSize: 12 }} <ListItemText
primary={t("Mixed Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={mixedPort}
onChange={(e) =>
setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
inputProps={{ style: { fontSize: 12 } }}
/> />
<div style={{ display: "flex", alignItems: "center" }}> <IconButton
<TextField size="small"
size="small" onClick={() => setMixedPort(generateRandomPort())}
sx={{ width: 80, mr: 0.5, fontSize: 12 }} title={t("Random Port")}
value={mixedPort} sx={{ mr: 0.5 }}
onChange={(e) => setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} >
inputProps={{ style: { fontSize: 12 } }} <Shuffle fontSize="small" />
/> </IconButton>
<IconButton <Switch
size="small" size="small"
onClick={() => setMixedPort(generateRandomPort())} checked={true}
title={t("Random Port")} disabled={true}
sx={{ mr: 0.5 }} sx={{ ml: 0.5, opacity: 0.7 }}
> />
<Shuffle fontSize="small" /> </div>
</IconButton> </ListItem>
<Switch
size="small"
checked={true}
disabled={true}
sx={{ ml: 0.5, opacity: 0.7 }}
/>
</div>
</ListItem>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Socks Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort}
onChange={(e) =>
setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!socksEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setSocksPort(generateRandomPort())}
title={t("Random Port")}
disabled={!socksEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={socksEnabled}
onChange={(_, c) => setSocksEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("HTTP Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort}
onChange={(e) =>
setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!httpEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setHttpPort(generateRandomPort())}
title={t("Random Port")}
disabled={!httpEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={httpEnabled}
onChange={(_, c) => setHttpEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
{OS !== "windows" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> <ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText <ListItemText
primary={t("Socks Port")} primary={t("Redir Port")}
primaryTypographyProps={{ fontSize: 12 }} primaryTypographyProps={{ fontSize: 12 }}
/> />
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<TextField <TextField
size="small" size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }} sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort} value={redirPort}
onChange={(e) => setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} onChange={(e) =>
disabled={!socksEnabled} setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!redirEnabled}
inputProps={{ style: { fontSize: 12 } }} inputProps={{ style: { fontSize: 12 } }}
/> />
<IconButton <IconButton
size="small" size="small"
onClick={() => setSocksPort(generateRandomPort())} onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")} title={t("Random Port")}
disabled={!socksEnabled} disabled={!redirEnabled}
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
> >
<Shuffle fontSize="small" /> <Shuffle fontSize="small" />
</IconButton> </IconButton>
<Switch <Switch
size="small" size="small"
checked={socksEnabled} checked={redirEnabled}
onChange={(_, c) => setSocksEnabled(c)} onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }} sx={{ ml: 0.5 }}
/> />
</div> </div>
</ListItem> </ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> <ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText <ListItemText
primary={t("HTTP Port")} primary={t("Tproxy Port")}
primaryTypographyProps={{ fontSize: 12 }} primaryTypographyProps={{ fontSize: 12 }}
/> />
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<TextField <TextField
size="small" size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }} sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort} value={tproxyPort}
onChange={(e) => setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} onChange={(e) =>
disabled={!httpEnabled} setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!tproxyEnabled}
inputProps={{ style: { fontSize: 12 } }} inputProps={{ style: { fontSize: 12 } }}
/> />
<IconButton <IconButton
size="small" size="small"
onClick={() => setHttpPort(generateRandomPort())} onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")} title={t("Random Port")}
disabled={!httpEnabled} disabled={!tproxyEnabled}
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
> >
<Shuffle fontSize="small" /> <Shuffle fontSize="small" />
</IconButton> </IconButton>
<Switch <Switch
size="small" size="small"
checked={httpEnabled} checked={tproxyEnabled}
onChange={(_, c) => setHttpEnabled(c)} onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }} sx={{ ml: 0.5 }}
/> />
</div> </div>
</ListItem> </ListItem>
)}
{OS !== "windows" && ( </List>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> </BaseDialog>
<ListItemText );
primary={t("Redir Port")} });
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={redirPort}
onChange={(e) => setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!redirEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")}
disabled={!redirEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={redirEnabled}
onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Tproxy Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={tproxyPort}
onChange={(e) => setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!tproxyEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")}
disabled={!tproxyEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={tproxyEnabled}
onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
</List>
</BaseDialog>
);
}
);

View File

@@ -13,7 +13,7 @@ import {
ListItemText, ListItemText,
Snackbar, Snackbar,
TextField, TextField,
Tooltip Tooltip,
} from "@mui/material"; } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
@@ -42,58 +42,72 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置 // 保存配置
const onSave = useLockFn(async () => { const onSave = useLockFn(async () => {
if (!controller.trim()) { if (!controller.trim()) {
showNotice('error', t("Controller address cannot be empty"), 3000); showNotice("error", t("Controller address cannot be empty"), 3000);
return; return;
} }
if (!secret.trim()) { if (!secret.trim()) {
showNotice('error', t("Secret cannot be empty"), 3000); showNotice("error", t("Secret cannot be empty"), 3000);
return; return;
} }
try { try {
setIsSaving(true); setIsSaving(true);
await patchInfo({ "external-controller": controller, secret }); await patchInfo({ "external-controller": controller, secret });
showNotice('success', t("Configuration saved successfully"), 2000); showNotice("success", t("Configuration saved successfully"), 2000);
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || t("Failed to save configuration"), 4000); showNotice(
"error",
err.message || t("Failed to save configuration"),
4000,
);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}); });
// 复制到剪贴板 // 复制到剪贴板
const handleCopyToClipboard = useLockFn(async (text: string, type: string) => { const handleCopyToClipboard = useLockFn(
try { async (text: string, type: string) => {
await navigator.clipboard.writeText(text); try {
setCopySuccess(type); await navigator.clipboard.writeText(text);
setTimeout(() => setCopySuccess(null), 2000); setCopySuccess(type);
} catch (err) { setTimeout(() => setCopySuccess(null), 2000);
showNotice('error', t("Failed to copy"), 2000); } catch (err) {
} showNotice("error", t("Failed to copy"), 2000);
}); }
},
);
return ( return (
<BaseDialog <BaseDialog
open={open} open={open}
title={t("External Controller")} title={t("External Controller")}
contentSx={{ width: 400 }} contentSx={{ width: 400 }}
okBtn={isSaving ? ( okBtn={
<Box display="flex" alignItems="center" gap={1}> isSaving ? (
<CircularProgress size={16} color="inherit" /> <Box display="flex" alignItems="center" gap={1}>
{t("Saving...")} <CircularProgress size={16} color="inherit" />
</Box> {t("Saving...")}
) : ( </Box>
t("Save") ) : (
)} t("Save")
)
}
cancelBtn={t("Cancel")} cancelBtn={t("Cancel")}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<List> <List>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}> <ListItem
sx={{
padding: "5px 2px",
display: "flex",
justifyContent: "space-between",
}}
>
<ListItemText primary={t("External Controller")} /> <ListItemText primary={t("External Controller")} />
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<TextField <TextField
@@ -101,11 +115,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{ sx={{
width: 175, width: 175,
opacity: 1, opacity: 1,
pointerEvents: 'auto' pointerEvents: "auto",
}} }}
value={controller} value={controller}
placeholder="Required" placeholder="Required"
onChange={e => setController(e.target.value)} onChange={(e) => setController(e.target.value)}
disabled={isSaving} disabled={isSaving}
/> />
<Tooltip title={t("Copy to clipboard")}> <Tooltip title={t("Copy to clipboard")}>
@@ -121,7 +135,13 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
</Box> </Box>
</ListItem> </ListItem>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}> <ListItem
sx={{
padding: "5px 2px",
display: "flex",
justifyContent: "space-between",
}}
>
<ListItemText primary={t("Core Secret")} /> <ListItemText primary={t("Core Secret")} />
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<TextField <TextField
@@ -129,11 +149,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{ sx={{
width: 175, width: 175,
opacity: 1, opacity: 1,
pointerEvents: 'auto' pointerEvents: "auto",
}} }}
value={secret} value={secret}
placeholder={t("Recommended")} placeholder={t("Recommended")}
onChange={e => setSecret(e.target.value)} onChange={(e) => setSecret(e.target.value)}
disabled={isSaving} disabled={isSaving}
/> />
<Tooltip title={t("Copy to clipboard")}> <Tooltip title={t("Copy to clipboard")}>
@@ -153,13 +173,12 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<Snackbar <Snackbar
open={copySuccess !== null} open={copySuccess !== null}
autoHideDuration={2000} autoHideDuration={2000}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
> >
<Alert severity="success"> <Alert severity="success">
{copySuccess === "controller" {copySuccess === "controller"
? t("Controller address copied to clipboard") ? t("Controller address copied to clipboard")
: t("Secret copied to clipboard") : t("Secret copied to clipboard")}
}
</Alert> </Alert>
</Snackbar> </Snackbar>
</BaseDialog> </BaseDialog>

View File

@@ -59,7 +59,13 @@ const DEFAULT_DNS_CONFIG = {
"*.msftncsi.com", "*.msftncsi.com",
"www.msftconnecttest.com", "www.msftconnecttest.com",
], ],
"default-nameserver": ["system", "223.6.6.6", "8.8.8.8", "2400:3200::1", "2001:4860:4860::8888"], "default-nameserver": [
"system",
"223.6.6.6",
"8.8.8.8",
"2400:3200::1",
"2001:4860:4860::8888",
],
nameserver: [ nameserver: [
"8.8.8.8", "8.8.8.8",
"https://doh.pub/dns-query", "https://doh.pub/dns-query",
@@ -70,7 +76,7 @@ const DEFAULT_DNS_CONFIG = {
"proxy-server-nameserver": [ "proxy-server-nameserver": [
"https://doh.pub/dns-query", "https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query", "https://dns.alidns.com/dns-query",
"tls://223.5.5.5" "tls://223.5.5.5",
], ],
"direct-nameserver": [], "direct-nameserver": [],
"direct-nameserver-follow-policy": false, "direct-nameserver-follow-policy": false,
@@ -219,8 +225,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"], dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"], useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
useSystemHosts: useSystemHosts:
dnsConfig["use-system-hosts"] ?? dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
DEFAULT_DNS_CONFIG["use-system-hosts"],
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6, ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: fakeIpFilter:
dnsConfig["fake-ip-filter"]?.join(", ") ?? dnsConfig["fake-ip-filter"]?.join(", ") ??
@@ -229,7 +234,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig.nameserver?.join(", ") ?? dnsConfig.nameserver?.join(", ") ??
DEFAULT_DNS_CONFIG.nameserver.join(", "), DEFAULT_DNS_CONFIG.nameserver.join(", "),
fallback: fallback:
dnsConfig.fallback?.join(", ") ?? DEFAULT_DNS_CONFIG.fallback.join(", "), dnsConfig.fallback?.join(", ") ??
DEFAULT_DNS_CONFIG.fallback.join(", "),
defaultNameserver: defaultNameserver:
dnsConfig["default-nameserver"]?.join(", ") ?? dnsConfig["default-nameserver"]?.join(", ") ??
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
@@ -299,9 +305,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
// 从表单值更新YAML内容 // 从表单值更新YAML内容
const updateYamlFromValues = () => { const updateYamlFromValues = () => {
const config: Record<string, any> = {}; const config: Record<string, any> = {};
const dnsConfig = generateDnsConfig(); const dnsConfig = generateDnsConfig();
if (Object.keys(dnsConfig).length > 0) { if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig; config.dns = dnsConfig;
@@ -311,7 +316,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
if (Object.keys(hosts).length > 0) { if (Object.keys(hosts).length > 0) {
config.hosts = hosts; config.hosts = hosts;
} }
setYamlContent(yaml.dump(config, { forceQuotes: true })); setYamlContent(yaml.dump(config, { forceQuotes: true }));
}; };
@@ -320,10 +325,10 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
try { try {
const parsedYaml = yaml.load(yamlContent) as any; const parsedYaml = yaml.load(yamlContent) as any;
if (!parsedYaml) return; if (!parsedYaml) return;
updateValuesFromConfig(parsedYaml); updateValuesFromConfig(parsedYaml);
} catch (err: any) { } catch (err: any) {
showNotice('error', t("Invalid YAML format")); showNotice("error", t("Invalid YAML format"));
} }
}; };
@@ -505,7 +510,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
if (Object.keys(dnsConfig).length > 0) { if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig; config.dns = dnsConfig;
} }
const hosts = parseHosts(values.hosts); const hosts = parseHosts(values.hosts);
if (Object.keys(hosts).length > 0) { if (Object.keys(hosts).length > 0) {
config.hosts = hosts; config.hosts = hosts;
@@ -521,30 +526,41 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置 // 保存配置
await invoke("save_dns_config", { dnsConfig: config }); await invoke("save_dns_config", { dnsConfig: config });
// 验证配置 // 验证配置
const [isValid, errorMsg] = await invoke<[boolean, string]>("validate_dns_config", {}); const [isValid, errorMsg] = await invoke<[boolean, string]>(
"validate_dns_config",
{},
);
if (!isValid) { if (!isValid) {
let cleanErrorMsg = errorMsg; let cleanErrorMsg = errorMsg;
// 提取关键错误信息 // 提取关键错误信息
if (errorMsg.includes("level=error")) { if (errorMsg.includes("level=error")) {
const errorLines = errorMsg.split('\n').filter(line => const errorLines = errorMsg
line.includes("level=error") || .split("\n")
line.includes("level=fatal") || .filter(
line.includes("failed") (line) =>
); line.includes("level=error") ||
line.includes("level=fatal") ||
line.includes("failed"),
);
if (errorLines.length > 0) { if (errorLines.length > 0) {
cleanErrorMsg = errorLines.map(line => { cleanErrorMsg = errorLines
const msgMatch = line.match(/msg="([^"]+)"/); .map((line) => {
return msgMatch ? msgMatch[1] : line; const msgMatch = line.match(/msg="([^"]+)"/);
}).join(", "); return msgMatch ? msgMatch[1] : line;
})
.join(", ");
} }
} }
showNotice('error', t("DNS configuration error") + ": " + cleanErrorMsg); showNotice(
"error",
t("DNS configuration error") + ": " + cleanErrorMsg,
);
return; return;
} }
@@ -555,9 +571,9 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
} }
setOpen(false); setOpen(false);
showNotice('success', t("DNS settings saved")); showNotice("success", t("DNS settings saved"));
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });

View File

@@ -80,7 +80,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
}); });
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });

View File

@@ -88,7 +88,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const onSwitchFormat = (_e: any, value: boolean) => value; const onSwitchFormat = (_e: any, value: boolean) => value;
const onError = (err: any) => { const onError = (err: any) => {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
}; };
const onChangeData = (patch: Partial<IVergeConfig>) => { const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false); mutateVerge({ ...verge, ...patch }, false);

View File

@@ -44,7 +44,7 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
}); });
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });

View File

@@ -62,7 +62,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
}); });
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -246,7 +246,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
endAdornment: ( endAdornment: (
<InputAdornment position="end">{t("millis")}</InputAdornment> <InputAdornment position="end">{t("millis")}</InputAdornment>
), ),
} },
}} }}
/> />
</ListItem> </ListItem>

View File

@@ -129,7 +129,7 @@ const AddressDisplay = (props: { label: string; content: string }) => {
size="small" size="small"
onClick={async () => { onClick={async () => {
await writeText(props.content); await writeText(props.content);
showNotice('success', t("Copy Success")); showNotice("success", t("Copy Success"));
}} }}
> >
<ContentCopyRounded sx={{ fontSize: "18px" }} /> <ContentCopyRounded sx={{ fontSize: "18px" }} />

View File

@@ -202,11 +202,14 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const onSave = useLockFn(async () => { const onSave = useLockFn(async () => {
if (value.duration < 1) { if (value.duration < 1) {
showNotice('error', t("Proxy Daemon Duration Cannot be Less than 1 Second")); showNotice(
"error",
t("Proxy Daemon Duration Cannot be Less than 1 Second"),
);
return; return;
} }
if (value.bypass && !validReg.test(value.bypass)) { if (value.bypass && !validReg.test(value.bypass)) {
showNotice('error', t("Invalid Bypass Format")); showNotice("error", t("Invalid Bypass Format"));
return; return;
} }
@@ -223,7 +226,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
!ipv6Regex.test(value.proxy_host) && !ipv6Regex.test(value.proxy_host) &&
!hostnameRegex.test(value.proxy_host) !hostnameRegex.test(value.proxy_host)
) { ) {
showNotice('error', t("Invalid Proxy Host Format")); showNotice("error", t("Invalid Proxy Host Format"));
return; return;
} }
@@ -271,41 +274,44 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
} }
// 判断是否需要重置系统代理 // 判断是否需要重置系统代理
const needResetProxy = const needResetProxy =
value.pac !== proxy_auto_config || value.pac !== proxy_auto_config ||
proxyHost !== proxy_host || proxyHost !== proxy_host ||
pacContent !== pac_file_content || pacContent !== pac_file_content ||
value.bypass !== system_proxy_bypass || value.bypass !== system_proxy_bypass ||
value.use_default !== use_default_bypass; value.use_default !== use_default_bypass;
await patchVerge(patch); await patchVerge(patch);
// 更新系统代理状态以便UI立即反映变化 // 更新系统代理状态以便UI立即反映变化
await Promise.all([mutate("getSystemProxy"), mutate("getAutotemProxy")]); await Promise.all([mutate("getSystemProxy"), mutate("getAutotemProxy")]);
// 只有在修改了影响系统代理的配置且系统代理当前启用时,才重置系统代理 // 只有在修改了影响系统代理的配置且系统代理当前启用时,才重置系统代理
if (needResetProxy) { if (needResetProxy) {
const currentSysProxy = await getSystemProxy(); const currentSysProxy = await getSystemProxy();
const currentAutoProxy = await getAutotemProxy(); const currentAutoProxy = await getAutotemProxy();
if (value.pac ? currentAutoProxy?.enable : currentSysProxy?.enable) { if (value.pac ? currentAutoProxy?.enable : currentSysProxy?.enable) {
// 临时关闭系统代理 // 临时关闭系统代理
await patchVergeConfig({ enable_system_proxy: false }); await patchVergeConfig({ enable_system_proxy: false });
// 减少等待时间 // 减少等待时间
await new Promise((resolve) => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
// 重新开启系统代理 // 重新开启系统代理
await patchVergeConfig({ enable_system_proxy: true }); await patchVergeConfig({ enable_system_proxy: true });
// 更新UI状态 // 更新UI状态
await Promise.all([mutate("getSystemProxy"), mutate("getAutotemProxy")]); await Promise.all([
mutate("getSystemProxy"),
mutate("getAutotemProxy"),
]);
} }
} }
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -415,7 +421,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
slotProps={{ slotProps={{
input: { input: {
endAdornment: <InputAdornment position="end">s</InputAdornment>, endAdornment: <InputAdornment position="end">s</InputAdornment>,
} },
}} }}
onChange={(e) => { onChange={(e) => {
setValue((v) => ({ setValue((v) => ({
@@ -432,12 +438,14 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
edge="end" edge="end"
disabled={!enabled} disabled={!enabled}
checked={value.use_default} checked={value.use_default}
onChange={(_, e) => setValue((v) => ({ onChange={(_, e) =>
...v, setValue((v) => ({
use_default: e, ...v,
// 当取消选择use_default且当前bypass为空时填充默认值 use_default: e,
bypass: (!e && !v.bypass) ? defaultBypass() : v.bypass // 当取消选择use_default且当前bypass为空时填充默认值
}))} bypass: !e && !v.bypass ? defaultBypass() : v.bypass,
}))
}
/> />
</ListItem> </ListItem>
)} )}

View File

@@ -49,7 +49,7 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
await patchVerge({ theme_setting: theme }); await patchVerge({ theme_setting: theme });
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });

View File

@@ -77,13 +77,13 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
); );
try { try {
await enhanceProfiles(); await enhanceProfiles();
showNotice('success', t("Settings Applied")); showNotice("success", t("Settings Applied"));
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });

View File

@@ -1,5 +1,11 @@
import useSWR from "swr"; import useSWR from "swr";
import { forwardRef, useImperativeHandle, useState, useMemo, useEffect } from "react"; import {
forwardRef,
useImperativeHandle,
useState,
useMemo,
useEffect,
} from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Box, LinearProgress, Button } from "@mui/material"; import { Box, LinearProgress, Button } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -18,7 +24,8 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [currentProgressListener, setCurrentProgressListener] = useState<UnlistenFn | null>(null); const [currentProgressListener, setCurrentProgressListener] =
useState<UnlistenFn | null>(null);
const updateState = useUpdateState(); const updateState = useUpdateState();
const setUpdateState = useSetUpdateState(); const setUpdateState = useSetUpdateState();
@@ -55,12 +62,12 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const onUpdate = useLockFn(async () => { const onUpdate = useLockFn(async () => {
if (portableFlag) { if (portableFlag) {
showNotice('error', t("Portable Updater Error")); showNotice("error", t("Portable Updater Error"));
return; return;
} }
if (!updateInfo?.body) return; if (!updateInfo?.body) return;
if (breakChangeFlag) { if (breakChangeFlag) {
showNotice('error', t("Break Change Update Error")); showNotice("error", t("Break Change Update Error"));
return; return;
} }
if (updateState) return; if (updateState) return;
@@ -86,7 +93,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
await updateInfo.downloadAndInstall(); await updateInfo.downloadAndInstall();
await relaunch(); await relaunch();
} catch (err: any) { } catch (err: any) {
showNotice('error', err?.message || err.toString()); showNotice("error", err?.message || err.toString());
} finally { } finally {
setUpdateState(false); setUpdateState(false);
if (progressListener) { if (progressListener) {

View File

@@ -7,10 +7,7 @@ import { updateGeoData } from "@/services/api";
import { invoke_uwp_tool } from "@/services/cmds"; import { invoke_uwp_tool } from "@/services/cmds";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { import { LanRounded, SettingsRounded } from "@mui/icons-material";
LanRounded,
SettingsRounded
} from "@mui/icons-material";
import { MenuItem, Select, TextField, Typography } from "@mui/material"; import { MenuItem, Select, TextField, Typography } from "@mui/material";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
@@ -71,9 +68,9 @@ const SettingClash = ({ onError }: Props) => {
const onUpdateGeo = async () => { const onUpdateGeo = async () => {
try { try {
await updateGeoData(); await updateGeoData();
showNotice('success', t("GeoData Updated")); showNotice("success", t("GeoData Updated"));
} catch (err: any) { } catch (err: any) {
showNotice('error', err?.response.data.message || err.toString()); showNotice("error", err?.response.data.message || err.toString());
} }
}; };
@@ -90,7 +87,7 @@ const SettingClash = ({ onError }: Props) => {
} catch (err: any) { } catch (err: any) {
setDnsSettingsEnabled(!enable); setDnsSettingsEnabled(!enable);
localStorage.setItem("dns_settings_enabled", String(!enable)); localStorage.setItem("dns_settings_enabled", String(!enable));
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
await patchVerge({ enable_dns_settings: !enable }).catch(() => {}); await patchVerge({ enable_dns_settings: !enable }).catch(() => {});
throw err; throw err;
} }
@@ -203,9 +200,7 @@ const SettingClash = ({ onError }: Props) => {
</GuardState> </GuardState>
</SettingItem> </SettingItem>
<SettingItem <SettingItem label={t("Port Config")}>
label={t("Port Config")}
>
<TextField <TextField
autoComplete="new-password" autoComplete="new-password"
disabled={false} disabled={false}
@@ -225,7 +220,9 @@ const SettingClash = ({ onError }: Props) => {
<> <>
{t("External")} {t("External")}
<TooltipIcon <TooltipIcon
title={t("Enable one-click random API port and key. Click to randomize the port and key")} title={t(
"Enable one-click random API port and key. Click to randomize the port and key",
)}
sx={{ opacity: "0.7" }} sx={{ opacity: "0.7" }}
/> />
</> </>

View File

@@ -47,18 +47,18 @@ const SettingVergeAdvanced = ({ onError }: Props) => {
try { try {
const info = await checkUpdate(); const info = await checkUpdate();
if (!info?.available) { if (!info?.available) {
showNotice('success', t("Currently on the Latest Version")); showNotice("success", t("Currently on the Latest Version"));
} else { } else {
updateRef.current?.open(); updateRef.current?.open();
} }
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}; };
const onExportDiagnosticInfo = useCallback(async () => { const onExportDiagnosticInfo = useCallback(async () => {
await exportDiagnosticInfo(); await exportDiagnosticInfo();
showNotice('success', t("Copy Success"), 1000); showNotice("success", t("Copy Success"), 1000);
}, []); }, []);
return ( return (
@@ -110,7 +110,10 @@ const SettingVergeAdvanced = ({ onError }: Props) => {
<SettingItem <SettingItem
label={t("LightWeight Mode Settings")} label={t("LightWeight Mode Settings")}
extra={ extra={
<TooltipIcon title={t("LightWeight Mode Info")} sx={{ opacity: "0.7" }} /> <TooltipIcon
title={t("LightWeight Mode Info")}
sx={{ opacity: "0.7" }}
/>
} }
onClick={() => liteModeRef.current?.open()} onClick={() => liteModeRef.current?.open()}
/> />

View File

@@ -69,7 +69,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
const onCopyClashEnv = useCallback(async () => { const onCopyClashEnv = useCallback(async () => {
await copyClashEnv(); await copyClashEnv();
showNotice('success', t("Copy Success"), 1000); showNotice("success", t("Copy Success"), 1000);
}, []); }, []);
return ( return (

View File

@@ -246,7 +246,10 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => {
onFormat={onSwitchFormat} onFormat={onSwitchFormat}
onChange={(e) => { onChange={(e) => {
if (isSidecarMode) { if (isSidecarMode) {
showNotice('error', t("TUN requires Service Mode or Admin Mode")); showNotice(
"error",
t("TUN requires Service Mode or Admin Mode"),
);
return Promise.reject( return Promise.reject(
new Error(t("TUN requires Service Mode or Admin Mode")), new Error(t("TUN requires Service Mode or Admin Mode")),
); );
@@ -255,7 +258,10 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => {
}} }}
onGuard={(e) => { onGuard={(e) => {
if (isSidecarMode) { if (isSidecarMode) {
showNotice('error', t("TUN requires Service Mode or Admin Mode")); showNotice(
"error",
t("TUN requires Service Mode or Admin Mode"),
);
return Promise.reject( return Promise.reject(
new Error(t("TUN requires Service Mode or Admin Mode")), new Error(t("TUN requires Service Mode or Admin Mode")),
); );

View File

@@ -72,7 +72,7 @@ export const TestItem = (props: Props) => {
try { try {
onDeleteItem(uid); onDeleteItem(uid);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -97,7 +97,9 @@ export const TestItem = (props: Props) => {
return () => { return () => {
if (unlistenFn) { if (unlistenFn) {
console.log(`TestItem for ${props.id} unmounting or url changed, cleaning up test-all listener.`); console.log(
`TestItem for ${props.id} unmounting or url changed, cleaning up test-all listener.`,
);
unlistenFn(); unlistenFn();
} }
}; };

View File

@@ -100,7 +100,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
setLoading(false); setLoading(false);
setTimeout(() => formIns.reset(), 500); setTimeout(() => formIns.reset(), 500);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
setLoading(false); setLoading(false);
} }
}), }),

View File

@@ -9,7 +9,7 @@ import { getProxies, updateProxy } from "@/services/api";
export const useProfiles = () => { export const useProfiles = () => {
const { data: profiles, mutate: mutateProfiles } = useSWR( const { data: profiles, mutate: mutateProfiles } = useSWR(
"getProfiles", "getProfiles",
getProfiles getProfiles,
); );
const patchProfiles = async (value: Partial<IProfilesConfig>) => { const patchProfiles = async (value: Partial<IProfilesConfig>) => {
@@ -32,7 +32,7 @@ export const useProfiles = () => {
if (!profileData || !proxiesData) return; if (!profileData || !proxiesData) return;
const current = profileData.items?.find( const current = profileData.items?.find(
(e) => e && e.uid === profileData.current (e) => e && e.uid === profileData.current,
); );
if (!current) return; if (!current) return;
@@ -40,7 +40,7 @@ export const useProfiles = () => {
// init selected array // init selected array
const { selected = [] } = current; const { selected = [] } = current;
const selectedMap = Object.fromEntries( const selectedMap = Object.fromEntries(
selected.map((each) => [each.name!, each.now!]) selected.map((each) => [each.name!, each.now!]),
); );
let hasChange = false; let hasChange = false;

View File

@@ -1,7 +1,11 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
import { installService, isServiceAvailable, restartCore } from "@/services/cmds"; import {
installService,
isServiceAvailable,
restartCore,
} from "@/services/cmds";
import { useSystemState } from "@/hooks/use-system-state"; import { useSystemState } from "@/hooks/use-system-state";
import { mutate } from "swr"; import { mutate } from "swr";
@@ -11,16 +15,16 @@ export function useServiceInstaller() {
const installServiceAndRestartCore = useLockFn(async () => { const installServiceAndRestartCore = useLockFn(async () => {
try { try {
showNotice('info', t("Installing Service...")); showNotice("info", t("Installing Service..."));
await installService(); await installService();
showNotice('success', t("Service Installed Successfully")); showNotice("success", t("Service Installed Successfully"));
showNotice('info', t("Waiting for service to be ready...")); showNotice("info", t("Waiting for service to be ready..."));
let serviceReady = false; let serviceReady = false;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
try { try {
// 等待1秒再检查 // 等待1秒再检查
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
const isAvailable = await isServiceAvailable(); const isAvailable = await isServiceAvailable();
if (isAvailable) { if (isAvailable) {
serviceReady = true; serviceReady = true;
@@ -29,52 +33,86 @@ export function useServiceInstaller() {
} }
// 最后一次尝试不显示重试信息 // 最后一次尝试不显示重试信息
if (i < 4) { if (i < 4) {
showNotice('info', t("Service not ready, retrying attempt {count}/{total}...", { count: i + 1, total: 5 })); showNotice(
"info",
t("Service not ready, retrying attempt {count}/{total}...", {
count: i + 1,
total: 5,
}),
);
} }
} catch (error) { } catch (error) {
console.error(t("Error checking service status:"), error); console.error(t("Error checking service status:"), error);
if (i < 4) { if (i < 4) {
showNotice('error', t("Failed to check service status, retrying attempt {count}/{total}...", { count: i + 1, total: 5 })); showNotice(
"error",
t(
"Failed to check service status, retrying attempt {count}/{total}...",
{ count: i + 1, total: 5 },
),
);
} }
} }
} }
if (!serviceReady) { if (!serviceReady) {
showNotice('info', t("Service did not become ready after attempts. Proceeding with core restart.")); showNotice(
"info",
t(
"Service did not become ready after attempts. Proceeding with core restart.",
),
);
} }
showNotice('info', t("Restarting Core...")); showNotice("info", t("Restarting Core..."));
await restartCore(); await restartCore();
// 核心重启后,再次确认并更新相关状态 // 核心重启后,再次确认并更新相关状态
await mutateRunningMode(); await mutateRunningMode();
const finalServiceStatus = await isServiceAvailable(); const finalServiceStatus = await isServiceAvailable();
mutate("isServiceAvailable", finalServiceStatus, false); mutate("isServiceAvailable", finalServiceStatus, false);
if (serviceReady && finalServiceStatus) { if (serviceReady && finalServiceStatus) {
showNotice('success', t("Service is ready and core restarted")); showNotice("success", t("Service is ready and core restarted"));
} else if (finalServiceStatus) { } else if (finalServiceStatus) {
showNotice('success', t("Core restarted. Service is now available.")); showNotice("success", t("Core restarted. Service is now available."));
} else if (serviceReady) { } else if (serviceReady) {
showNotice('info', t("Service was ready, but core restart might have issues or service became unavailable. Please check.")); showNotice(
"info",
t(
"Service was ready, but core restart might have issues or service became unavailable. Please check.",
),
);
} else { } else {
showNotice('error', t("Service installation or core restart encountered issues. Service might not be available. Please check system logs.")); showNotice(
"error",
t(
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.",
),
);
} }
return finalServiceStatus; return finalServiceStatus;
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
// 尝试性回退或最终操作 // 尝试性回退或最终操作
try { try {
showNotice('info', t("Attempting to restart core as a fallback...")); showNotice("info", t("Attempting to restart core as a fallback..."));
await restartCore(); await restartCore();
await mutateRunningMode(); await mutateRunningMode();
await isServiceAvailable().then(status => mutate("isServiceAvailable", status, false)); await isServiceAvailable().then((status) =>
mutate("isServiceAvailable", status, false),
);
} catch (recoveryError: any) { } catch (recoveryError: any) {
showNotice('error', t("Fallback core restart also failed: {message}", { message: recoveryError.message })); showNotice(
"error",
t("Fallback core restart also failed: {message}", {
message: recoveryError.message,
}),
);
} }
return false; return false;
} }
}); });
return { installServiceAndRestartCore }; return { installServiceAndRestartCore };
} }

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View File

@@ -559,4 +559,4 @@
"Port Config": "Port-Konfiguration", "Port Config": "Port-Konfiguration",
"Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert", "Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert",
"Enable one-click random API port and key. Click to randomize the port and key": "Einstellsichere Zufalls-API-Port- und Schlüsselgenerierung aktivieren. Klicken Sie, um Port und Schlüssel zu randomisieren" "Enable one-click random API port and key. Click to randomize the port and key": "Einstellsichere Zufalls-API-Port- und Schlüsselgenerierung aktivieren. Klicken Sie, um Port und Schlüssel zu randomisieren"
} }

View File

@@ -559,4 +559,4 @@
"Port Config": "Configuración de puerto", "Port Config": "Configuración de puerto",
"Configuration saved successfully": "Configuración aleatoria guardada correctamente", "Configuration saved successfully": "Configuración aleatoria guardada correctamente",
"Enable one-click random API port and key. Click to randomize the port and key": "Habilitar la generación de puerto y clave API aleatorios con un solo clic. Haz clic para randomizar el puerto y la clave" "Enable one-click random API port and key. Click to randomize the port and key": "Habilitar la generación de puerto y clave API aleatorios con un solo clic. Haz clic para randomizar el puerto y la clave"
} }

View File

@@ -562,4 +562,4 @@
"Port Config": "ポート設定", "Port Config": "ポート設定",
"Configuration saved successfully": "ランダム設定を保存完了", "Configuration saved successfully": "ランダム設定を保存完了",
"Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください" "Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください"
} }

View File

@@ -399,4 +399,4 @@
"View Profile-Merge": "프로필-병합 보기", "View Profile-Merge": "프로필-병합 보기",
"Update Successful": "업데이트 성공", "Update Successful": "업데이트 성공",
"Update Failed": "업데이트 실패" "Update Failed": "업데이트 실패"
} }

View File

@@ -562,4 +562,4 @@
"Port Config": "端口設置", "Port Config": "端口設置",
"Configuration saved successfully": "配置保存完成", "Configuration saved successfully": "配置保存完成",
"Enable one-click random API port and key. Click to randomize the port and key": "開啟一鍵隨機 API 端口和密鑰,點進去就可以隨機端口和密鑰" "Enable one-click random API port and key. Click to randomize the port and key": "開啟一鍵隨機 API 端口和密鑰,點進去就可以隨機端口和密鑰"
} }

View File

@@ -26,7 +26,7 @@ const container = document.getElementById(mainElementId);
if (!container) { if (!container) {
throw new Error( throw new Error(
`No container '${mainElementId}' found to render application` `No container '${mainElementId}' found to render application`,
); );
} }
@@ -37,7 +37,7 @@ document.addEventListener("keydown", (event) => {
(event.altKey && ["ArrowLeft", "ArrowRight"].includes(event.key)) || (event.altKey && ["ArrowLeft", "ArrowRight"].includes(event.key)) ||
((event.ctrlKey || event.metaKey) && ((event.ctrlKey || event.metaKey) &&
["F", "G", "H", "J", "P", "Q", "R", "U"].includes( ["F", "G", "H", "J", "P", "Q", "R", "U"].includes(
event.key.toUpperCase() event.key.toUpperCase(),
)); ));
disabledShortcuts && event.preventDefault(); disabledShortcuts && event.preventDefault();
}); });
@@ -59,5 +59,5 @@ createRoot(container).render(
</AppDataProvider> </AppDataProvider>
</BaseErrorBoundary> </BaseErrorBoundary>
</ComposeContextProvider> </ComposeContextProvider>
</React.StrictMode> </React.StrictMode>,
); );

View File

@@ -50,86 +50,95 @@ const handleNoticeMessage = (
switch (status) { switch (status) {
case "import_sub_url::ok": case "import_sub_url::ok":
navigate("/profile", { state: { current: msg } }); navigate("/profile", { state: { current: msg } });
showNotice('success', t("Import Subscription Successful")); showNotice("success", t("Import Subscription Successful"));
break; break;
case "import_sub_url::error": case "import_sub_url::error":
navigate("/profile"); navigate("/profile");
showNotice('error', msg); showNotice("error", msg);
break; break;
case "set_config::error": case "set_config::error":
showNotice('error', msg); showNotice("error", msg);
break; break;
case "update_with_clash_proxy": case "update_with_clash_proxy":
showNotice('success', `${t("Update with Clash proxy successfully")} ${msg}`); showNotice(
"success",
`${t("Update with Clash proxy successfully")} ${msg}`,
);
break; break;
case "update_retry_with_clash": case "update_retry_with_clash":
showNotice('info', t("Update failed, retrying with Clash proxy...")); showNotice("info", t("Update failed, retrying with Clash proxy..."));
break; break;
case "update_failed_even_with_clash": case "update_failed_even_with_clash":
showNotice('error', `${t("Update failed even with Clash proxy")}: ${msg}`); showNotice(
"error",
`${t("Update failed even with Clash proxy")}: ${msg}`,
);
break; break;
case "update_failed": case "update_failed":
showNotice('error', msg); showNotice("error", msg);
break; break;
case "config_validate::boot_error": case "config_validate::boot_error":
showNotice('error', `${t("Boot Config Validation Failed")} ${msg}`); showNotice("error", `${t("Boot Config Validation Failed")} ${msg}`);
break; break;
case "config_validate::core_change": case "config_validate::core_change":
showNotice('error', `${t("Core Change Config Validation Failed")} ${msg}`); showNotice(
"error",
`${t("Core Change Config Validation Failed")} ${msg}`,
);
break; break;
case "config_validate::error": case "config_validate::error":
showNotice('error', `${t("Config Validation Failed")} ${msg}`); showNotice("error", `${t("Config Validation Failed")} ${msg}`);
break; break;
case "config_validate::process_terminated": case "config_validate::process_terminated":
showNotice('error', t("Config Validation Process Terminated")); showNotice("error", t("Config Validation Process Terminated"));
break; break;
case "config_validate::stdout_error": case "config_validate::stdout_error":
showNotice('error', `${t("Config Validation Failed")} ${msg}`); showNotice("error", `${t("Config Validation Failed")} ${msg}`);
break; break;
case "config_validate::script_error": case "config_validate::script_error":
showNotice('error', `${t("Script File Error")} ${msg}`); showNotice("error", `${t("Script File Error")} ${msg}`);
break; break;
case "config_validate::script_syntax_error": case "config_validate::script_syntax_error":
showNotice('error', `${t("Script Syntax Error")} ${msg}`); showNotice("error", `${t("Script Syntax Error")} ${msg}`);
break; break;
case "config_validate::script_missing_main": case "config_validate::script_missing_main":
showNotice('error', `${t("Script Missing Main")} ${msg}`); showNotice("error", `${t("Script Missing Main")} ${msg}`);
break; break;
case "config_validate::file_not_found": case "config_validate::file_not_found":
showNotice('error', `${t("File Not Found")} ${msg}`); showNotice("error", `${t("File Not Found")} ${msg}`);
break; break;
case "config_validate::yaml_syntax_error": case "config_validate::yaml_syntax_error":
showNotice('error', `${t("YAML Syntax Error")} ${msg}`); showNotice("error", `${t("YAML Syntax Error")} ${msg}`);
break; break;
case "config_validate::yaml_read_error": case "config_validate::yaml_read_error":
showNotice('error', `${t("YAML Read Error")} ${msg}`); showNotice("error", `${t("YAML Read Error")} ${msg}`);
break; break;
case "config_validate::yaml_mapping_error": case "config_validate::yaml_mapping_error":
showNotice('error', `${t("YAML Mapping Error")} ${msg}`); showNotice("error", `${t("YAML Mapping Error")} ${msg}`);
break; break;
case "config_validate::yaml_key_error": case "config_validate::yaml_key_error":
showNotice('error', `${t("YAML Key Error")} ${msg}`); showNotice("error", `${t("YAML Key Error")} ${msg}`);
break; break;
case "config_validate::yaml_error": case "config_validate::yaml_error":
showNotice('error', `${t("YAML Error")} ${msg}`); showNotice("error", `${t("YAML Error")} ${msg}`);
break; break;
case "config_validate::merge_syntax_error": case "config_validate::merge_syntax_error":
showNotice('error', `${t("Merge File Syntax Error")} ${msg}`); showNotice("error", `${t("Merge File Syntax Error")} ${msg}`);
break; break;
case "config_validate::merge_mapping_error": case "config_validate::merge_mapping_error":
showNotice('error', `${t("Merge File Mapping Error")} ${msg}`); showNotice("error", `${t("Merge File Mapping Error")} ${msg}`);
break; break;
case "config_validate::merge_key_error": case "config_validate::merge_key_error":
showNotice('error', `${t("Merge File Key Error")} ${msg}`); showNotice("error", `${t("Merge File Key Error")} ${msg}`);
break; break;
case "config_validate::merge_error": case "config_validate::merge_error":
showNotice('error', `${t("Merge File Error")} ${msg}`); showNotice("error", `${t("Merge File Error")} ${msg}`);
break; break;
case "config_core::change_success": case "config_core::change_success":
showNotice('success', `${t("Core Changed Successfully")}: ${msg}`); showNotice("success", `${t("Core Changed Successfully")}: ${msg}`);
break; break;
case "config_core::change_error": case "config_core::change_error":
showNotice('error', `${t("Failed to Change Core")}: ${msg}`); showNotice("error", `${t("Failed to Change Core")}: ${msg}`);
break; break;
default: // Optional: Log unhandled statuses default: // Optional: Log unhandled statuses
console.warn(`[通知监听 V2] 未处理的状态: ${status}`); console.warn(`[通知监听 V2] 未处理的状态: ${status}`);
@@ -190,7 +199,6 @@ const Layout = () => {
mutate("getAutotemProxy"); mutate("getAutotemProxy");
}), }),
addListener("verge://notice-message", ({ payload }) => addListener("verge://notice-message", ({ payload }) =>
handleNotice(payload as [string, string]), handleNotice(payload as [string, string]),
), ),
@@ -276,7 +284,7 @@ const Layout = () => {
return unlisten; return unlisten;
} catch (err) { } catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err); console.error("[Layout] 监听启动完成事件失败:", err);
return () => { }; return () => {};
} }
}; };
@@ -298,7 +306,7 @@ const Layout = () => {
const unlistenPromise = listenStartupCompleted(); const unlistenPromise = listenStartupCompleted();
return () => { return () => {
unlistenPromise.then(unlisten => unlisten()); unlistenPromise.then((unlisten) => unlisten());
}; };
}, []); }, []);
@@ -368,11 +376,11 @@ const Layout = () => {
({ palette }) => ({ bgcolor: palette.background.paper }), ({ palette }) => ({ bgcolor: palette.background.paper }),
OS === "linux" OS === "linux"
? { ? {
borderRadius: "8px", borderRadius: "8px",
border: "1px solid var(--divider-color)", border: "1px solid var(--divider-color)",
width: "calc(100vw - 4px)", width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)", height: "calc(100vh - 4px)",
} }
: {}, : {},
]} ]}
> >
@@ -420,8 +428,7 @@ const Layout = () => {
</div> </div>
<div className="layout__right"> <div className="layout__right">
<div className="the-bar"> <div className="the-bar"></div>
</div>
<div className="the-content"> <div className="the-content">
{React.cloneElement(routersEles, { key: location.pathname })} {React.cloneElement(routersEles, { key: location.pathname })}

View File

@@ -43,7 +43,7 @@ const ConnectionsPage = () => {
const isDark = theme.palette.mode === "dark"; const isDark = theme.palette.mode === "dark";
const [match, setMatch] = useState(() => (_: string) => true); const [match, setMatch] = useState(() => (_: string) => true);
const [curOrderOpt, setOrderOpt] = useState("Default"); const [curOrderOpt, setOrderOpt] = useState("Default");
// 使用全局数据 // 使用全局数据
const { connections } = useAppData(); const { connections } = useAppData();
@@ -69,19 +69,21 @@ const ConnectionsPage = () => {
// 使用全局连接数据 // 使用全局连接数据
const displayData = useMemo(() => { const displayData = useMemo(() => {
if (!pageVisible) return initConn; if (!pageVisible) return initConn;
if (isPaused) { if (isPaused) {
return frozenData ?? { return (
uploadTotal: connections.uploadTotal, frozenData ?? {
downloadTotal: connections.downloadTotal, uploadTotal: connections.uploadTotal,
connections: connections.data downloadTotal: connections.downloadTotal,
}; connections: connections.data,
}
);
} }
return { return {
uploadTotal: connections.uploadTotal, uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal, downloadTotal: connections.downloadTotal,
connections: connections.data connections: connections.data,
}; };
}, [isPaused, frozenData, connections, pageVisible]); }, [isPaused, frozenData, connections, pageVisible]);
@@ -113,7 +115,7 @@ const ConnectionsPage = () => {
setFrozenData({ setFrozenData({
uploadTotal: connections.uploadTotal, uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal, downloadTotal: connections.downloadTotal,
connections: connections.data connections: connections.data,
}); });
} else { } else {
setFrozenData(null); setFrozenData(null);

View File

@@ -398,4 +398,4 @@ const ClashModeEnhancedCard = () => {
); );
}; };
export default HomePage; export default HomePage;

View File

@@ -80,7 +80,7 @@ const ProfilePage = () => {
for (let file of paths) { for (let file of paths) {
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) { if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
showNotice('error', t("Only YAML Files Supported")); showNotice("error", t("Only YAML Files Supported"));
continue; continue;
} }
const item = { const item = {
@@ -145,31 +145,34 @@ const ProfilePage = () => {
try { try {
// 尝试正常导入 // 尝试正常导入
await importProfile(url); await importProfile(url);
showNotice('success', t("Profile Imported Successfully")); showNotice("success", t("Profile Imported Successfully"));
setUrl(""); setUrl("");
mutateProfiles(); mutateProfiles();
await onEnhance(false); await onEnhance(false);
} catch (err: any) { } catch (err: any) {
// 首次导入失败,尝试使用自身代理 // 首次导入失败,尝试使用自身代理
const errmsg = err.message || err.toString(); const errmsg = err.message || err.toString();
showNotice('info', t("Import failed, retrying with Clash proxy...")); showNotice("info", t("Import failed, retrying with Clash proxy..."));
try { try {
// 使用自身代理尝试导入 // 使用自身代理尝试导入
await importProfile(url, { await importProfile(url, {
with_proxy: false, with_proxy: false,
self_proxy: true self_proxy: true,
}); });
// 回退导入成功 // 回退导入成功
showNotice('success', t("Profile Imported with Clash proxy")); showNotice("success", t("Profile Imported with Clash proxy"));
setUrl(""); setUrl("");
mutateProfiles(); mutateProfiles();
await onEnhance(false); await onEnhance(false);
} catch (retryErr: any) { } catch (retryErr: any) {
// 回退导入也失败 // 回退导入也失败
const retryErrmsg = retryErr?.message || retryErr.toString(); const retryErrmsg = retryErr?.message || retryErr.toString();
showNotice('error', `${t("Import failed even with Clash proxy")}: ${retryErrmsg}`); showNotice(
"error",
`${t("Import failed even with Clash proxy")}: ${retryErrmsg}`,
);
} }
} finally { } finally {
setDisabled(false); setDisabled(false);
@@ -199,10 +202,10 @@ const ProfilePage = () => {
closeAllConnections(); closeAllConnections();
await activateSelected(); await activateSelected();
if (notifySuccess && success) { if (notifySuccess && success) {
showNotice('success', t("Profile Switched"), 1000); showNotice("success", t("Profile Switched"), 1000);
} }
} catch (err: any) { } catch (err: any) {
showNotice('error', err?.message || err.toString(), 4000); showNotice("error", err?.message || err.toString(), 4000);
} finally { } finally {
clearTimeout(reset); clearTimeout(reset);
setActivatings([]); setActivatings([]);
@@ -228,10 +231,10 @@ const ProfilePage = () => {
await enhanceProfiles(); await enhanceProfiles();
mutateLogs(); mutateLogs();
if (notifySuccess) { if (notifySuccess) {
showNotice('success', t("Profile Reactivated"), 1000); showNotice("success", t("Profile Reactivated"), 1000);
} }
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString(), 3000); showNotice("error", err.message || err.toString(), 3000);
} finally { } finally {
setActivatings([]); setActivatings([]);
} }
@@ -246,7 +249,7 @@ const ProfilePage = () => {
mutateLogs(); mutateLogs();
current && (await onEnhance(false)); current && (await onEnhance(false));
} catch (err: any) { } catch (err: any) {
showNotice('error', err?.message || err.toString()); showNotice("error", err?.message || err.toString());
} finally { } finally {
setActivatings([]); setActivatings([]);
} }
@@ -300,12 +303,12 @@ const ProfilePage = () => {
let timeoutId: ReturnType<typeof setTimeout> | undefined; let timeoutId: ReturnType<typeof setTimeout> | undefined;
const setupListener = async () => { const setupListener = async () => {
unlistenPromise = listen<string>('profile-changed', (event) => { unlistenPromise = listen<string>("profile-changed", (event) => {
console.log('Profile changed event received:', event.payload); console.log("Profile changed event received:", event.payload);
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
mutateProfiles(); mutateProfiles();
timeoutId = undefined; timeoutId = undefined;
@@ -319,7 +322,7 @@ const ProfilePage = () => {
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
unlistenPromise?.then(unlisten => unlisten()); unlistenPromise?.then((unlisten) => unlisten());
}; };
}, [mutateProfiles, t]); }, [mutateProfiles, t]);
@@ -398,7 +401,7 @@ const ProfilePage = () => {
<ClearRounded fontSize="inherit" /> <ClearRounded fontSize="inherit" />
</IconButton> </IconButton>
), ),
} },
}} }}
/> />
<LoadingButton <LoadingButton

View File

@@ -20,8 +20,8 @@ const ProxyPage = () => {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateIfStale: true, revalidateIfStale: true,
dedupingInterval: 1000, dedupingInterval: 1000,
errorRetryInterval: 5000 errorRetryInterval: 5000,
} },
); );
const { verge } = useVerge(); const { verge } = useVerge();

View File

@@ -15,7 +15,7 @@ const SettingPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const onError = (err: any) => { const onError = (err: any) => {
showNotice('error', err?.message || err.toString()); showNotice("error", err?.message || err.toString());
}; };
const toGithubRepo = useLockFn(() => { const toGithubRepo = useLockFn(() => {

View File

@@ -130,7 +130,9 @@ const UnlockPage = () => {
): Promise<T> => { ): Promise<T> => {
return Promise.race([ return Promise.race([
invoke<T>(cmd, args), invoke<T>(cmd, args),
new Promise<T>((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)), new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeout),
),
]); ]);
}; };
@@ -138,7 +140,8 @@ const UnlockPage = () => {
const checkAllMedia = useLockFn(async () => { const checkAllMedia = useLockFn(async () => {
try { try {
setIsCheckingAll(true); setIsCheckingAll(true);
const result = await invokeWithTimeout<UnlockItem[]>("check_media_unlock"); const result =
await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
const sortedItems = sortItemsByName(result); const sortedItems = sortItemsByName(result);
setUnlockItems(sortedItems); setUnlockItems(sortedItems);
@@ -150,7 +153,7 @@ const UnlockPage = () => {
setIsCheckingAll(false); setIsCheckingAll(false);
} catch (err: any) { } catch (err: any) {
setIsCheckingAll(false); setIsCheckingAll(false);
showNotice('error', err?.message || err?.toString() || '检测超时或失败'); showNotice("error", err?.message || err?.toString() || "检测超时或失败");
alert("检测超时或失败: " + (err?.message || err)); alert("检测超时或失败: " + (err?.message || err));
console.error("Failed to check media unlock:", err); console.error("Failed to check media unlock:", err);
} }
@@ -160,7 +163,8 @@ const UnlockPage = () => {
const checkSingleMedia = useLockFn(async (name: string) => { const checkSingleMedia = useLockFn(async (name: string) => {
try { try {
setLoadingItems((prev) => [...prev, name]); setLoadingItems((prev) => [...prev, name]);
const result = await invokeWithTimeout<UnlockItem[]>("check_media_unlock"); const result =
await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
const targetItem = result.find((item: UnlockItem) => item.name === name); const targetItem = result.find((item: UnlockItem) => item.name === name);
@@ -181,7 +185,7 @@ const UnlockPage = () => {
setLoadingItems((prev) => prev.filter((item) => item !== name)); setLoadingItems((prev) => prev.filter((item) => item !== name));
} catch (err: any) { } catch (err: any) {
setLoadingItems((prev) => prev.filter((item) => item !== name)); setLoadingItems((prev) => prev.filter((item) => item !== name));
showNotice('error', err?.message || err?.toString() || `检测${name}失败`); showNotice("error", err?.message || err?.toString() || `检测${name}失败`);
alert("检测超时或失败: " + (err?.message || err)); alert("检测超时或失败: " + (err?.message || err));
console.error(`Failed to check ${name}:`, err); console.error(`Failed to check ${name}:`, err);
} }

View File

@@ -23,7 +23,7 @@
if (eventType !== "change" || typeof listener !== "function") { if (eventType !== "change" || typeof listener !== "function") {
console.error( console.error(
"Invalid arguments for removeEventListener:", "Invalid arguments for removeEventListener:",
arguments arguments,
); );
return; return;
} }

View File

@@ -1,7 +1,13 @@
import { createContext, useContext, useMemo } from "react"; import { createContext, useContext, useMemo } from "react";
import useSWR from "swr"; import useSWR from "swr";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
import { getProxies, getRules, getClashConfig, getProxyProviders, getRuleProviders } from "@/services/api"; import {
getProxies,
getRules,
getClashConfig,
getProxyProviders,
getRuleProviders,
} from "@/services/api";
import { getSystemProxy, getRunningMode, getAppUptime } from "@/services/cmds"; import { getSystemProxy, getRunningMode, getAppUptime } from "@/services/cmds";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
import { createAuthSockette } from "@/utils/websocket"; import { createAuthSockette } from "@/utils/websocket";
@@ -23,8 +29,8 @@ interface AppDataContextType {
uploadTotal: number; uploadTotal: number;
downloadTotal: number; downloadTotal: number;
}; };
traffic: {up: number; down: number}; traffic: { up: number; down: number };
memory: {inuse: number}; memory: { inuse: number };
refreshProxy: () => Promise<any>; refreshProxy: () => Promise<any>;
refreshClashConfig: () => Promise<any>; refreshClashConfig: () => Promise<any>;
refreshRules: () => Promise<any>; refreshRules: () => Promise<any>;
@@ -38,33 +44,37 @@ interface AppDataContextType {
const AppDataContext = createContext<AppDataContextType | null>(null); const AppDataContext = createContext<AppDataContextType | null>(null);
// 全局数据提供者组件 // 全局数据提供者组件
export const AppDataProvider = ({ children }: { children: React.ReactNode }) => { export const AppDataProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const { clashInfo } = useClashInfo(); const { clashInfo } = useClashInfo();
const pageVisible = useVisibility(); const pageVisible = useVisibility();
// 基础数据 - 中频率更新 (5秒) // 基础数据 - 中频率更新 (5秒)
const { data: proxiesData, mutate: refreshProxy } = useSWR( const { data: proxiesData, mutate: refreshProxy } = useSWR(
"getProxies", "getProxies",
getProxies, getProxies,
{ {
refreshInterval: 5000, refreshInterval: 5000,
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3 errorRetryCount: 3,
} },
); );
const { data: clashConfig, mutate: refreshClashConfig } = useSWR( const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
"getClashConfig", "getClashConfig",
getClashConfig, getClashConfig,
{ {
refreshInterval: 5000, refreshInterval: 5000,
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3 errorRetryCount: 3,
} },
); );
// 提供者数据 // 提供者数据
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR( const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
"getProxyProviders", "getProxyProviders",
@@ -74,219 +84,291 @@ export const AppDataProvider = ({ children }: { children: React.ReactNode }) =>
revalidateOnReconnect: false, revalidateOnReconnect: false,
dedupingInterval: 3000, dedupingInterval: 3000,
suspense: false, suspense: false,
errorRetryCount: 3 errorRetryCount: 3,
} },
); );
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR( const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
"getRuleProviders", "getRuleProviders",
getRuleProviders, getRuleProviders,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3 errorRetryCount: 3,
} },
); );
// 低频率更新数据 // 低频率更新数据
const { data: rulesData, mutate: refreshRules } = useSWR( const { data: rulesData, mutate: refreshRules } = useSWR(
"getRules", "getRules",
getRules, getRules,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3 errorRetryCount: 3,
} },
); );
const { data: sysproxy, mutate: refreshSysproxy } = useSWR( const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
"getSystemProxy", "getSystemProxy",
getSystemProxy, getSystemProxy,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3 errorRetryCount: 3,
} },
); );
const { data: runningMode } = useSWR( const { data: runningMode } = useSWR("getRunningMode", getRunningMode, {
"getRunningMode", revalidateOnFocus: false,
getRunningMode, suspense: false,
{ errorRetryCount: 3,
revalidateOnFocus: false, });
suspense: false,
errorRetryCount: 3
}
);
// 高频率更新数据 (2秒) // 高频率更新数据 (2秒)
const { data: uptimeData } = useSWR( const { data: uptimeData } = useSWR("appUptime", getAppUptime, {
"appUptime", refreshInterval: 2000,
getAppUptime, revalidateOnFocus: false,
{ suspense: false,
refreshInterval: 2000, });
revalidateOnFocus: false,
suspense: false
}
);
// 连接数据 - 使用WebSocket实时更新 // 连接数据 - 使用WebSocket实时更新
const { data: connectionsData = { connections: [], uploadTotal: 0, downloadTotal: 0 } } = const {
useSWRSubscription( data: connectionsData = {
clashInfo && pageVisible ? "connections" : null, connections: [],
(_key, { next }) => { uploadTotal: 0,
if (!clashInfo || !pageVisible) return () => {}; downloadTotal: 0,
},
const { server = "", secret = "" } = clashInfo; } = useSWRSubscription(
if (!server) return () => {}; clashInfo && pageVisible ? "connections" : null,
(_key, { next }) => {
console.log(`[Connections][${AppDataProvider.name}] 正在连接: ${server}/connections`); if (!clashInfo || !pageVisible) return () => {};
const socket = createAuthSockette(`${server}/connections`, secret, {
timeout: 5000, const { server = "", secret = "" } = clashInfo;
onmessage(event) { if (!server) return () => {};
try {
const data = JSON.parse(event.data); console.log(
// 处理连接数据,计算当前上传下载速度 `[Connections][${AppDataProvider.name}] 正在连接: ${server}/connections`,
next(null, (prev: any = { connections: [], uploadTotal: 0, downloadTotal: 0 }) => { );
const socket = createAuthSockette(`${server}/connections`, secret, {
timeout: 5000,
onmessage(event) {
try {
const data = JSON.parse(event.data);
// 处理连接数据,计算当前上传下载速度
next(
null,
(
prev: any = {
connections: [],
uploadTotal: 0,
downloadTotal: 0,
},
) => {
const oldConns = prev.connections || []; const oldConns = prev.connections || [];
const newConns = data.connections || []; const newConns = data.connections || [];
// 计算当前速度 // 计算当前速度
const processedConns = newConns.map((conn: any) => { const processedConns = newConns.map((conn: any) => {
const oldConn = oldConns.find((old: any) => old.id === conn.id); const oldConn = oldConns.find(
(old: any) => old.id === conn.id,
);
if (oldConn) { if (oldConn) {
return { return {
...conn, ...conn,
curUpload: conn.upload - oldConn.upload, curUpload: conn.upload - oldConn.upload,
curDownload: conn.download - oldConn.download curDownload: conn.download - oldConn.download,
}; };
} }
return { ...conn, curUpload: 0, curDownload: 0 }; return { ...conn, curUpload: 0, curDownload: 0 };
}); });
return { return {
...data, ...data,
connections: processedConns connections: processedConns,
}; };
}); },
} catch (err) { );
console.error(`[Connections][${AppDataProvider.name}] 解析数据错误:`, err, event.data); } catch (err) {
} console.error(
}, `[Connections][${AppDataProvider.name}] 解析数据错误:`,
onopen: (event) => { err,
console.log(`[Connections][${AppDataProvider.name}] WebSocket 连接已建立`, event); event.data,
}, );
onerror(event) {
console.error(`[Connections][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`, event);
next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 });
},
onclose: (event) => {
console.log(`[Connections][${AppDataProvider.name}] WebSocket 连接关闭`, event.code, event.reason);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Connections][${AppDataProvider.name}] 连接非正常关闭,重置数据`);
next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 });
}
} }
}); },
onopen: (event) => {
return () => { console.log(
console.log(`[Connections][${AppDataProvider.name}] 清理WebSocket连接`); `[Connections][${AppDataProvider.name}] WebSocket 连接已建立`,
socket.close(); event,
}; );
} },
); onerror(event) {
console.error(
`[Connections][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 });
},
onclose: (event) => {
console.log(
`[Connections][${AppDataProvider.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Connections][${AppDataProvider.name}] 连接非正常关闭,重置数据`,
);
next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 });
}
},
});
return () => {
console.log(`[Connections][${AppDataProvider.name}] 清理WebSocket连接`);
socket.close();
};
},
);
// 流量和内存数据 - 通过WebSocket获取实时流量数据 // 流量和内存数据 - 通过WebSocket获取实时流量数据
const { data: trafficData = { up: 0, down: 0 } } = useSWRSubscription( const { data: trafficData = { up: 0, down: 0 } } = useSWRSubscription(
clashInfo && pageVisible ? "traffic" : null, clashInfo && pageVisible ? "traffic" : null,
(_key, { next }) => { (_key, { next }) => {
if (!clashInfo || !pageVisible) return () => {}; if (!clashInfo || !pageVisible) return () => {};
const { server = "", secret = "" } = clashInfo; const { server = "", secret = "" } = clashInfo;
if (!server) return () => {}; if (!server) return () => {};
console.log(`[Traffic][${AppDataProvider.name}] 正在连接: ${server}/traffic`); console.log(
`[Traffic][${AppDataProvider.name}] 正在连接: ${server}/traffic`,
);
const socket = createAuthSockette(`${server}/traffic`, secret, { const socket = createAuthSockette(`${server}/traffic`, secret, {
onmessage(event) { onmessage(event) {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data && typeof data.up === 'number' && typeof data.down === 'number') { if (
data &&
typeof data.up === "number" &&
typeof data.down === "number"
) {
next(null, data); next(null, data);
} else { } else {
console.warn(`[Traffic][${AppDataProvider.name}] 收到无效数据:`, data); console.warn(
`[Traffic][${AppDataProvider.name}] 收到无效数据:`,
data,
);
} }
} catch (err) { } catch (err) {
console.error(`[Traffic][${AppDataProvider.name}] 解析数据错误:`, err, event.data); console.error(
`[Traffic][${AppDataProvider.name}] 解析数据错误:`,
err,
event.data,
);
} }
}, },
onopen: (event) => { onopen: (event) => {
console.log(`[Traffic][${AppDataProvider.name}] WebSocket 连接已建立`, event); console.log(
`[Traffic][${AppDataProvider.name}] WebSocket 连接已建立`,
event,
);
}, },
onerror(event) { onerror(event) {
console.error(`[Traffic][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`, event); console.error(
`[Traffic][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
next(null, { up: 0, down: 0 }); next(null, { up: 0, down: 0 });
}, },
onclose: (event) => { onclose: (event) => {
console.log(`[Traffic][${AppDataProvider.name}] WebSocket 连接关闭`, event.code, event.reason); console.log(
if (event.code !== 1000 && event.code !== 1001) { `[Traffic][${AppDataProvider.name}] WebSocket 连接关闭`,
console.warn(`[Traffic][${AppDataProvider.name}] 连接非正常关闭,重置数据`); event.code,
next(null, { up: 0, down: 0 }); event.reason,
} );
} if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Traffic][${AppDataProvider.name}] 连接非正常关闭,重置数据`,
);
next(null, { up: 0, down: 0 });
}
},
}); });
return () => { return () => {
console.log(`[Traffic][${AppDataProvider.name}] 清理WebSocket连接`); console.log(`[Traffic][${AppDataProvider.name}] 清理WebSocket连接`);
socket.close(); socket.close();
}; };
} },
); );
const { data: memoryData = { inuse: 0 } } = useSWRSubscription( const { data: memoryData = { inuse: 0 } } = useSWRSubscription(
clashInfo && pageVisible ? "memory" : null, clashInfo && pageVisible ? "memory" : null,
(_key, { next }) => { (_key, { next }) => {
if (!clashInfo || !pageVisible) return () => {}; if (!clashInfo || !pageVisible) return () => {};
const { server = "", secret = "" } = clashInfo; const { server = "", secret = "" } = clashInfo;
if (!server) return () => {}; if (!server) return () => {};
console.log(`[Memory][${AppDataProvider.name}] 正在连接: ${server}/memory`); console.log(
`[Memory][${AppDataProvider.name}] 正在连接: ${server}/memory`,
);
const socket = createAuthSockette(`${server}/memory`, secret, { const socket = createAuthSockette(`${server}/memory`, secret, {
onmessage(event) { onmessage(event) {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data && typeof data.inuse === 'number') { if (data && typeof data.inuse === "number") {
next(null, data); next(null, data);
} else { } else {
console.warn(`[Memory][${AppDataProvider.name}] 收到无效数据:`, data); console.warn(
`[Memory][${AppDataProvider.name}] 收到无效数据:`,
data,
);
} }
} catch (err) { } catch (err) {
console.error(`[Memory][${AppDataProvider.name}] 解析数据错误:`, err, event.data); console.error(
`[Memory][${AppDataProvider.name}] 解析数据错误:`,
err,
event.data,
);
} }
}, },
onopen: (event) => { onopen: (event) => {
console.log(`[Memory][${AppDataProvider.name}] WebSocket 连接已建立`, event); console.log(
`[Memory][${AppDataProvider.name}] WebSocket 连接已建立`,
event,
);
}, },
onerror(event) { onerror(event) {
console.error(`[Memory][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`, event); console.error(
`[Memory][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
next(null, { inuse: 0 }); next(null, { inuse: 0 });
}, },
onclose: (event) => { onclose: (event) => {
console.log(`[Memory][${AppDataProvider.name}] WebSocket 连接关闭`, event.code, event.reason); console.log(
`[Memory][${AppDataProvider.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) { if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Memory][${AppDataProvider.name}] 连接非正常关闭,重置数据`); console.warn(
`[Memory][${AppDataProvider.name}] 连接非正常关闭,重置数据`,
);
next(null, { inuse: 0 }); next(null, { inuse: 0 });
} }
} },
}); });
return () => { return () => {
console.log(`[Memory][${AppDataProvider.name}] 清理WebSocket连接`); console.log(`[Memory][${AppDataProvider.name}] 清理WebSocket连接`);
socket.close(); socket.close();
}; };
} },
); );
// 提供统一的刷新方法 // 提供统一的刷新方法
const refreshAll = async () => { const refreshAll = async () => {
await Promise.all([ await Promise.all([
@@ -295,66 +377,79 @@ export const AppDataProvider = ({ children }: { children: React.ReactNode }) =>
refreshRules(), refreshRules(),
refreshSysproxy(), refreshSysproxy(),
refreshProxyProviders(), refreshProxyProviders(),
refreshRuleProviders() refreshRuleProviders(),
]); ]);
}; };
// 聚合所有数据 // 聚合所有数据
const value = useMemo(() => ({ const value = useMemo(
// 数据 () => ({
proxies: proxiesData, // 数据
clashConfig, proxies: proxiesData,
rules: rulesData || [], clashConfig,
sysproxy, rules: rulesData || [],
runningMode, sysproxy,
uptime: uptimeData || 0, runningMode,
uptime: uptimeData || 0,
// 提供者数据
proxyProviders: proxyProviders || {}, // 提供者数据
ruleProviders: ruleProviders || {}, proxyProviders: proxyProviders || {},
ruleProviders: ruleProviders || {},
// 连接数据
connections: { // 连接数据
data: connectionsData.connections || [], connections: {
count: connectionsData.connections?.length || 0, data: connectionsData.connections || [],
uploadTotal: connectionsData.uploadTotal || 0, count: connectionsData.connections?.length || 0,
downloadTotal: connectionsData.downloadTotal || 0 uploadTotal: connectionsData.uploadTotal || 0,
}, downloadTotal: connectionsData.downloadTotal || 0,
},
// 实时流量数据
traffic: trafficData, // 实时流量数据
memory: memoryData, traffic: trafficData,
memory: memoryData,
// 刷新方法
refreshProxy, // 刷新方法
refreshClashConfig, refreshProxy,
refreshRules, refreshClashConfig,
refreshSysproxy, refreshRules,
refreshProxyProviders, refreshSysproxy,
refreshRuleProviders, refreshProxyProviders,
refreshAll refreshRuleProviders,
}), [ refreshAll,
proxiesData, clashConfig, rulesData, sysproxy, }),
runningMode, uptimeData, connectionsData, [
trafficData, memoryData, proxyProviders, ruleProviders, proxiesData,
refreshProxy, refreshClashConfig, refreshRules, refreshSysproxy, clashConfig,
refreshProxyProviders, refreshRuleProviders rulesData,
]); sysproxy,
runningMode,
uptimeData,
connectionsData,
trafficData,
memoryData,
proxyProviders,
ruleProviders,
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
],
);
return ( return (
<AppDataContext.Provider value={value}> <AppDataContext.Provider value={value}>{children}</AppDataContext.Provider>
{children}
</AppDataContext.Provider>
); );
}; };
// 自定义Hook访问全局数据 // 自定义Hook访问全局数据
export const useAppData = () => { export const useAppData = () => {
const context = useContext(AppDataContext); const context = useContext(AppDataContext);
if (!context) { if (!context) {
throw new Error("useAppData必须在AppDataProvider内使用"); throw new Error("useAppData必须在AppDataProvider内使用");
} }
return context; return context;
}; };

View File

@@ -199,10 +199,12 @@ export const getProxyProviders = async () => {
providers: Record<string, IProxyProviderItem>; providers: Record<string, IProxyProviderItem>;
}>("get_providers_proxies"); }>("get_providers_proxies");
if (!response || !response.providers) { if (!response || !response.providers) {
console.warn("getProxyProviders: Invalid response structure, returning empty object"); console.warn(
"getProxyProviders: Invalid response structure, returning empty object",
);
return {}; return {};
} }
const providers = response.providers as Record<string, IProxyProviderItem>; const providers = response.providers as Record<string, IProxyProviderItem>;
return Object.fromEntries( return Object.fromEntries(
@@ -351,65 +353,65 @@ const IP_CHECK_SERVICES: ServiceConfig[] = [
{ {
url: "https://api.ip.sb/geoip", url: "https://api.ip.sb/geoip",
mapping: (data) => ({ mapping: (data) => ({
ip: data.ip || '', ip: data.ip || "",
country_code: data.country_code || '', country_code: data.country_code || "",
country: data.country || '', country: data.country || "",
region: data.region || '', region: data.region || "",
city: data.city || '', city: data.city || "",
organization: data.organization || data.isp || '', organization: data.organization || data.isp || "",
asn: data.asn || 0, asn: data.asn || 0,
asn_organization: data.asn_organization || '', asn_organization: data.asn_organization || "",
longitude: data.longitude || 0, longitude: data.longitude || 0,
latitude: data.latitude || 0, latitude: data.latitude || 0,
timezone: data.timezone || '', timezone: data.timezone || "",
}), }),
}, },
{ {
url: "https://ipapi.co/json", url: "https://ipapi.co/json",
mapping: (data) => ({ mapping: (data) => ({
ip: data.ip || '', ip: data.ip || "",
country_code: data.country_code || '', country_code: data.country_code || "",
country: data.country_name || '', country: data.country_name || "",
region: data.region || '', region: data.region || "",
city: data.city || '', city: data.city || "",
organization: data.org || '', organization: data.org || "",
asn: data.asn? parseInt(data.asn.replace('AS', '')) : 0, asn: data.asn ? parseInt(data.asn.replace("AS", "")) : 0,
asn_organization: data.org || '', asn_organization: data.org || "",
longitude: data.longitude || 0, longitude: data.longitude || 0,
latitude: data.latitude || 0, latitude: data.latitude || 0,
timezone: data.timezone || '', timezone: data.timezone || "",
}), }),
}, },
{ {
url: "https://api.ipapi.is/", url: "https://api.ipapi.is/",
mapping: (data) => ({ mapping: (data) => ({
ip: data.ip || '', ip: data.ip || "",
country_code: data.location?.country_code || '', country_code: data.location?.country_code || "",
country: data.location?.country || '', country: data.location?.country || "",
region: data.location?.state || '', region: data.location?.state || "",
city: data.location?.city || '', city: data.location?.city || "",
organization: data.asn?.org || data.company?.name || '', organization: data.asn?.org || data.company?.name || "",
asn: data.asn?.asn || 0, asn: data.asn?.asn || 0,
asn_organization: data.asn?.org || '', asn_organization: data.asn?.org || "",
longitude: data.location?.longitude || 0, longitude: data.location?.longitude || 0,
latitude: data.location?.latitude || 0, latitude: data.location?.latitude || 0,
timezone: data.location?.timezone || '', timezone: data.location?.timezone || "",
}), }),
}, },
{ {
url: "https://ipwho.is/", url: "https://ipwho.is/",
mapping: (data) => ({ mapping: (data) => ({
ip: data.ip || '', ip: data.ip || "",
country_code: data.country_code || '', country_code: data.country_code || "",
country: data.country || '', country: data.country || "",
region: data.region || '', region: data.region || "",
city: data.city || '', city: data.city || "",
organization: data.connection?.org || data.connection?.isp || '', organization: data.connection?.org || data.connection?.isp || "",
asn: data.connection?.asn || 0, asn: data.connection?.asn || 0,
asn_organization: data.connection?.isp || '', asn_organization: data.connection?.isp || "",
longitude: data.longitude || 0, longitude: data.longitude || 0,
latitude: data.latitude || 0, latitude: data.latitude || 0,
timezone: data.timezone?.id || '', timezone: data.timezone?.id || "",
}), }),
}, },
]; ];
@@ -418,43 +420,39 @@ const IP_CHECK_SERVICES: ServiceConfig[] = [
function shuffleServices() { function shuffleServices() {
// 过滤无效服务并确保每个元素符合ServiceConfig接口 // 过滤无效服务并确保每个元素符合ServiceConfig接口
const validServices = IP_CHECK_SERVICES.filter( const validServices = IP_CHECK_SERVICES.filter(
(service): service is ServiceConfig => (service): service is ServiceConfig =>
service !== null && service !== null &&
service !== undefined && service !== undefined &&
typeof service.url === 'string' && typeof service.url === "string" &&
typeof service.mapping === 'function' // 添加对mapping属性的检查 typeof service.mapping === "function", // 添加对mapping属性的检查
); );
if (validServices.length === 0) { if (validServices.length === 0) {
console.error('No valid services found in IP_CHECK_SERVICES'); console.error("No valid services found in IP_CHECK_SERVICES");
return []; return [];
} }
// 使用单一Fisher-Yates洗牌算法增强随机性 // 使用单一Fisher-Yates洗牌算法增强随机性
const shuffled = [...validServices]; const shuffled = [...validServices];
const length = shuffled.length; const length = shuffled.length;
// 使用多个种子进行多次洗牌 // 使用多个种子进行多次洗牌
const seeds = [ const seeds = [Math.random(), Date.now() / 1000, performance.now() / 1000];
Math.random(),
Date.now() / 1000,
performance.now() / 1000
];
for (const seed of seeds) { for (const seed of seeds) {
const prng = createPrng(seed); const prng = createPrng(seed);
// Fisher-Yates洗牌算法 // Fisher-Yates洗牌算法
for (let i = length - 1; i > 0; i--) { for (let i = length - 1; i > 0; i--) {
const j = Math.floor(prng() * (i + 1)); const j = Math.floor(prng() * (i + 1));
// 使用临时变量进行交换,避免解构赋值可能的问题 // 使用临时变量进行交换,避免解构赋值可能的问题
const temp = shuffled[i]; const temp = shuffled[i];
shuffled[i] = shuffled[j]; shuffled[i] = shuffled[j];
shuffled[j] = temp; shuffled[j] = temp;
} }
} }
return shuffled; return shuffled;
} }
@@ -462,11 +460,11 @@ function shuffleServices() {
function createPrng(seed: number): () => number { function createPrng(seed: number): () => number {
// 使用xorshift32算法 // 使用xorshift32算法
let state = seed >>> 0; let state = seed >>> 0;
// 如果种子为0设置一个默认值 // 如果种子为0设置一个默认值
if (state === 0) state = 123456789; if (state === 0) state = 123456789;
return function() { return function () {
state ^= state << 13; state ^= state << 13;
state ^= state >>> 17; state ^= state >>> 17;
state ^= state << 5; state ^= state << 5;
@@ -522,7 +520,7 @@ export const getIpInfo = async (): Promise<IpInfo> => {
lastError = error; lastError = error;
console.log( console.log(
`尝试 ${attempt + 1}/${maxRetries} 失败 (${service.url}):`, `尝试 ${attempt + 1}/${maxRetries} 失败 (${service.url}):`,
error.message error.message,
); );
if (error.name === "AbortError") { if (error.name === "AbortError") {
@@ -530,7 +528,7 @@ export const getIpInfo = async (): Promise<IpInfo> => {
} }
if (attempt < maxRetries - 1) { if (attempt < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
} }
} }
} }
@@ -539,9 +537,9 @@ export const getIpInfo = async (): Promise<IpInfo> => {
if (lastError) { if (lastError) {
throw new Error(`所有IP检测服务都失败: ${lastError.message}`); throw new Error(`所有IP检测服务都失败: ${lastError.message}`);
} else { } else {
throw new Error('没有可用的IP检测服务'); throw new Error("没有可用的IP检测服务");
} }
} finally { } finally {
clearTimeout(overallTimeoutId); clearTimeout(overallTimeoutId);
} }
}; };

View File

@@ -1,8 +1,8 @@
import { ReactNode } from 'react'; import { ReactNode } from "react";
export interface NoticeItem { export interface NoticeItem {
id: number; id: number;
type: 'success' | 'error' | 'info'; type: "success" | "error" | "info";
message: ReactNode; message: ReactNode;
duration: number; duration: number;
timerId?: ReturnType<typeof setTimeout>; timerId?: ReturnType<typeof setTimeout>;
@@ -21,13 +21,13 @@ function notifyListeners() {
// Shows a notification. // Shows a notification.
export function showNotice( export function showNotice(
type: 'success' | 'error' | 'info', type: "success" | "error" | "info",
message: ReactNode, message: ReactNode,
duration?: number, duration?: number,
): number { ): number {
const id = nextId++; const id = nextId++;
const effectiveDuration = const effectiveDuration =
duration ?? (type === 'error' ? 8000 : type === 'info' ? 5000 : 3000); // Longer defaults duration ?? (type === "error" ? 8000 : type === "info" ? 5000 : 3000); // Longer defaults
const newNotice: NoticeItem = { const newNotice: NoticeItem = {
id, id,
@@ -38,12 +38,11 @@ export function showNotice(
// Auto-hide timer (only if duration is not null/0) // Auto-hide timer (only if duration is not null/0)
if (effectiveDuration > 0) { if (effectiveDuration > 0) {
newNotice.timerId = setTimeout(() => { newNotice.timerId = setTimeout(() => {
hideNotice(id); hideNotice(id);
}, effectiveDuration); }, effectiveDuration);
} }
notices = [...notices, newNotice]; notices = [...notices, newNotice];
notifyListeners(); notifyListeners();
return id; return id;
@@ -54,7 +53,7 @@ export function showNotice(
export function hideNotice(id: number) { export function hideNotice(id: number) {
const notice = notices.find((n) => n.id === id); const notice = notices.find((n) => n.id === id);
if (notice?.timerId) { if (notice?.timerId) {
clearTimeout(notice.timerId); // Clear timeout if manually closed clearTimeout(notice.timerId); // Clear timeout if manually closed
} }
notices = notices.filter((n) => n.id !== id); notices = notices.filter((n) => n.id !== id);
notifyListeners(); notifyListeners();
@@ -72,9 +71,9 @@ export function subscribeNotices(listener: Listener): () => void {
// Function to clear all notices at once // Function to clear all notices at once
export function clearAllNotices() { export function clearAllNotices() {
notices.forEach(n => { notices.forEach((n) => {
if (n.timerId) clearTimeout(n.timerId); if (n.timerId) clearTimeout(n.timerId);
}); });
notices = []; notices = [];
notifyListeners(); notifyListeners();
} }

View File

@@ -1,6 +1,6 @@
export default function debounce<T extends (...args: any[]) => void>( export default function debounce<T extends (...args: any[]) => void>(
func: T, func: T,
wait: number wait: number,
): T { ): T {
let timeout: ReturnType<typeof setTimeout> | null = null; let timeout: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>) { return function (this: any, ...args: Parameters<T>) {

View File

@@ -578,8 +578,8 @@ function URI_VLESS(line: string): IProxyVlessConfig {
proxy.network = "ws"; proxy.network = "ws";
httpupgrade = true; httpupgrade = true;
} else { } else {
proxy.network = ["tcp", "ws", "http", "grpc", "h2"].includes(params.type) proxy.network = ["tcp", "ws", "http", "grpc", "h2"].includes(params.type)
? (params.type as NetworkType) ? (params.type as NetworkType)
: "tcp"; : "tcp";
} }
if (!proxy.network && isShadowrocket && params.obfs) { if (!proxy.network && isShadowrocket && params.obfs) {