mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 17:15:38 +08:00
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:
2
.github/ISSUE_TEMPLATE/i18n_request.yml
vendored
2
.github/ISSUE_TEMPLATE/i18n_request.yml
vendored
@@ -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
51
.github/workflows/fmt.yml
vendored
Normal 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
|
||||||
@@ -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
7
.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# README.md
|
||||||
|
# UPDATELOG.md
|
||||||
|
# CONTRIBUTING.md
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
src-tauri/target/
|
||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"experimentalOperatorPosition": "start"
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -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 或任何高负载应用
|
||||||
|
|||||||
274
UPDATELOG.md
274
UPDATELOG.md
@@ -3,141 +3,153 @@
|
|||||||
尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。
|
尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。
|
||||||
|
|
||||||
#### ⚠️ 已知问题
|
#### ⚠️ 已知问题
|
||||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
|
||||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
- 仅在Ubuntu 22.04/24.04,Fedora 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.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
|
||||||
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
|
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
|
||||||
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
|
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||||
|
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
|
||||||
|
|
||||||
### 2.2.3 相对于 2.2.2
|
### 2.2.3 相对于 2.2.2
|
||||||
|
|
||||||
#### 修复了:
|
#### 修复了:
|
||||||
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
|
|
||||||
- “开机自启”和“DNS覆写”开关跳动问题
|
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
|
||||||
- 自定义托盘图标未能应用更改
|
- “开机自启”和“DNS覆写”开关跳动问题
|
||||||
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
|
- 自定义托盘图标未能应用更改
|
||||||
- MacOS 托盘速率显示不全
|
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
|
||||||
- Linux 在系统服务模式下无法拉起 Mihomo 内核
|
- MacOS 托盘速率显示不全
|
||||||
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
|
- Linux 在系统服务模式下无法拉起 Mihomo 内核
|
||||||
- 相同节点名称可能导致的页面渲染出错
|
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
|
||||||
- URL Schemes被截断的问题
|
- 相同节点名称可能导致的页面渲染出错
|
||||||
- 首页流量统计卡更好的时间戳范围
|
- URL Schemes被截断的问题
|
||||||
- 静默启动无法触发自动轻量化计时器
|
- 首页流量统计卡更好的时间戳范围
|
||||||
|
- 静默启动无法触发自动轻量化计时器
|
||||||
|
|
||||||
#### 新增了:
|
#### 新增了:
|
||||||
- Mihomo(Meta)内核升级至 1.19.4
|
|
||||||
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
|
- Mihomo(Meta)内核升级至 1.19.4
|
||||||
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
|
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
|
||||||
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
|
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
|
||||||
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
|
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
|
||||||
- 代理组显示节点数量
|
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
|
||||||
- 统一运行模式检测,支持管理员模式下开启TUN模式
|
- 代理组显示节点数量
|
||||||
- 托盘切换代理模式会根据设置自动断开之前连接
|
- 统一运行模式检测,支持管理员模式下开启TUN模式
|
||||||
- 如订阅获取失败回退使用Clash内核代理再次尝试
|
- 托盘切换代理模式会根据设置自动断开之前连接
|
||||||
|
- 如订阅获取失败回退使用Clash内核代理再次尝试
|
||||||
|
|
||||||
#### 移除了:
|
#### 移除了:
|
||||||
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
|
|
||||||
|
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
|
||||||
|
|
||||||
#### 优化了:
|
#### 优化了:
|
||||||
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
|
||||||
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
|
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
||||||
- 优化首页流量图表代码,调整图表文字边距
|
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
|
||||||
- MacOS 托盘速率更好的显示样式和更新逻辑
|
- 优化首页流量图表代码,调整图表文字边距
|
||||||
- 首页仅在有流量图表时显示流量图表区域
|
- MacOS 托盘速率更好的显示样式和更新逻辑
|
||||||
- 更新DNS默认覆写配置
|
- 首页仅在有流量图表时显示流量图表区域
|
||||||
- 移除测试目录,简化资源初始化逻辑
|
- 更新DNS默认覆写配置
|
||||||
|
- 移除测试目录,简化资源初始化逻辑
|
||||||
|
|
||||||
## v2.2.2
|
## v2.2.2
|
||||||
|
|
||||||
@@ -148,23 +160,29 @@
|
|||||||
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
||||||
|
|
||||||
#### 已知问题
|
#### 已知问题
|
||||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
|
||||||
|
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||||
|
|
||||||
### 2.2.2 相对于 2.2.1(已下架不再提供)
|
### 2.2.2 相对于 2.2.1(已下架不再提供)
|
||||||
|
|
||||||
#### 修复了:
|
#### 修复了:
|
||||||
- 弹黑框的问题(原因是服务崩溃触发重装机制)
|
|
||||||
- MacOS进入轻量模式以后隐藏Dock图标
|
- 弹黑框的问题(原因是服务崩溃触发重装机制)
|
||||||
- 增加轻量模式缺失的tray翻译
|
- MacOS进入轻量模式以后隐藏Dock图标
|
||||||
- Linux下的窗口边框被削掉的问题
|
- 增加轻量模式缺失的tray翻译
|
||||||
|
- Linux下的窗口边框被削掉的问题
|
||||||
|
|
||||||
#### 新增了:
|
#### 新增了:
|
||||||
- 加强服务检测和重装逻辑
|
|
||||||
- 增强内核与服务保活机制
|
- 加强服务检测和重装逻辑
|
||||||
- 增加服务模式下的僵尸进程清理机制
|
- 增强内核与服务保活机制
|
||||||
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
- 增加服务模式下的僵尸进程清理机制
|
||||||
|
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
||||||
|
|
||||||
### 2.2.1 相对于 2.2.0(已下架不再提供)
|
### 2.2.1 相对于 2.2.0(已下架不再提供)
|
||||||
|
|
||||||
#### 修复了:
|
#### 修复了:
|
||||||
|
|
||||||
1. **首页**
|
1. **首页**
|
||||||
- 修复 Direct 模式首页无法渲染
|
- 修复 Direct 模式首页无法渲染
|
||||||
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
||||||
@@ -181,6 +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 处理流量。
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export const BaseSearchBox = (props: SearchProps) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -107,7 +107,14 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
{information.map((each) => (
|
{information.map((each) => (
|
||||||
<div key={each.label}>
|
<div key={each.label}>
|
||||||
<b>{each.label}</b>
|
<b>{each.label}</b>
|
||||||
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span>
|
<span
|
||||||
|
style={{
|
||||||
|
wordBreak: "break-all",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
: {each.value}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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, // 平滑滚动
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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 },
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
fileDataRef.current = null;
|
fileDataRef.current = null;
|
||||||
setTimeout(() => formIns.reset(), 500);
|
setTimeout(() => formIns.reset(), 500);
|
||||||
} catch { }
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const text = {
|
const text = {
|
||||||
|
|||||||
@@ -66,27 +66,27 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
const filteredPrependSeq = useMemo(
|
const filteredPrependSeq = useMemo(
|
||||||
() => prependSeq.filter((proxy) => match(proxy.name)),
|
() => prependSeq.filter((proxy) => match(proxy.name)),
|
||||||
[prependSeq, match]
|
[prependSeq, match],
|
||||||
);
|
);
|
||||||
const filteredProxyList = useMemo(
|
const filteredProxyList = useMemo(
|
||||||
() => proxyList.filter((proxy) => match(proxy.name)),
|
() => proxyList.filter((proxy) => match(proxy.name)),
|
||||||
[proxyList, match]
|
[proxyList, match],
|
||||||
);
|
);
|
||||||
const filteredAppendSeq = useMemo(
|
const filteredAppendSeq = useMemo(
|
||||||
() => appendSeq.filter((proxy) => match(proxy.name)),
|
() => appendSeq.filter((proxy) => match(proxy.name)),
|
||||||
[appendSeq, match]
|
[appendSeq, match],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(PointerSensor),
|
||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
const reorder = (
|
const reorder = (
|
||||||
list: IProxyConfig[],
|
list: IProxyConfig[],
|
||||||
startIndex: number,
|
startIndex: number,
|
||||||
endIndex: number
|
endIndex: number,
|
||||||
) => {
|
) => {
|
||||||
const result = Array.from(list);
|
const result = Array.from(list);
|
||||||
const [removed] = result.splice(startIndex, 1);
|
const [removed] = result.splice(startIndex, 1);
|
||||||
@@ -208,8 +208,8 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
setCurrData(
|
setCurrData(
|
||||||
yaml.dump(
|
yaml.dump(
|
||||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||||
{ forceQuotes: true }
|
{ forceQuotes: true },
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 防止异常导致UI卡死
|
// 防止异常导致UI卡死
|
||||||
@@ -232,11 +232,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await saveProfileFile(property, currData);
|
await saveProfileFile(property, currData);
|
||||||
showNotice('success', t("Saved Successfully"));
|
showNotice("success", t("Saved Successfully"));
|
||||||
onSave?.(prevData, currData);
|
onSave?.(prevData, currData);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice('error', err.toString());
|
showNotice("error", err.toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,8 +358,8 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
setPrependSeq(
|
setPrependSeq(
|
||||||
prependSeq.filter(
|
prependSeq.filter(
|
||||||
(v) => v.name !== item.name
|
(v) => v.name !== item.name,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -385,8 +385,8 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
) {
|
) {
|
||||||
setDeleteSeq(
|
setDeleteSeq(
|
||||||
deleteSeq.filter(
|
deleteSeq.filter(
|
||||||
(v) => v !== filteredProxyList[newIndex].name
|
(v) => v !== filteredProxyList[newIndex].name,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setDeleteSeq((prev) => [
|
setDeleteSeq((prev) => [
|
||||||
@@ -418,8 +418,8 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
setAppendSeq(
|
setAppendSeq(
|
||||||
appendSeq.filter(
|
appendSeq.filter(
|
||||||
(v) => v.name !== item.name
|
(v) => v.name !== item.name,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -453,8 +453,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
padding: {
|
padding: {
|
||||||
top: 33, // 顶部padding防止遮挡snippets
|
top: 33, // 顶部padding防止遮挡snippets
|
||||||
},
|
},
|
||||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
|
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
||||||
}`,
|
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||||
|
}`,
|
||||||
fontLigatures: false, // 连字符
|
fontLigatures: false, // 连字符
|
||||||
smoothScrolling: true, // 平滑滚动
|
smoothScrolling: true, // 平滑滚动
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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, // 平滑滚动
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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 />}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Box,
|
Box,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
alpha,
|
alpha,
|
||||||
styled,
|
styled,
|
||||||
useTheme
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
@@ -54,74 +54,80 @@ export const ProviderButton = () => {
|
|||||||
|
|
||||||
// 检查是否有提供者
|
// 检查是否有提供者
|
||||||
const hasProviders = Object.keys(ruleProviders || {}).length > 0;
|
const hasProviders = Object.keys(ruleProviders || {}).length > 0;
|
||||||
|
|
||||||
// 更新单个规则提供者
|
// 更新单个规则提供者
|
||||||
const updateProvider = useLockFn(async (name: string) => {
|
const updateProvider = useLockFn(async (name: string) => {
|
||||||
try {
|
try {
|
||||||
// 设置更新状态
|
// 设置更新状态
|
||||||
setUpdating(prev => ({ ...prev, [name]: true }));
|
setUpdating((prev) => ({ ...prev, [name]: true }));
|
||||||
|
|
||||||
await ruleProviderUpdate(name);
|
await ruleProviderUpdate(name);
|
||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
await refreshRules();
|
await refreshRules();
|
||||||
await refreshRuleProviders();
|
await refreshRuleProviders();
|
||||||
|
|
||||||
showNotice('success', `${name} 更新成功`);
|
showNotice("success", `${name} 更新成功`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`);
|
showNotice(
|
||||||
|
"error",
|
||||||
|
`${name} 更新失败: ${err?.message || err.toString()}`,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// 清除更新状态
|
// 清除更新状态
|
||||||
setUpdating(prev => ({ ...prev, [name]: false }));
|
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新所有规则提供者
|
// 更新所有规则提供者
|
||||||
const updateAllProviders = useLockFn(async () => {
|
const updateAllProviders = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
// 获取所有provider的名称
|
// 获取所有provider的名称
|
||||||
const allProviders = Object.keys(ruleProviders || {});
|
const allProviders = Object.keys(ruleProviders || {});
|
||||||
if (allProviders.length === 0) {
|
if (allProviders.length === 0) {
|
||||||
showNotice('info', "没有可更新的规则提供者");
|
showNotice("info", "没有可更新的规则提供者");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置所有provider为更新中状态
|
// 设置所有provider为更新中状态
|
||||||
const newUpdating = allProviders.reduce((acc, key) => {
|
const newUpdating = allProviders.reduce(
|
||||||
acc[key] = true;
|
(acc, key) => {
|
||||||
return acc;
|
acc[key] = true;
|
||||||
}, {} as Record<string, boolean>);
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, boolean>,
|
||||||
|
);
|
||||||
setUpdating(newUpdating);
|
setUpdating(newUpdating);
|
||||||
|
|
||||||
// 改为串行逐个更新所有provider
|
// 改为串行逐个更新所有provider
|
||||||
for (const name of allProviders) {
|
for (const name of allProviders) {
|
||||||
try {
|
try {
|
||||||
await ruleProviderUpdate(name);
|
await ruleProviderUpdate(name);
|
||||||
// 每个更新完成后更新状态
|
// 每个更新完成后更新状态
|
||||||
setUpdating(prev => ({ ...prev, [name]: false }));
|
setUpdating((prev) => ({ ...prev, [name]: false }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`更新 ${name} 失败`, err);
|
console.error(`更新 ${name} 失败`, err);
|
||||||
// 继续执行下一个,不中断整体流程
|
// 继续执行下一个,不中断整体流程
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
await refreshRules();
|
await refreshRules();
|
||||||
await refreshRuleProviders();
|
await refreshRuleProviders();
|
||||||
|
|
||||||
showNotice('success', "全部规则提供者更新成功");
|
showNotice("success", "全部规则提供者更新成功");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice('error', `更新失败: ${err?.message || err.toString()}`);
|
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
|
||||||
} finally {
|
} finally {
|
||||||
// 清除所有更新状态
|
// 清除所有更新状态
|
||||||
setUpdating({});
|
setUpdating({});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!hasProviders) return null;
|
if (!hasProviders) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -134,15 +140,14 @@ export const ProviderButton = () => {
|
|||||||
>
|
>
|
||||||
{t("Rule Provider")}
|
{t("Rule Provider")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog
|
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||||
open={open}
|
|
||||||
onClose={handleClose}
|
|
||||||
maxWidth="sm"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
<Typography variant="h6">{t("Rule Providers")}</Typography>
|
<Typography variant="h6">{t("Rule Providers")}</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -153,49 +158,52 @@ export const ProviderButton = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<List sx={{ py: 0, minHeight: 250 }}>
|
<List sx={{ py: 0, minHeight: 250 }}>
|
||||||
{Object.entries(ruleProviders || {}).map(([key, item]) => {
|
{Object.entries(ruleProviders || {}).map(([key, item]) => {
|
||||||
const provider = item as RuleProviderItem;
|
const provider = item as RuleProviderItem;
|
||||||
const time = dayjs(provider.updatedAt);
|
const time = dayjs(provider.updatedAt);
|
||||||
const isUpdating = updating[key];
|
const isUpdating = updating[key];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={key}
|
key={key}
|
||||||
sx={[
|
sx={[
|
||||||
{
|
{
|
||||||
p: 0,
|
p: 0,
|
||||||
mb: "8px",
|
mb: "8px",
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
transition: "all 0.2s"
|
transition: "all 0.2s",
|
||||||
},
|
},
|
||||||
({ palette: { mode, primary } }) => {
|
({ palette: { mode, primary } }) => {
|
||||||
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
||||||
const hoverColor = mode === "light"
|
const hoverColor =
|
||||||
? alpha(primary.main, 0.1)
|
mode === "light"
|
||||||
: alpha(primary.main, 0.2);
|
? alpha(primary.main, 0.1)
|
||||||
|
: alpha(primary.main, 0.2);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: bgcolor,
|
backgroundColor: bgcolor,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: hoverColor,
|
backgroundColor: hoverColor,
|
||||||
borderColor: alpha(primary.main, 0.3)
|
borderColor: alpha(primary.main, 0.3),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
sx={{ px: 2, py: 1 }}
|
sx={{ px: 2, py: 1 }}
|
||||||
primary={
|
primary={
|
||||||
<Box sx={{
|
<Box
|
||||||
display: "flex",
|
sx={{
|
||||||
justifyContent: "space-between",
|
display: "flex",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
}}>
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
component="div"
|
component="div"
|
||||||
@@ -203,14 +211,19 @@ export const ProviderButton = () => {
|
|||||||
title={key}
|
title={key}
|
||||||
sx={{ display: "flex", alignItems: "center" }}
|
sx={{ display: "flex", alignItems: "center" }}
|
||||||
>
|
>
|
||||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||||
<TypeBox component="span">
|
<TypeBox component="span">
|
||||||
{provider.ruleCount}
|
{provider.ruleCount}
|
||||||
</TypeBox>
|
</TypeBox>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" noWrap>
|
<Typography
|
||||||
<small>{t("Update At")}: </small>{time.fromNow()}
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
<small>{t("Update At")}: </small>
|
||||||
|
{time.fromNow()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
@@ -219,30 +232,32 @@ export const ProviderButton = () => {
|
|||||||
<TypeBox component="span">
|
<TypeBox component="span">
|
||||||
{provider.vehicleType}
|
{provider.vehicleType}
|
||||||
</TypeBox>
|
</TypeBox>
|
||||||
<TypeBox component="span">
|
<TypeBox component="span">{provider.behavior}</TypeBox>
|
||||||
{provider.behavior}
|
|
||||||
</TypeBox>
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
<Box sx={{
|
<Box
|
||||||
width: 40,
|
sx={{
|
||||||
display: "flex",
|
width: 40,
|
||||||
justifyContent: "center",
|
display: "flex",
|
||||||
alignItems: "center"
|
justifyContent: "center",
|
||||||
}}>
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => updateProvider(key)}
|
onClick={() => updateProvider(key)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
sx={{
|
sx={{
|
||||||
animation: isUpdating ? "spin 1s linear infinite" : "none",
|
animation: isUpdating
|
||||||
|
? "spin 1s linear infinite"
|
||||||
|
: "none",
|
||||||
"@keyframes spin": {
|
"@keyframes spin": {
|
||||||
"0%": { transform: "rotate(0deg)" },
|
"0%": { transform: "rotate(0deg)" },
|
||||||
"100%": { transform: "rotate(360deg)" }
|
"100%": { transform: "rotate(360deg)" },
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
title={t("Update Provider") as string}
|
title={t("Update Provider") as string}
|
||||||
>
|
>
|
||||||
@@ -254,7 +269,7 @@ export const ProviderButton = () => {
|
|||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} variant="outlined">
|
<Button onClick={handleClose} variant="outlined">
|
||||||
{t("Close")}
|
{t("Close")}
|
||||||
|
|||||||
@@ -82,21 +82,21 @@ export const BackupConfigViewer = memo(
|
|||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
urlRef.current?.focus();
|
urlRef.current?.focus();
|
||||||
showNotice('error', t("WebDAV URL Required"));
|
showNotice("error", t("WebDAV URL Required"));
|
||||||
throw new Error(t("WebDAV URL Required"));
|
throw new Error(t("WebDAV URL Required"));
|
||||||
} else if (!isValidUrl(url)) {
|
} else if (!isValidUrl(url)) {
|
||||||
urlRef.current?.focus();
|
urlRef.current?.focus();
|
||||||
showNotice('error', t("Invalid WebDAV URL"));
|
showNotice("error", t("Invalid WebDAV URL"));
|
||||||
throw new Error(t("Invalid WebDAV URL"));
|
throw new Error(t("Invalid WebDAV URL"));
|
||||||
}
|
}
|
||||||
if (!username) {
|
if (!username) {
|
||||||
usernameRef.current?.focus();
|
usernameRef.current?.focus();
|
||||||
showNotice('error', t("WebDAV URL Required"));
|
showNotice("error", t("WebDAV URL Required"));
|
||||||
throw new Error(t("Username Required"));
|
throw new Error(t("Username Required"));
|
||||||
}
|
}
|
||||||
if (!password) {
|
if (!password) {
|
||||||
passwordRef.current?.focus();
|
passwordRef.current?.focus();
|
||||||
showNotice('error', t("WebDAV URL Required"));
|
showNotice("error", t("WebDAV URL Required"));
|
||||||
throw new Error(t("Password Required"));
|
throw new Error(t("Password Required"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -110,11 +110,11 @@ export const BackupConfigViewer = memo(
|
|||||||
data.username.trim(),
|
data.username.trim(),
|
||||||
data.password,
|
data.password,
|
||||||
).then(() => {
|
).then(() => {
|
||||||
showNotice('success', t("WebDAV Config Saved"));
|
showNotice("success", t("WebDAV Config Saved"));
|
||||||
onSaveSuccess();
|
onSaveSuccess();
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice('error', t("WebDAV Config Save Failed", { error }), 3000);
|
showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -125,11 +125,11 @@ export const BackupConfigViewer = memo(
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await createWebdavBackup().then(async () => {
|
await createWebdavBackup().then(async () => {
|
||||||
showNotice('success', t("Backup Created"));
|
showNotice("success", t("Backup Created"));
|
||||||
await onBackupSuccess();
|
await onBackupSuccess();
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice('error', t("Backup Failed", { error }));
|
showNotice("error", t("Backup Failed", { error }));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const BackupTableViewer = memo(
|
|||||||
|
|
||||||
const handleRestore = useLockFn(async (filename: string) => {
|
const handleRestore = useLockFn(async (filename: string) => {
|
||||||
await restoreWebDavBackup(filename).then(() => {
|
await restoreWebDavBackup(filename).then(() => {
|
||||||
showNotice('success', t("Restore Success, App will restart in 1s"));
|
showNotice("success", t("Restore Success, App will restart in 1s"));
|
||||||
});
|
});
|
||||||
await restartApp();
|
await restartApp();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
const errorMsg = await changeClashCore(core);
|
const errorMsg = await changeClashCore(core);
|
||||||
|
|
||||||
if (errorMsg) {
|
if (errorMsg) {
|
||||||
showNotice('error', errorMsg);
|
showNotice("error", errorMsg);
|
||||||
setChangingCore(null);
|
setChangingCore(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setChangingCore(null);
|
setChangingCore(null);
|
||||||
showNotice('error', err.message || err.toString());
|
showNotice("error", err.message || err.toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,11 +73,11 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
try {
|
try {
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
await restartCore();
|
await restartCore();
|
||||||
showNotice('success', t(`Clash Core Restarted`));
|
showNotice("success", t(`Clash Core Restarted`));
|
||||||
setRestarting(false);
|
setRestarting(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setRestarting(false);
|
setRestarting(false);
|
||||||
showNotice('error', err.message || err.toString());
|
showNotice("error", err.message || err.toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,14 +86,14 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
setUpgrading(true);
|
setUpgrading(true);
|
||||||
await upgradeCore();
|
await upgradeCore();
|
||||||
setUpgrading(false);
|
setUpgrading(false);
|
||||||
showNotice('success', t(`Core Version Updated`));
|
showNotice("success", t(`Core Version Updated`));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setUpgrading(false);
|
setUpgrading(false);
|
||||||
const errMsg = err.response?.data?.message || err.toString();
|
const errMsg = err.response?.data?.message || err.toString();
|
||||||
const showMsg = errMsg.includes("already using latest version")
|
const showMsg = errMsg.includes("already using latest version")
|
||||||
? "Already Using Latest Core Version"
|
? "Already Using Latest Core Version"
|
||||||
: errMsg;
|
: errMsg;
|
||||||
showNotice('error', t(showMsg));
|
showNotice("error", t(showMsg));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Stack,
|
Stack,
|
||||||
TextField
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useLockFn, useRequest } from "ahooks";
|
import { useLockFn, useRequest } from "ahooks";
|
||||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||||
@@ -26,127 +26,136 @@ interface ClashPortViewerRef {
|
|||||||
close: () => void;
|
close: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
|
const generateRandomPort = () =>
|
||||||
|
Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
|
||||||
|
|
||||||
export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerProps>(
|
export const ClashPortViewer = forwardRef<
|
||||||
(props, ref) => {
|
ClashPortViewerRef,
|
||||||
const { t } = useTranslation();
|
ClashPortViewerProps
|
||||||
const { clashInfo, patchInfo } = useClashInfo();
|
>((props, ref) => {
|
||||||
const { verge, patchVerge } = useVerge();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const { clashInfo, patchInfo } = useClashInfo();
|
||||||
|
const { verge, patchVerge } = useVerge();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
// Mixed Port
|
// Mixed Port
|
||||||
const [mixedPort, setMixedPort] = useState(
|
const [mixedPort, setMixedPort] = useState(
|
||||||
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
|
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 其他端口状态
|
// 其他端口状态
|
||||||
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
|
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
|
||||||
const [socksEnabled, setSocksEnabled] = useState(verge?.verge_socks_enabled ?? false);
|
const [socksEnabled, setSocksEnabled] = useState(
|
||||||
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
|
verge?.verge_socks_enabled ?? false,
|
||||||
const [httpEnabled, setHttpEnabled] = useState(verge?.verge_http_enabled ?? false);
|
);
|
||||||
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895);
|
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
|
||||||
const [redirEnabled, setRedirEnabled] = useState(verge?.verge_redir_enabled ?? false);
|
const [httpEnabled, setHttpEnabled] = useState(
|
||||||
const [tproxyPort, setTproxyPort] = useState(verge?.verge_tproxy_port ?? 7896);
|
verge?.verge_http_enabled ?? false,
|
||||||
const [tproxyEnabled, setTproxyEnabled] = useState(verge?.verge_tproxy_enabled ?? false);
|
);
|
||||||
|
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895);
|
||||||
|
const [redirEnabled, setRedirEnabled] = useState(
|
||||||
|
verge?.verge_redir_enabled ?? false,
|
||||||
|
);
|
||||||
|
const [tproxyPort, setTproxyPort] = useState(
|
||||||
|
verge?.verge_tproxy_port ?? 7896,
|
||||||
|
);
|
||||||
|
const [tproxyEnabled, setTproxyEnabled] = useState(
|
||||||
|
verge?.verge_tproxy_enabled ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
// 添加保存请求,防止GUI卡死
|
// 添加保存请求,防止GUI卡死
|
||||||
const { loading, run: saveSettings } = useRequest(
|
const { loading, run: saveSettings } = useRequest(
|
||||||
async (params: {
|
async (params: { clashConfig: any; vergeConfig: any }) => {
|
||||||
clashConfig: any;
|
const { clashConfig, vergeConfig } = params;
|
||||||
vergeConfig: any;
|
await Promise.all([patchInfo(clashConfig), patchVerge(vergeConfig)]);
|
||||||
}) => {
|
},
|
||||||
const { clashConfig, vergeConfig } = params;
|
{
|
||||||
await Promise.all([
|
manual: true,
|
||||||
patchInfo(clashConfig),
|
onSuccess: () => {
|
||||||
patchVerge(vergeConfig)
|
setOpen(false);
|
||||||
]);
|
showNotice("success", t("Port settings saved")); // 调用提示函数
|
||||||
},
|
},
|
||||||
{
|
onError: () => {
|
||||||
manual: true,
|
showNotice("error", t("Failed to save settings")); // 调用提示函数
|
||||||
onSuccess: () => {
|
|
||||||
setOpen(false);
|
|
||||||
showNotice("success", t("Port settings saved")); // 调用提示函数
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
showNotice("error", t("Failed to save settings")); // 调用提示函数
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
open: () => {
|
|
||||||
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
|
|
||||||
setSocksPort(verge?.verge_socks_port ?? 7898);
|
|
||||||
setSocksEnabled(verge?.verge_socks_enabled ?? false);
|
|
||||||
setHttpPort(verge?.verge_port ?? 7899);
|
|
||||||
setHttpEnabled(verge?.verge_http_enabled ?? false);
|
|
||||||
setRedirPort(verge?.verge_redir_port ?? 7895);
|
|
||||||
setRedirEnabled(verge?.verge_redir_enabled ?? false);
|
|
||||||
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
|
|
||||||
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
|
|
||||||
setOpen( true);
|
|
||||||
},
|
},
|
||||||
close: () => setOpen(false),
|
},
|
||||||
}));
|
);
|
||||||
|
|
||||||
const onSave = useLockFn(async () => {
|
useImperativeHandle(ref, () => ({
|
||||||
// 端口冲突检测
|
open: () => {
|
||||||
const portList = [
|
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
|
||||||
mixedPort,
|
setSocksPort(verge?.verge_socks_port ?? 7898);
|
||||||
socksEnabled ? socksPort : -1,
|
setSocksEnabled(verge?.verge_socks_enabled ?? false);
|
||||||
httpEnabled ? httpPort : -1,
|
setHttpPort(verge?.verge_port ?? 7899);
|
||||||
redirEnabled ? redirPort : -1,
|
setHttpEnabled(verge?.verge_http_enabled ?? false);
|
||||||
tproxyEnabled ? tproxyPort : -1
|
setRedirPort(verge?.verge_redir_port ?? 7895);
|
||||||
].filter(p => p !== -1);
|
setRedirEnabled(verge?.verge_redir_enabled ?? false);
|
||||||
|
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
|
||||||
|
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
|
||||||
|
setOpen(true);
|
||||||
|
},
|
||||||
|
close: () => setOpen(false),
|
||||||
|
}));
|
||||||
|
|
||||||
if (new Set(portList).size !== portList.length) {
|
const onSave = useLockFn(async () => {
|
||||||
return;
|
// 端口冲突检测
|
||||||
}
|
const portList = [
|
||||||
|
mixedPort,
|
||||||
|
socksEnabled ? socksPort : -1,
|
||||||
|
httpEnabled ? httpPort : -1,
|
||||||
|
redirEnabled ? redirPort : -1,
|
||||||
|
tproxyEnabled ? tproxyPort : -1,
|
||||||
|
].filter((p) => p !== -1);
|
||||||
|
|
||||||
// 验证端口范围
|
if (new Set(portList).size !== portList.length) {
|
||||||
const isValidPort = (port: number) => port >= 1 && port <= 65535;
|
return;
|
||||||
const allPortsValid = [
|
}
|
||||||
mixedPort,
|
|
||||||
socksEnabled ? socksPort : 0,
|
|
||||||
httpEnabled ? httpPort : 0,
|
|
||||||
redirEnabled ? redirPort : 0,
|
|
||||||
tproxyEnabled ? tproxyPort : 0
|
|
||||||
].every(port => port === 0 || isValidPort(port));
|
|
||||||
|
|
||||||
if (!allPortsValid) {
|
// 验证端口范围
|
||||||
return;
|
const isValidPort = (port: number) => port >= 1 && port <= 65535;
|
||||||
}
|
const allPortsValid = [
|
||||||
|
mixedPort,
|
||||||
|
socksEnabled ? socksPort : 0,
|
||||||
|
httpEnabled ? httpPort : 0,
|
||||||
|
redirEnabled ? redirPort : 0,
|
||||||
|
tproxyEnabled ? tproxyPort : 0,
|
||||||
|
].every((port) => port === 0 || isValidPort(port));
|
||||||
|
|
||||||
// 准备配置数据
|
if (!allPortsValid) {
|
||||||
const clashConfig = {
|
return;
|
||||||
"mixed-port": mixedPort,
|
}
|
||||||
"socks-port": socksPort,
|
|
||||||
port: httpPort,
|
|
||||||
"redir-port": redirPort,
|
|
||||||
"tproxy-port": tproxyPort
|
|
||||||
};
|
|
||||||
|
|
||||||
const vergeConfig = {
|
// 准备配置数据
|
||||||
verge_mixed_port: mixedPort,
|
const clashConfig = {
|
||||||
verge_socks_port: socksPort,
|
"mixed-port": mixedPort,
|
||||||
verge_socks_enabled: socksEnabled,
|
"socks-port": socksPort,
|
||||||
verge_port: httpPort,
|
port: httpPort,
|
||||||
verge_http_enabled: httpEnabled,
|
"redir-port": redirPort,
|
||||||
verge_redir_port: redirPort,
|
"tproxy-port": tproxyPort,
|
||||||
verge_redir_enabled: redirEnabled,
|
};
|
||||||
verge_tproxy_port: tproxyPort,
|
|
||||||
verge_tproxy_enabled: tproxyEnabled
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提交保存请求
|
const vergeConfig = {
|
||||||
await saveSettings({ clashConfig, vergeConfig });
|
verge_mixed_port: mixedPort,
|
||||||
});
|
verge_socks_port: socksPort,
|
||||||
|
verge_socks_enabled: socksEnabled,
|
||||||
|
verge_port: httpPort,
|
||||||
|
verge_http_enabled: httpEnabled,
|
||||||
|
verge_redir_port: redirPort,
|
||||||
|
verge_redir_enabled: redirEnabled,
|
||||||
|
verge_tproxy_port: tproxyPort,
|
||||||
|
verge_tproxy_enabled: tproxyEnabled,
|
||||||
|
};
|
||||||
|
|
||||||
// 优化的数字输入处理
|
// 提交保存请求
|
||||||
const handleNumericChange = (setter: (value: number) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
await saveSettings({ clashConfig, vergeConfig });
|
||||||
const value = e.target.value.replace(/\D+/, '');
|
});
|
||||||
if (value === '') {
|
|
||||||
|
// 优化的数字输入处理
|
||||||
|
const handleNumericChange =
|
||||||
|
(setter: (value: number) => void) =>
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value.replace(/\D+/, "");
|
||||||
|
if (value === "") {
|
||||||
setter(0);
|
setter(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -157,190 +166,201 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerPro
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("Port Configuration")}
|
title={t("Port Configuration")}
|
||||||
contentSx={{
|
contentSx={{
|
||||||
width: 400
|
width: 400,
|
||||||
}}
|
}}
|
||||||
okBtn={
|
okBtn={
|
||||||
loading ? (
|
loading ? (
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<CircularProgress size={20} />
|
<CircularProgress size={20} />
|
||||||
{t("Saving...")}
|
{t("Saving...")}
|
||||||
</Stack>
|
</Stack>
|
||||||
) : t("Save")
|
) : (
|
||||||
}
|
t("Save")
|
||||||
cancelBtn={t("Cancel")}
|
)
|
||||||
onClose={() => setOpen(false)}
|
}
|
||||||
onCancel={() => setOpen(false)}
|
cancelBtn={t("Cancel")}
|
||||||
onOk={onSave}
|
onClose={() => setOpen(false)}
|
||||||
>
|
onCancel={() => setOpen(false)}
|
||||||
<List sx={{ width: "100%" }}>
|
onOk={onSave}
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
>
|
||||||
<ListItemText
|
<List sx={{ width: "100%" }}>
|
||||||
primary={t("Mixed Port")}
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
primaryTypographyProps={{ fontSize: 12 }}
|
<ListItemText
|
||||||
|
primary={t("Mixed Port")}
|
||||||
|
primaryTypographyProps={{ fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||||||
|
value={mixedPort}
|
||||||
|
onChange={(e) =>
|
||||||
|
setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||||||
|
}
|
||||||
|
inputProps={{ style: { fontSize: 12 } }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<IconButton
|
||||||
<TextField
|
size="small"
|
||||||
size="small"
|
onClick={() => setMixedPort(generateRandomPort())}
|
||||||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
title={t("Random Port")}
|
||||||
value={mixedPort}
|
sx={{ mr: 0.5 }}
|
||||||
onChange={(e) => setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
|
>
|
||||||
inputProps={{ style: { fontSize: 12 } }}
|
<Shuffle fontSize="small" />
|
||||||
/>
|
</IconButton>
|
||||||
<IconButton
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setMixedPort(generateRandomPort())}
|
checked={true}
|
||||||
title={t("Random Port")}
|
disabled={true}
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ ml: 0.5, opacity: 0.7 }}
|
||||||
>
|
/>
|
||||||
<Shuffle fontSize="small" />
|
</div>
|
||||||
</IconButton>
|
</ListItem>
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
checked={true}
|
|
||||||
disabled={true}
|
|
||||||
sx={{ ml: 0.5, opacity: 0.7 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary={t("Socks Port")}
|
||||||
|
primaryTypographyProps={{ fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||||||
|
value={socksPort}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||||||
|
}
|
||||||
|
disabled={!socksEnabled}
|
||||||
|
inputProps={{ style: { fontSize: 12 } }}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => setSocksPort(generateRandomPort())}
|
||||||
|
title={t("Random Port")}
|
||||||
|
disabled={!socksEnabled}
|
||||||
|
sx={{ mr: 0.5 }}
|
||||||
|
>
|
||||||
|
<Shuffle fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={socksEnabled}
|
||||||
|
onChange={(_, c) => setSocksEnabled(c)}
|
||||||
|
sx={{ ml: 0.5 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary={t("HTTP Port")}
|
||||||
|
primaryTypographyProps={{ fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||||||
|
value={httpPort}
|
||||||
|
onChange={(e) =>
|
||||||
|
setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||||||
|
}
|
||||||
|
disabled={!httpEnabled}
|
||||||
|
inputProps={{ style: { fontSize: 12 } }}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => setHttpPort(generateRandomPort())}
|
||||||
|
title={t("Random Port")}
|
||||||
|
disabled={!httpEnabled}
|
||||||
|
sx={{ mr: 0.5 }}
|
||||||
|
>
|
||||||
|
<Shuffle fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={httpEnabled}
|
||||||
|
onChange={(_, c) => setHttpEnabled(c)}
|
||||||
|
sx={{ ml: 0.5 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
{OS !== "windows" && (
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("Socks Port")}
|
primary={t("Redir Port")}
|
||||||
primaryTypographyProps={{ fontSize: 12 }}
|
primaryTypographyProps={{ fontSize: 12 }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||||||
value={socksPort}
|
value={redirPort}
|
||||||
onChange={(e) => setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
|
onChange={(e) =>
|
||||||
disabled={!socksEnabled}
|
setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||||||
|
}
|
||||||
|
disabled={!redirEnabled}
|
||||||
inputProps={{ style: { fontSize: 12 } }}
|
inputProps={{ style: { fontSize: 12 } }}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setSocksPort(generateRandomPort())}
|
onClick={() => setRedirPort(generateRandomPort())}
|
||||||
title={t("Random Port")}
|
title={t("Random Port")}
|
||||||
disabled={!socksEnabled}
|
disabled={!redirEnabled}
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ mr: 0.5 }}
|
||||||
>
|
>
|
||||||
<Shuffle fontSize="small" />
|
<Shuffle fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={socksEnabled}
|
checked={redirEnabled}
|
||||||
onChange={(_, c) => setSocksEnabled(c)}
|
onChange={(_, c) => setRedirEnabled(c)}
|
||||||
sx={{ ml: 0.5 }}
|
sx={{ ml: 0.5 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{OS === "linux" && (
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t("HTTP Port")}
|
primary={t("Tproxy Port")}
|
||||||
primaryTypographyProps={{ fontSize: 12 }}
|
primaryTypographyProps={{ fontSize: 12 }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
||||||
value={httpPort}
|
value={tproxyPort}
|
||||||
onChange={(e) => setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
|
onChange={(e) =>
|
||||||
disabled={!httpEnabled}
|
setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||||||
|
}
|
||||||
|
disabled={!tproxyEnabled}
|
||||||
inputProps={{ style: { fontSize: 12 } }}
|
inputProps={{ style: { fontSize: 12 } }}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setHttpPort(generateRandomPort())}
|
onClick={() => setTproxyPort(generateRandomPort())}
|
||||||
title={t("Random Port")}
|
title={t("Random Port")}
|
||||||
disabled={!httpEnabled}
|
disabled={!tproxyEnabled}
|
||||||
sx={{ mr: 0.5 }}
|
sx={{ mr: 0.5 }}
|
||||||
>
|
>
|
||||||
<Shuffle fontSize="small" />
|
<Shuffle fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={httpEnabled}
|
checked={tproxyEnabled}
|
||||||
onChange={(_, c) => setHttpEnabled(c)}
|
onChange={(_, c) => setTproxyEnabled(c)}
|
||||||
sx={{ ml: 0.5 }}
|
sx={{ ml: 0.5 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
)}
|
||||||
{OS !== "windows" && (
|
</List>
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
</BaseDialog>
|
||||||
<ListItemText
|
);
|
||||||
primary={t("Redir Port")}
|
});
|
||||||
primaryTypographyProps={{ fontSize: 12 }}
|
|
||||||
/>
|
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
|
||||||
value={redirPort}
|
|
||||||
onChange={(e) => setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
|
|
||||||
disabled={!redirEnabled}
|
|
||||||
inputProps={{ style: { fontSize: 12 } }}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() => setRedirPort(generateRandomPort())}
|
|
||||||
title={t("Random Port")}
|
|
||||||
disabled={!redirEnabled}
|
|
||||||
sx={{ mr: 0.5 }}
|
|
||||||
>
|
|
||||||
<Shuffle fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
checked={redirEnabled}
|
|
||||||
onChange={(_, c) => setRedirEnabled(c)}
|
|
||||||
sx={{ ml: 0.5 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{OS === "linux" && (
|
|
||||||
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
|
|
||||||
<ListItemText
|
|
||||||
primary={t("Tproxy Port")}
|
|
||||||
primaryTypographyProps={{ fontSize: 12 }}
|
|
||||||
/>
|
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
|
|
||||||
value={tproxyPort}
|
|
||||||
onChange={(e) => setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
|
|
||||||
disabled={!tproxyEnabled}
|
|
||||||
inputProps={{ style: { fontSize: 12 } }}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() => setTproxyPort(generateRandomPort())}
|
|
||||||
title={t("Random Port")}
|
|
||||||
disabled={!tproxyEnabled}
|
|
||||||
sx={{ mr: 0.5 }}
|
|
||||||
>
|
|
||||||
<Shuffle fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
checked={tproxyEnabled}
|
|
||||||
onChange={(_, c) => setTproxyEnabled(c)}
|
|
||||||
sx={{ ml: 0.5 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</BaseDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
@@ -42,58 +42,72 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
// 保存配置
|
// 保存配置
|
||||||
const onSave = useLockFn(async () => {
|
const onSave = useLockFn(async () => {
|
||||||
if (!controller.trim()) {
|
if (!controller.trim()) {
|
||||||
showNotice('error', t("Controller address cannot be empty"), 3000);
|
showNotice("error", t("Controller address cannot be empty"), 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!secret.trim()) {
|
if (!secret.trim()) {
|
||||||
showNotice('error', t("Secret cannot be empty"), 3000);
|
showNotice("error", t("Secret cannot be empty"), 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
await patchInfo({ "external-controller": controller, secret });
|
await patchInfo({ "external-controller": controller, secret });
|
||||||
showNotice('success', t("Configuration saved successfully"), 2000);
|
showNotice("success", t("Configuration saved successfully"), 2000);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice('error', err.message || t("Failed to save configuration"), 4000);
|
showNotice(
|
||||||
|
"error",
|
||||||
|
err.message || t("Failed to save configuration"),
|
||||||
|
4000,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 复制到剪贴板
|
// 复制到剪贴板
|
||||||
const handleCopyToClipboard = useLockFn(async (text: string, type: string) => {
|
const handleCopyToClipboard = useLockFn(
|
||||||
try {
|
async (text: string, type: string) => {
|
||||||
await navigator.clipboard.writeText(text);
|
try {
|
||||||
setCopySuccess(type);
|
await navigator.clipboard.writeText(text);
|
||||||
setTimeout(() => setCopySuccess(null), 2000);
|
setCopySuccess(type);
|
||||||
} catch (err) {
|
setTimeout(() => setCopySuccess(null), 2000);
|
||||||
showNotice('error', t("Failed to copy"), 2000);
|
} catch (err) {
|
||||||
}
|
showNotice("error", t("Failed to copy"), 2000);
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("External Controller")}
|
title={t("External Controller")}
|
||||||
contentSx={{ width: 400 }}
|
contentSx={{ width: 400 }}
|
||||||
okBtn={isSaving ? (
|
okBtn={
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
isSaving ? (
|
||||||
<CircularProgress size={16} color="inherit" />
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
{t("Saving...")}
|
<CircularProgress size={16} color="inherit" />
|
||||||
</Box>
|
{t("Saving...")}
|
||||||
) : (
|
</Box>
|
||||||
t("Save")
|
) : (
|
||||||
)}
|
t("Save")
|
||||||
|
)
|
||||||
|
}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("Cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}>
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
padding: "5px 2px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ListItemText primary={t("External Controller")} />
|
<ListItemText primary={t("External Controller")} />
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -101,11 +115,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
sx={{
|
sx={{
|
||||||
width: 175,
|
width: 175,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
pointerEvents: 'auto'
|
pointerEvents: "auto",
|
||||||
}}
|
}}
|
||||||
value={controller}
|
value={controller}
|
||||||
placeholder="Required"
|
placeholder="Required"
|
||||||
onChange={e => setController(e.target.value)}
|
onChange={(e) => setController(e.target.value)}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
/>
|
/>
|
||||||
<Tooltip title={t("Copy to clipboard")}>
|
<Tooltip title={t("Copy to clipboard")}>
|
||||||
@@ -121,7 +135,13 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}>
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
padding: "5px 2px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ListItemText primary={t("Core Secret")} />
|
<ListItemText primary={t("Core Secret")} />
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -129,11 +149,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
sx={{
|
sx={{
|
||||||
width: 175,
|
width: 175,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
pointerEvents: 'auto'
|
pointerEvents: "auto",
|
||||||
}}
|
}}
|
||||||
value={secret}
|
value={secret}
|
||||||
placeholder={t("Recommended")}
|
placeholder={t("Recommended")}
|
||||||
onChange={e => setSecret(e.target.value)}
|
onChange={(e) => setSecret(e.target.value)}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
/>
|
/>
|
||||||
<Tooltip title={t("Copy to clipboard")}>
|
<Tooltip title={t("Copy to clipboard")}>
|
||||||
@@ -153,13 +173,12 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
<Snackbar
|
<Snackbar
|
||||||
open={copySuccess !== null}
|
open={copySuccess !== null}
|
||||||
autoHideDuration={2000}
|
autoHideDuration={2000}
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||||
>
|
>
|
||||||
<Alert severity="success">
|
<Alert severity="success">
|
||||||
{copySuccess === "controller"
|
{copySuccess === "controller"
|
||||||
? t("Controller address copied to clipboard")
|
? t("Controller address copied to clipboard")
|
||||||
: t("Secret copied to clipboard")
|
: t("Secret copied to clipboard")}
|
||||||
}
|
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|||||||
@@ -59,7 +59,13 @@ const DEFAULT_DNS_CONFIG = {
|
|||||||
"*.msftncsi.com",
|
"*.msftncsi.com",
|
||||||
"www.msftconnecttest.com",
|
"www.msftconnecttest.com",
|
||||||
],
|
],
|
||||||
"default-nameserver": ["system", "223.6.6.6", "8.8.8.8", "2400:3200::1", "2001:4860:4860::8888"],
|
"default-nameserver": [
|
||||||
|
"system",
|
||||||
|
"223.6.6.6",
|
||||||
|
"8.8.8.8",
|
||||||
|
"2400:3200::1",
|
||||||
|
"2001:4860:4860::8888",
|
||||||
|
],
|
||||||
nameserver: [
|
nameserver: [
|
||||||
"8.8.8.8",
|
"8.8.8.8",
|
||||||
"https://doh.pub/dns-query",
|
"https://doh.pub/dns-query",
|
||||||
@@ -70,7 +76,7 @@ const DEFAULT_DNS_CONFIG = {
|
|||||||
"proxy-server-nameserver": [
|
"proxy-server-nameserver": [
|
||||||
"https://doh.pub/dns-query",
|
"https://doh.pub/dns-query",
|
||||||
"https://dns.alidns.com/dns-query",
|
"https://dns.alidns.com/dns-query",
|
||||||
"tls://223.5.5.5"
|
"tls://223.5.5.5",
|
||||||
],
|
],
|
||||||
"direct-nameserver": [],
|
"direct-nameserver": [],
|
||||||
"direct-nameserver-follow-policy": false,
|
"direct-nameserver-follow-policy": false,
|
||||||
@@ -219,8 +225,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
|
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
|
||||||
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
|
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
|
||||||
useSystemHosts:
|
useSystemHosts:
|
||||||
dnsConfig["use-system-hosts"] ??
|
dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
|
||||||
DEFAULT_DNS_CONFIG["use-system-hosts"],
|
|
||||||
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
|
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
|
||||||
fakeIpFilter:
|
fakeIpFilter:
|
||||||
dnsConfig["fake-ip-filter"]?.join(", ") ??
|
dnsConfig["fake-ip-filter"]?.join(", ") ??
|
||||||
@@ -229,7 +234,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
dnsConfig.nameserver?.join(", ") ??
|
dnsConfig.nameserver?.join(", ") ??
|
||||||
DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
||||||
fallback:
|
fallback:
|
||||||
dnsConfig.fallback?.join(", ") ?? DEFAULT_DNS_CONFIG.fallback.join(", "),
|
dnsConfig.fallback?.join(", ") ??
|
||||||
|
DEFAULT_DNS_CONFIG.fallback.join(", "),
|
||||||
defaultNameserver:
|
defaultNameserver:
|
||||||
dnsConfig["default-nameserver"]?.join(", ") ??
|
dnsConfig["default-nameserver"]?.join(", ") ??
|
||||||
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
||||||
@@ -299,9 +305,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
// 从表单值更新YAML内容
|
// 从表单值更新YAML内容
|
||||||
const updateYamlFromValues = () => {
|
const updateYamlFromValues = () => {
|
||||||
|
|
||||||
const config: Record<string, any> = {};
|
const config: Record<string, any> = {};
|
||||||
|
|
||||||
const dnsConfig = generateDnsConfig();
|
const dnsConfig = generateDnsConfig();
|
||||||
if (Object.keys(dnsConfig).length > 0) {
|
if (Object.keys(dnsConfig).length > 0) {
|
||||||
config.dns = dnsConfig;
|
config.dns = dnsConfig;
|
||||||
@@ -311,7 +316,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
if (Object.keys(hosts).length > 0) {
|
if (Object.keys(hosts).length > 0) {
|
||||||
config.hosts = hosts;
|
config.hosts = hosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
setYamlContent(yaml.dump(config, { forceQuotes: true }));
|
setYamlContent(yaml.dump(config, { forceQuotes: true }));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -320,10 +325,10 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
try {
|
try {
|
||||||
const parsedYaml = yaml.load(yamlContent) as any;
|
const parsedYaml = yaml.load(yamlContent) as any;
|
||||||
if (!parsedYaml) return;
|
if (!parsedYaml) return;
|
||||||
|
|
||||||
updateValuesFromConfig(parsedYaml);
|
updateValuesFromConfig(parsedYaml);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice('error', t("Invalid YAML format"));
|
showNotice("error", t("Invalid YAML format"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -505,7 +510,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
if (Object.keys(dnsConfig).length > 0) {
|
if (Object.keys(dnsConfig).length > 0) {
|
||||||
config.dns = dnsConfig;
|
config.dns = dnsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hosts = parseHosts(values.hosts);
|
const hosts = parseHosts(values.hosts);
|
||||||
if (Object.keys(hosts).length > 0) {
|
if (Object.keys(hosts).length > 0) {
|
||||||
config.hosts = hosts;
|
config.hosts = hosts;
|
||||||
@@ -521,30 +526,41 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
await invoke("save_dns_config", { dnsConfig: config });
|
await invoke("save_dns_config", { dnsConfig: config });
|
||||||
|
|
||||||
// 验证配置
|
// 验证配置
|
||||||
const [isValid, errorMsg] = await invoke<[boolean, string]>("validate_dns_config", {});
|
const [isValid, errorMsg] = await invoke<[boolean, string]>(
|
||||||
|
"validate_dns_config",
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
let cleanErrorMsg = errorMsg;
|
let cleanErrorMsg = errorMsg;
|
||||||
|
|
||||||
// 提取关键错误信息
|
// 提取关键错误信息
|
||||||
if (errorMsg.includes("level=error")) {
|
if (errorMsg.includes("level=error")) {
|
||||||
const errorLines = errorMsg.split('\n').filter(line =>
|
const errorLines = errorMsg
|
||||||
line.includes("level=error") ||
|
.split("\n")
|
||||||
line.includes("level=fatal") ||
|
.filter(
|
||||||
line.includes("failed")
|
(line) =>
|
||||||
);
|
line.includes("level=error") ||
|
||||||
|
line.includes("level=fatal") ||
|
||||||
|
line.includes("failed"),
|
||||||
|
);
|
||||||
|
|
||||||
if (errorLines.length > 0) {
|
if (errorLines.length > 0) {
|
||||||
cleanErrorMsg = errorLines.map(line => {
|
cleanErrorMsg = errorLines
|
||||||
const msgMatch = line.match(/msg="([^"]+)"/);
|
.map((line) => {
|
||||||
return msgMatch ? msgMatch[1] : line;
|
const msgMatch = line.match(/msg="([^"]+)"/);
|
||||||
}).join(", ");
|
return msgMatch ? msgMatch[1] : line;
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotice('error', t("DNS configuration error") + ": " + cleanErrorMsg);
|
showNotice(
|
||||||
|
"error",
|
||||||
|
t("DNS configuration error") + ": " + cleanErrorMsg,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,9 +571,9 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
showNotice('success', t("DNS settings saved"));
|
showNotice("success", t("DNS settings saved"));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice('error', err.message || err.toString());
|
showNotice("error", err.message || err.toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice('error', err.toString());
|
showNotice("error", err.toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" }} />
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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" }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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")),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,4 +399,4 @@
|
|||||||
"View Profile-Merge": "프로필-병합 보기",
|
"View Profile-Merge": "프로필-병합 보기",
|
||||||
"Update Successful": "업데이트 성공",
|
"Update Successful": "업데이트 성공",
|
||||||
"Update Failed": "업데이트 실패"
|
"Update Failed": "업데이트 실패"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 端口和密鑰,點進去就可以隨機端口和密鑰"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 })}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -398,4 +398,4 @@ const ClashModeEnhancedCard = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HomePage;
|
export default HomePage;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user