mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad623da86d | ||
|
|
3ec7772e5d | ||
|
|
e6b57561e3 | ||
|
|
3b1c2c95ec | ||
|
|
364abafffe | ||
|
|
389281b96c | ||
|
|
fed64bcd08 | ||
|
|
17a5c4d094 | ||
|
|
f2317c7816 | ||
|
|
7fce7ca62f | ||
|
|
167bcb222d | ||
|
|
108a599666 | ||
|
|
57476741cd | ||
|
|
077f3e79f8 | ||
|
|
2c9aa4bca7 | ||
|
|
9cf0f1e0a7 | ||
|
|
660c03a564 | ||
|
|
1d49d79af2 | ||
|
|
4ccb17dde6 | ||
|
|
8bc433711d | ||
|
|
2e82bd8624 | ||
|
|
4b57d513a2 | ||
|
|
d36c3a83a9 | ||
|
|
ac3afe4dee | ||
|
|
667844aa12 | ||
|
|
6d192233d1 | ||
|
|
764ef48fd1 | ||
|
|
3f95c81243 | ||
|
|
7c385d329d | ||
|
|
553438d219 | ||
|
|
941921904c | ||
|
|
7d808ed798 | ||
|
|
7704072a65 | ||
|
|
9076baf1c8 | ||
|
|
954ff53d9b | ||
|
|
37d268bb16 | ||
|
|
5befa90f81 | ||
|
|
6e3313f00d | ||
|
|
7b5afb7afe | ||
|
|
fe13dad06f | ||
|
|
b41bad0ae2 | ||
|
|
18ef7f0272 | ||
|
|
4435a5aee4 | ||
|
|
d9e3a47894 | ||
|
|
c96be18187 | ||
|
|
47416dd3f8 | ||
|
|
4486f734bb | ||
|
|
bea6a2c8f7 | ||
|
|
82af9ed78e | ||
|
|
2b9e38d259 | ||
|
|
7c4222aed2 | ||
|
|
a574ced428 | ||
|
|
cf437e6d94 | ||
|
|
e1bb8aa125 | ||
|
|
c11bdd81e9 | ||
|
|
f1192c95a8 | ||
|
|
ae187cc21a | ||
|
|
a7875718f7 | ||
|
|
5db1f7cda7 | ||
|
|
cb98b17052 | ||
|
|
7aee1c6d6e | ||
|
|
f22199b7d9 | ||
|
|
0a8d6e5147 | ||
|
|
6d519dac1e | ||
|
|
d5a174c71b | ||
|
|
628de70e89 | ||
|
|
fee08f3826 | ||
|
|
bdfc383a18 | ||
|
|
f6b5524e0e | ||
|
|
e7461fccab | ||
|
|
a92872c831 | ||
|
|
094feb74ec | ||
|
|
9b1c660306 | ||
|
|
4b860ba897 | ||
|
|
3d8b2cf35f | ||
|
|
41fc13cfe2 | ||
|
|
5fde5dcc7c | ||
|
|
034885d810 | ||
|
|
3f7a7b8cd2 | ||
|
|
98dc50a9ed | ||
|
|
5dd820d12e | ||
|
|
bc30db2875 | ||
|
|
abe914d446 | ||
|
|
cc65ce6812 | ||
|
|
1a6454ee79 | ||
|
|
b72f397369 | ||
|
|
e698fe8d18 | ||
|
|
c8cad1c295 |
@@ -0,0 +1 @@
|
||||
github: clash-verge-rev
|
||||
@@ -9,13 +9,16 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
|
||||
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
|
||||
5. 请 **务必** 尝试 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本,确定问题是否仍然存在
|
||||
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
|
||||
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
|
||||
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
@@ -34,6 +37,7 @@ body:
|
||||
attributes:
|
||||
label: 软件版本 / Verge Version
|
||||
description: 请提供Verge的具体版本,如果是alpha版本,请注明下载时间(精确到小时分钟) / Please provide the specific version of Verge. If it is an alpha version, please indicate the download time (accurate to hours and minutes)
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -42,13 +46,13 @@ body:
|
||||
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 操作系统 / OS
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- MacOS
|
||||
- label: Windows
|
||||
- label: Linux
|
||||
- label: MacOS
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -61,5 +65,9 @@ body:
|
||||
attributes:
|
||||
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to debug, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
|
||||
placeholder: |
|
||||
日志目录一般位于 Clash Verge Rev 安装目录的 "logs/" 子目录中,请将日志内容粘贴到此处。
|
||||
Log directory is usually located in the "logs/" subdirectory of the Clash Verge Rev installation directory, please paste the log content here.
|
||||
render: log
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -150,6 +150,28 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
||||
clean_old_assets:
|
||||
name: Clean Old Release Assets
|
||||
runs-on: ubuntu-latest
|
||||
needs: update_tag
|
||||
if: ${{ needs.update_tag.result == 'success' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Remove old assets from release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ env.TAG_NAME }}
|
||||
run: |
|
||||
VERSION=$(cat package.json | jq -r '.version')
|
||||
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
|
||||
for asset in $assets; do
|
||||
if [[ "$asset" != *"$VERSION"* ]]; then
|
||||
echo "Deleting old asset: $asset"
|
||||
gh release delete-asset "$TAG_NAME" "$asset" -y
|
||||
fi
|
||||
done
|
||||
|
||||
autobuild-x86-windows-macos-linux:
|
||||
name: Autobuild x86 Windows, MacOS and Linux
|
||||
needs: update_tag
|
||||
|
||||
@@ -2,12 +2,28 @@ name: Development Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_windows:
|
||||
description: "运行 Windows"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
run_macos_aarch64:
|
||||
description: "运行 macOS aarch64"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
run_macos_x86_64:
|
||||
description: "运行 macOS x86_64"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
concurrency:
|
||||
# only allow per workflow per commit (and not pr) to run at a time
|
||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
@@ -20,46 +36,60 @@ jobs:
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
bundle: nsis
|
||||
id: windows
|
||||
input: run_windows
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
bundle: dmg
|
||||
id: macos-aarch64
|
||||
input: run_macos_aarch64
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
bundle: dmg
|
||||
id: macos-x86_64
|
||||
input: run_macos_x86_64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Add Rust Target
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Pnpm install and check
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Tauri build
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
@@ -77,7 +107,7 @@ jobs:
|
||||
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
@@ -85,7 +115,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
|
||||
@@ -26,11 +26,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: corepack enable
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm format:check
|
||||
|
||||
# taplo:
|
||||
|
||||
+18
-11
@@ -1,5 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# $1: remote name (e.g., origin)
|
||||
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
|
||||
|
||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
cargo clippy --manifest-path ./src-tauri/Cargo.toml
|
||||
if [ $? -ne 0 ]; then
|
||||
@@ -8,20 +11,24 @@ if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# Only run format check if the remote exists and is the main repo
|
||||
remote_name="$1"
|
||||
remote_url=$(git remote get-url "$remote_name")
|
||||
|
||||
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
|
||||
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
|
||||
echo "[pre-push] Running pnpm format:check..."
|
||||
|
||||
pnpm format:check
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Code format check failed. Please fix formatting before pushing."
|
||||
exit 1
|
||||
if git remote get-url "$remote_name" >/dev/null 2>&1; then
|
||||
remote_url=$(git remote get-url "$remote_name")
|
||||
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
|
||||
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
|
||||
echo "[pre-push] Running pnpm format:check..."
|
||||
pnpm format:check
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Code format check failed. Please fix formatting before pushing."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "[pre-push] Not pushing to target repo. Skipping format check."
|
||||
fi
|
||||
else
|
||||
echo "[pre-push] Not pushing to target repo. Skipping format check."
|
||||
echo "[pre-push] Remote $remote_name does not exist. Skipping format check."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
+14
-4
@@ -1,6 +1,16 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"experimentalOperatorPosition": "start"
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"endOfLine": "lf",
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ Before you start contributing to the project, you need to set up your developmen
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
|
||||
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/start/prerequisites/) to install them on your system.
|
||||
|
||||
### Setup for Windows Users
|
||||
|
||||
|
||||
+43
-16
@@ -1,3 +1,44 @@
|
||||
## v2.3.2
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复系统代理端口不同步问题
|
||||
- 修复自定义 `css` 背景图无法生效问题
|
||||
- 修复在轻量模式下快速点击托盘图标带来的竞争态卡死问题
|
||||
- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题
|
||||
- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步
|
||||
- 修复导入订阅时非 http 协议链接被错误尝试导入
|
||||
- 修复切换节点后页面长时间 loading 及缓存过期导致的数据不同步问题
|
||||
- 修复将快捷键名称更名为 `Clash Verge`之后无法删除图标和无法删除注册表
|
||||
- 修复`DNS`覆写 `fallback` `proxy server` `nameserver` `direct Nameserver` 字段支持留空
|
||||
- 修复`DNS`覆写 `nameserver-policy` 字段无法正确识别 `geo` 库
|
||||
- 修复搜索框输入特殊字符崩溃
|
||||
- 修复 Windows 下 Start UP 名称与 exe 名称不统一
|
||||
- 修复显示 Mihomo 内核日志等级应该大于设置等级
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- `sidecar` 模式下清理多余的内核进程,防止运行出现异常
|
||||
- 新 macOS 下 TUN 和系统代理模式托盘图标(暂测)
|
||||
- 快捷键事件通过系统通知
|
||||
- 添加外部 `cors` 控制面板
|
||||
|
||||
### 🚀 优化改进
|
||||
|
||||
- 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死
|
||||
- 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死
|
||||
- 改进主页订阅卡流量已使用比例计算精度
|
||||
- 优化后端缓存刷新机制,支持毫秒级 TTL(默认 3000ms),减少重复请求并提升性能,切换节点时强制刷新后端数据,前端 UI 实时更新,操作更流畅
|
||||
- 解耦前端数据拉取与后端缓存刷新,提升节点切换速度和一致性
|
||||
|
||||
### 🗑️ 移除内容
|
||||
|
||||
- 移除了 macOS tray 图标显示网络速率
|
||||
|
||||
### 🌐 国际化更新
|
||||
|
||||
- 修复部分翻译缺失和不一致问题
|
||||
|
||||
## v2.3.1
|
||||
|
||||
### 🐞 修复问题
|
||||
@@ -18,7 +59,8 @@
|
||||
|
||||
- 优化 托盘 统一响应
|
||||
- 优化 静默启动+自启动轻量模式 运行方式
|
||||
- 升级依赖
|
||||
- 降低前端潜在内存泄漏风险,提升运行时性能
|
||||
- 优化 React 状态、副作用、数据获取、清理等流程。
|
||||
|
||||
## v2.3.0
|
||||
|
||||
@@ -31,7 +73,6 @@
|
||||
|
||||
- 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
|
||||
- macOS:
|
||||
|
||||
- MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
|
||||
- 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
|
||||
- 彩色 Tray 图标颜色偏淡;
|
||||
@@ -69,13 +110,11 @@
|
||||
- 托盘新增当前轻量模式状态显示
|
||||
- 关闭系统代理时同时断开已建立的连接
|
||||
- 新增 WebDAV 功能:
|
||||
|
||||
- 加入 UA 请求头
|
||||
- 支持目录重定向
|
||||
- 备份目录检查与上传重试机制
|
||||
|
||||
- 自动订阅更新机制:
|
||||
|
||||
- 加入请求超时机制防止卡死
|
||||
- 支持在代理状态下自动重试订阅更新
|
||||
- 支持订阅卡片点击切换下次自动更新时间,并显示更新结果提示
|
||||
@@ -92,13 +131,11 @@
|
||||
### 🚀 优化改进
|
||||
|
||||
- **系统相关:**
|
||||
|
||||
- 系统代理 Bypass 设置优化
|
||||
- 优化代理设置更新逻辑与守卫机制
|
||||
- Windows 启动方式调整为 Startup 文件夹,解决管理员模式下自启问题
|
||||
|
||||
- **性能与稳定性:**
|
||||
|
||||
- 全面异步化处理配置加载、UI 启动、事件通知等关键流程,解决卡顿问题
|
||||
- 优化 MihomoManager 实现与窗口创建流程
|
||||
- 改进内核日志等级为 `warn`,减少噪音输出
|
||||
@@ -109,7 +146,6 @@
|
||||
- 优化缓存 Mihomo proxy 和 providers 信息内存使用
|
||||
|
||||
- **前端与界面体验:**
|
||||
|
||||
- 切换规则页自动刷新数据
|
||||
- 非激活订阅编辑时不再触发配置重载
|
||||
- 优化托盘速率显示,macOS 下默认关闭
|
||||
@@ -120,7 +156,6 @@
|
||||
- 优化保存机制,使用平滑函数防止卡顿
|
||||
|
||||
- **配置增强与安全性:**
|
||||
|
||||
- 配置缺失 `secret` 字段时自动补全为 `set-your-secret`
|
||||
- 强制为 Mihomo 配置补全 `external-controller-cors` 字段(默认不允许跨域,限制本地访问)计划后续支持自定义 cors
|
||||
- 优化窗口权限设置与状态初始化逻辑
|
||||
@@ -255,7 +290,6 @@
|
||||
#### 新增功能
|
||||
|
||||
1. **首页**
|
||||
|
||||
- 新增首页功能,默认启动页面改为首页。
|
||||
- 首页流量图卡片显示上传/下载名称。
|
||||
- 首页支持轻量模式切换。
|
||||
@@ -263,21 +297,17 @@
|
||||
- 限制首页配置文件卡片URL长度。
|
||||
|
||||
2. **DNS 设置与覆写**
|
||||
|
||||
- 新增 DNS 覆写功能。
|
||||
- 默认启用 DNS 覆写。
|
||||
|
||||
3. **解锁测试**
|
||||
|
||||
- 新增解锁测试页面。
|
||||
|
||||
4. **轻量模式**
|
||||
|
||||
- 新增轻量模式及设置。
|
||||
- 添加自动轻量模式定时器。
|
||||
|
||||
5. **系统支持**
|
||||
|
||||
- Mihomo(meta)内核升级 1.19.3
|
||||
- macOS 支持 CMD+W 关闭窗口。
|
||||
- 新增 macOS 应用菜单。
|
||||
@@ -292,7 +322,6 @@
|
||||
#### 修复
|
||||
|
||||
1. **系统**
|
||||
|
||||
- 修复 Windows 热键崩溃。
|
||||
- 修复 macOS 无框标题。
|
||||
- 修复 macOS 静默启动崩溃。
|
||||
@@ -307,7 +336,6 @@
|
||||
#### 优化
|
||||
|
||||
1. **性能**
|
||||
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化首页组件性能。
|
||||
- 优化流量图表资源使用。
|
||||
@@ -320,7 +348,6 @@
|
||||
- 优化修改verge配置性能。
|
||||
|
||||
2. **重构**
|
||||
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化定时器管理。
|
||||
- 重构 MihomoManager 处理流量。
|
||||
|
||||
+32
-43
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.3.1",
|
||||
"version": "2.3.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
@@ -29,88 +29,77 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/lab": "7.0.0-beta.13",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-data-grid": "^8.5.2",
|
||||
"@tauri-apps/api": "2.5.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
|
||||
"@tauri-apps/plugin-notification": "^2.2.3",
|
||||
"@tauri-apps/plugin-process": "^2.2.2",
|
||||
"@tauri-apps/plugin-shell": "2.2.2",
|
||||
"@tauri-apps/plugin-updater": "2.8.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.3",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@mui/icons-material": "^7.2.0",
|
||||
"@mui/lab": "7.0.0-beta.14",
|
||||
"@mui/material": "^7.2.0",
|
||||
"@mui/x-data-grid": "^8.8.0",
|
||||
"@tauri-apps/api": "2.6.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.0",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "2.3.0",
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.3.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.5",
|
||||
"ahooks": "^3.9.0",
|
||||
"axios": "^1.10.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"cli-color": "^2.0.4",
|
||||
"d3-shape": "^3.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.49",
|
||||
"glob": "^11.0.3",
|
||||
"i18next": "^25.2.1",
|
||||
"js-base64": "^3.7.7",
|
||||
"i18next": "^25.3.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"peggy": "^5.0.3",
|
||||
"react": "19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "15.5.3",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-i18next": "15.6.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.58.0",
|
||||
"react-router-dom": "7.6.2",
|
||||
"react-router-dom": "7.6.3",
|
||||
"react-virtuoso": "^4.13.0",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.3.3",
|
||||
"swr": "^2.3.4",
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.5"
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@tauri-apps/cli": "2.5.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@tauri-apps/cli": "2.6.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@vitejs/plugin-legacy": "^6.1.1",
|
||||
"@vitejs/plugin-react": "4.5.2",
|
||||
"@vitejs/plugin-legacy": "^7.0.0",
|
||||
"@vitejs/plugin-react": "4.6.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"commander": "^14.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"meta-json-schema": "^1.19.10",
|
||||
"meta-json-schema": "^1.19.11",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"sass": "^1.89.2",
|
||||
"terser": "^5.43.0",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite": "^7.0.4",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"endOfLine": "lf"
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.13.2"
|
||||
}
|
||||
|
||||
Generated
+339
-448
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": ["config:recommended"],
|
||||
"extends": ["config:recommended", ":disableDependencyDashboard"],
|
||||
"baseBranches": ["dev"],
|
||||
"enabledManagers": ["cargo", "npm"],
|
||||
"labels": ["dependencies"],
|
||||
|
||||
Generated
+1390
-1152
File diff suppressed because it is too large
Load Diff
+30
-27
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.3.1"
|
||||
version = "2.3.2"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -13,7 +13,7 @@ build = "build.rs"
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
tauri-build = { version = "2.3.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3.7"
|
||||
@@ -25,29 +25,28 @@ dunce = "1.0.5"
|
||||
log4rs = "1.3.0"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.41"
|
||||
sysinfo = "0.35.2"
|
||||
sysinfo = "=0.35.2"
|
||||
boa_engine = "0.20.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.34-deprecated"
|
||||
serde_yaml = "0.9.34"
|
||||
once_cell = "1.21.3"
|
||||
lazy_static = "1.5.0"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3.1"
|
||||
tokio = { version = "1.45.1", features = [
|
||||
tokio = { version = "1.46.1", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] }
|
||||
reqwest = { version = "0.12.22", features = ["json", "rustls-tls", "cookies"] }
|
||||
regex = "1.11.1"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
image = "0.25.6"
|
||||
imageproc = "0.25.0"
|
||||
tauri = { version = "2.5.1", features = [
|
||||
tauri = { version = "2.6.2", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
@@ -55,31 +54,31 @@ tauri = { version = "2.5.1", features = [
|
||||
"image-png",
|
||||
] }
|
||||
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.2.2"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-fs = "2.3.0"
|
||||
tauri-plugin-process = "2.2.2"
|
||||
tauri-plugin-clipboard-manager = "2.2.3"
|
||||
tauri-plugin-deep-link = "2.3.0"
|
||||
tauri-plugin-shell = "2.3.0"
|
||||
tauri-plugin-dialog = "2.3.0"
|
||||
tauri-plugin-fs = "2.4.0"
|
||||
tauri-plugin-process = "2.3.0"
|
||||
tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-deep-link = "2.4.0"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-window-state = "2.2.3"
|
||||
zip = "4.1.0"
|
||||
tauri-plugin-window-state = "2.3.0"
|
||||
zip = "=4.2.0"
|
||||
reqwest_dav = "0.2.1"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
getrandom = "0.3.3"
|
||||
tokio-tungstenite = "0.27.0"
|
||||
futures = "0.3.31"
|
||||
sys-locale = "0.3.2"
|
||||
async-trait = "0.1.88"
|
||||
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
||||
ab_glyph = "0.2.29"
|
||||
tungstenite = "0.27.0"
|
||||
libc = "0.2.173"
|
||||
libc = "0.2.174"
|
||||
gethostname = "1.0.2"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.9"
|
||||
hex = "0.4.3"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.3.0"
|
||||
dashmap = "7.0.0-rc2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
@@ -93,15 +92,19 @@ winapi = { version = "0.3.9", features = [
|
||||
"errhandlingapi",
|
||||
"minwindef",
|
||||
"winerror",
|
||||
"tlhelp32",
|
||||
"processthreadsapi",
|
||||
"winhttp",
|
||||
"winreg",
|
||||
] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.4.0"
|
||||
tauri-plugin-global-shortcut = "2.2.1"
|
||||
tauri-plugin-updater = "2.8.1"
|
||||
tauri-plugin-autostart = "2.5.0"
|
||||
tauri-plugin-global-shortcut = "2.3.0"
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
@@ -136,12 +139,12 @@ name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.6.0"
|
||||
tempfile = "3.20.0"
|
||||
|
||||
[workspace]
|
||||
members = ["src_crates/crate_mihomo_api"]
|
||||
|
||||
# [patch.crates-io]
|
||||
# bitflags = { git = "https://github.com/bitflags/bitflags", rev = "2.9.0" }
|
||||
# zerocopy = { git = "https://github.com/google/zerocopy", rev = "v0.8.24" }
|
||||
# tungstenite = { git = "https://github.com/snapview/tungstenite-rs", rev = "v0.26.2" }
|
||||
[[bench]]
|
||||
name = "draft_benchmark"
|
||||
harness = false
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use std::hint::black_box;
|
||||
|
||||
// 业务模型 & Draft
|
||||
use app_lib::config::Draft as DraftNew;
|
||||
use app_lib::config::IVerge;
|
||||
|
||||
// fn bench_apply_old(c: &mut Criterion) {
|
||||
// c.bench_function("apply_draft_old", |b| {
|
||||
// b.iter(|| {
|
||||
// let verge = Box::new(IVerge {
|
||||
// enable_auto_launch: Some(true),
|
||||
// enable_tun_mode: Some(false),
|
||||
// ..Default::default()
|
||||
// });
|
||||
|
||||
// let draft = DraftOld::from(black_box(verge));
|
||||
|
||||
// {
|
||||
// let mut d = draft.draft_mut();
|
||||
// d.enable_auto_launch = Some(false);
|
||||
// }
|
||||
|
||||
// let _ = draft.apply();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// fn bench_discard_old(c: &mut Criterion) {
|
||||
// c.bench_function("discard_draft_old", |b| {
|
||||
// b.iter(|| {
|
||||
// let verge = Box::new(IVerge::default());
|
||||
// let draft = DraftOld::from(black_box(verge));
|
||||
|
||||
// {
|
||||
// let mut d = draft.draft_mut();
|
||||
// d.enable_auto_launch = Some(false);
|
||||
// }
|
||||
|
||||
// let _ = draft.discard();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
/// 基准:修改草稿并 apply()
|
||||
fn bench_apply_new(c: &mut Criterion) {
|
||||
c.bench_function("apply_draft_new", |b| {
|
||||
b.iter(|| {
|
||||
let verge = Box::new(IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let draft = DraftNew::from(black_box(verge));
|
||||
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
}
|
||||
|
||||
let _ = draft.apply();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:修改草稿并 discard()
|
||||
fn bench_discard_new(c: &mut Criterion) {
|
||||
c.bench_function("discard_draft_new", |b| {
|
||||
b.iter(|| {
|
||||
let verge = Box::new(IVerge::default());
|
||||
let draft = DraftNew::from(black_box(verge));
|
||||
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
}
|
||||
|
||||
let _ = draft.discard();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
// bench_apply_old,
|
||||
// bench_discard_old,
|
||||
bench_apply_new,
|
||||
bench_discard_new
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -17,6 +17,7 @@
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"core:window:allow-set-theme"
|
||||
"core:window:allow-set-theme",
|
||||
"notification:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
# This template contains all of the possible sections and their default values
|
||||
|
||||
# Note that all fields that take a lint level have these possible values:
|
||||
# * deny - An error will be produced and the check will fail
|
||||
# * warn - A warning will be produced, but the check will not fail
|
||||
# * allow - No warning or error will be produced, though in some cases a note
|
||||
# will be
|
||||
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
# dependency, such as, for example, the `nix` crate only being used via the
|
||||
# `target_family = "unix"` configuration, that only having windows targets in
|
||||
# this list would mean the nix crate, as well as any of its exclusive
|
||||
# dependencies not shared by any other crates, would be ignored, as the target
|
||||
# list here is effectively saying which targets you are building for.
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||
# This option can be overridden via `--feature-depth` on the cmd line
|
||||
feature-depth = 1
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
"RUSTSEC-2024-0415",
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
#"MIT",
|
||||
#"Apache-2.0",
|
||||
#"Apache-2.0 WITH LLVM-exception",
|
||||
]
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.85
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
#crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# github.com organizations to allow git sources for
|
||||
github = []
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# bitbucket.org organizations to allow git sources for
|
||||
bitbucket = []
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@@ -691,8 +691,6 @@ SectionEnd
|
||||
app_check_done:
|
||||
!macroend
|
||||
|
||||
|
||||
|
||||
Var VC_REDIST_URL
|
||||
Var VC_REDIST_EXE
|
||||
|
||||
@@ -701,25 +699,25 @@ Section CheckAndInstallVSRuntime
|
||||
${If} ${IsNativeARM64}
|
||||
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
|
||||
StrCpy $VC_REDIST_EXE "vc_redist.arm64.exe"
|
||||
|
||||
|
||||
; 检查关键DLL
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
|
||||
|
||||
|
||||
${ElseIf} ${RunningX64}
|
||||
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
||||
StrCpy $VC_REDIST_EXE "vc_redist.x64.exe"
|
||||
|
||||
|
||||
; 检查关键DLL
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
|
||||
|
||||
|
||||
${Else}
|
||||
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x86.exe"
|
||||
StrCpy $VC_REDIST_EXE "vc_redist.x86.exe"
|
||||
|
||||
|
||||
; 检查关键DLL
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
|
||||
${EndIf}
|
||||
|
||||
@@ -754,30 +752,34 @@ Section CheckAndInstallVSRuntime
|
||||
${Else}
|
||||
DetailPrint "Visual C++ Redistributable 下载失败"
|
||||
${EndIf}
|
||||
|
||||
|
||||
Done:
|
||||
SectionEnd
|
||||
|
||||
|
||||
|
||||
Section Install
|
||||
SetOutPath $INSTDIR
|
||||
nsExec::Exec 'netsh int tcp res'
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
|
||||
; 删除 window-state.json 文件 .window-state.json 文件
|
||||
DetailPrint "开始删除删除 window-state.json or .window-state.json"
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
SetRegView 64
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
${EndIf}
|
||||
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
@@ -788,7 +790,7 @@ Section Install
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
${EndIf}
|
||||
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
@@ -798,7 +800,7 @@ Section Install
|
||||
; Delete clash-verge.desktop
|
||||
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
|
||||
Delete "$INSTDIR\Clash Verge.exe"
|
||||
|
||||
|
||||
; Copy main executable
|
||||
File "${MAINBINARYSRCPATH}"
|
||||
|
||||
@@ -916,26 +918,29 @@ FunctionEnd
|
||||
!macroend
|
||||
|
||||
Section Uninstall
|
||||
;删除 window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
!insertmacro RemoveVergeService
|
||||
|
||||
; 删除 window-state.json 文件 .window-state.json 文件
|
||||
DetailPrint "开始删除删除 window-state.json or .window-state.json"
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
|
||||
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
${EndIf}
|
||||
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
@@ -946,7 +951,7 @@ Section Uninstall
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
${EndIf}
|
||||
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
@@ -978,24 +983,144 @@ Section Uninstall
|
||||
{{/each}}
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
; 删除固定栏
|
||||
!insertmacro DeleteAppUserModelId
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk"
|
||||
|
||||
; Remove start menu shortcut
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||
; 删除所有用户的桌面快捷方式
|
||||
DetailPrint "开始删除所有用户桌面的 Clash Verge 快捷方式..."
|
||||
|
||||
; 删除公共桌面快捷方式
|
||||
Delete "C:\Users\Public\Desktop\Clash Verge.lnk"
|
||||
Delete "C:\Users\Public\Desktop\clash-verge.lnk"
|
||||
|
||||
; 枚举所有用户配置文件目录
|
||||
SetRegView 64
|
||||
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" ""
|
||||
|
||||
; 初始化循环
|
||||
StrCpy $R1 0
|
||||
Loop:
|
||||
EnumRegKey $R2 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $R1
|
||||
${If} $R2 == ""
|
||||
Goto Done
|
||||
${EndIf}
|
||||
|
||||
; 读取用户配置文件路径
|
||||
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$R2" "ProfileImagePath"
|
||||
${If} $R3 != ""
|
||||
; 构建用户桌面路径
|
||||
StrCpy $R4 "$R3\Desktop"
|
||||
|
||||
; 删除该用户桌面的快捷方式
|
||||
Delete "$R4\Clash Verge.lnk"
|
||||
Delete "$R4\clash-verge.lnk"
|
||||
|
||||
DetailPrint "尝试删除用户 '$R3' 桌面的 Clash Verge 快捷方式"
|
||||
${EndIf}
|
||||
|
||||
; 递增循环计数器
|
||||
IntOp $R1 $R1 + 1
|
||||
Goto Loop
|
||||
Done:
|
||||
|
||||
DetailPrint "所有用户桌面快捷方式删除完成"
|
||||
|
||||
; 删除用户开始菜单文件夹
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
||||
RMDir /r /REBOOTOK "$SMPROGRAMS\$AppStartMenuFolder"
|
||||
|
||||
; 删除系统级开始菜单中的 Clash Verge
|
||||
Delete "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge\Clash Verge.lnk"
|
||||
Delete "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge\clash-verge.lnk"
|
||||
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge"
|
||||
|
||||
; 删除所有带 Clash Verge 或 clash-verge 的注册表项
|
||||
DetailPrint "开始清理所有 Clash Verge 相关的注册表项..."
|
||||
|
||||
; 设置注册表查看模式 (64位)
|
||||
SetRegView 64
|
||||
|
||||
; 清理 CurrentVersion\Run 中的自启动项
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
|
||||
; 清理 App Paths
|
||||
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Clash Verge.exe"
|
||||
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\clash-verge.exe"
|
||||
|
||||
; 删除指定的注册表路径
|
||||
DeleteRegKey HKLM "Software\Clash Verge Rev"
|
||||
DeleteRegKey HKCU "Software\Clash Verge Rev"
|
||||
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\ClashVerge"
|
||||
DeleteRegKey HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Clash Verge"
|
||||
|
||||
; 清理 Uninstall 信息
|
||||
StrCpy $R1 0
|
||||
EnumUninstallLoop:
|
||||
EnumRegKey $R2 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $R1
|
||||
${If} $R2 == ""
|
||||
Goto EnumUninstallDone
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$R2" "DisplayName"
|
||||
${If} $R3 != ""
|
||||
StrCmp $R3 "Clash Verge" 0 +3
|
||||
StrCmp $R3 "clash-verge" 0 +2
|
||||
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$R2"
|
||||
${EndIf}
|
||||
|
||||
IntOp $R1 $R1 + 1
|
||||
Goto EnumUninstallLoop
|
||||
EnumUninstallDone:
|
||||
|
||||
; 清理用户特定的注册表项
|
||||
StrCpy $R1 0
|
||||
EnumHKCULoop:
|
||||
EnumRegKey $R2 HKCU "SOFTWARE" $R1
|
||||
${If} $R2 == ""
|
||||
Goto EnumHKCUDone
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R3 HKCU "SOFTWARE\$R2" ""
|
||||
${If} $R3 != ""
|
||||
StrCmp $R3 "Clash Verge" 0 +3
|
||||
StrCmp $R3 "clash-verge" 0 +2
|
||||
DeleteRegKey HKCU "SOFTWARE\$R2"
|
||||
${EndIf}
|
||||
|
||||
IntOp $R1 $R1 + 1
|
||||
Goto EnumHKCULoop
|
||||
EnumHKCUDone:
|
||||
|
||||
; 清理系统范围的注册表项
|
||||
StrCpy $R1 0
|
||||
EnumHKLMLoop:
|
||||
EnumRegKey $R2 HKLM "SOFTWARE" $R1
|
||||
${If} $R2 == ""
|
||||
Goto EnumHKLMDone
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R3 HKLM "SOFTWARE\$R2" ""
|
||||
${If} $R3 != ""
|
||||
StrCmp $R3 "Clash Verge" 0 +3
|
||||
StrCmp $R3 "clash-verge" 0 +2
|
||||
DeleteRegKey HKLM "SOFTWARE\$R2"
|
||||
${EndIf}
|
||||
|
||||
IntOp $R1 $R1 + 1
|
||||
Goto EnumHKLMLoop
|
||||
EnumHKLMDone:
|
||||
|
||||
DetailPrint "注册表清理完成"
|
||||
|
||||
; Remove desktop shortcuts
|
||||
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
Delete "$DESKTOP\clash-verge.lnk"
|
||||
|
||||
; Remove registry information for add/remove programs
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
@@ -1015,10 +1140,6 @@ Section Uninstall
|
||||
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||
${EndIf}
|
||||
|
||||
;删除 window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
|
||||
${GetOptions} $CMDLINE "/P" $R0
|
||||
IfErrors +2 0
|
||||
SetAutoClose true
|
||||
@@ -1051,7 +1172,6 @@ FunctionEnd
|
||||
System::Free $4
|
||||
System::Free $5
|
||||
${IPropertyStore::Commit} $2 ""
|
||||
${IUnknown::Release} $2 ""
|
||||
${IPersistFile::Save} $1 '("${shortcut}",1)'
|
||||
${EndIf}
|
||||
${IUnknown::Release} $1 ""
|
||||
|
||||
@@ -147,7 +147,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
Err(format!("下载的内容不是有效图片: {}", url))
|
||||
Err(format!("下载的内容不是有效图片: {url}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
/// UI加载阶段
|
||||
#[tauri::command]
|
||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
log::info!(target: "app", "UI加载阶段更新: {}", stage);
|
||||
log::info!(target: "app", "UI加载阶段更新: {stage}");
|
||||
|
||||
use crate::utils::resolve::UiReadyStage;
|
||||
|
||||
@@ -228,8 +228,8 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
||||
"Ready" => UiReadyStage::Ready,
|
||||
_ => {
|
||||
log::warn!(target: "app", "未知的UI加载阶段: {}", stage);
|
||||
return Err(format!("未知的UI加载阶段: {}", stage));
|
||||
log::warn!(target: "app", "未知的UI加载阶段: {stage}");
|
||||
return Err(format!("未知的UI加载阶段: {stage}"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+12
-12
@@ -14,7 +14,7 @@ pub fn copy_clash_env() -> CmdResult {
|
||||
/// 获取Clash信息
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest().get_client_info())
|
||||
Ok(Config::clash().latest_ref().get_client_info())
|
||||
}
|
||||
|
||||
/// 修改Clash配置
|
||||
@@ -49,8 +49,8 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Core changed but failed to restart: {}", err);
|
||||
log::error!(target: "app", "{}", error_msg);
|
||||
let error_msg = format!("Core changed but failed to restart: {err}");
|
||||
log::error!(target: "app", "{error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
@@ -116,7 +116,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
// 保存DNS配置到文件
|
||||
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
||||
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
|
||||
log::info!(target: "app", "DNS config saved to {:?}", dns_path);
|
||||
log::info!(target: "app", "DNS config saved to {dns_path:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -137,7 +137,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
let dns_path = match dirs::app_home_dir() {
|
||||
Ok(path) => path.join("dns_config.yaml"),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get home dir: {}", e);
|
||||
log::error!(target: "app", "Failed to get home dir: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -150,7 +150,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to read DNS config: {}", e);
|
||||
log::error!(target: "app", "Failed to read DNS config: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -163,7 +163,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
patch
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to parse DNS config: {}", e);
|
||||
log::error!(target: "app", "Failed to parse DNS config: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -173,18 +173,18 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
// 重新生成配置,确保DNS配置被正确应用
|
||||
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
|
||||
Config::runtime()
|
||||
.latest()
|
||||
.draft_mut()
|
||||
.patch_config(patch_config.clone());
|
||||
|
||||
// 首先重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
|
||||
log::error!(target: "app", "Failed to regenerate config with DNS: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 然后应用新配置
|
||||
if let Err(err) = CoreManager::global().update_config().await {
|
||||
log::error!(target: "app", "Failed to apply config with DNS: {}", err);
|
||||
log::error!(target: "app", "Failed to apply config with DNS: {err}");
|
||||
} else {
|
||||
log::info!(target: "app", "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -196,7 +196,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
|
||||
// 重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config: {}", err);
|
||||
log::error!(target: "app", "Failed to regenerate config: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
|
||||
log::error!(target: "app", "Failed to apply regenerated config: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ fn country_code_to_emoji(country_code: &str) -> String {
|
||||
let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
|
||||
|
||||
char::from_u32(c1)
|
||||
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{}{}", c1, c2)))
|
||||
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}")))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
||||
|
||||
map.get("loc").map(|loc| {
|
||||
let emoji = country_code_to_emoji(loc);
|
||||
format!("{}{}", emoji, loc)
|
||||
format!("{emoji}{loc}")
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -255,7 +255,7 @@ async fn check_gemini(client: &Client) -> UnlockItem {
|
||||
caps.get(1).map(|m| {
|
||||
let country_code = m.as_str();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{}{}", emoji, country_code)
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
caps.get(1).map(|m| {
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{}{}", emoji, country_code)
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -384,10 +384,8 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
}
|
||||
|
||||
// 第二步:使用设备ID检查访问权限 (使用相同的Cookie)
|
||||
let url = format!(
|
||||
"https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={}",
|
||||
device_id
|
||||
);
|
||||
let url =
|
||||
format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
|
||||
|
||||
let token_result = match client_with_cookies.get(&url).send().await {
|
||||
Ok(response) => {
|
||||
@@ -431,7 +429,7 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
.map(|m| {
|
||||
let country_code = m.as_str();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{}{}", emoji, country_code)
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
}
|
||||
Err(_) => None,
|
||||
@@ -470,7 +468,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
|
||||
// 检查连接失败情况
|
||||
if let Err(e) = &result1 {
|
||||
eprintln!("Netflix请求错误: {}", e);
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
@@ -487,7 +485,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
.await;
|
||||
|
||||
if let Err(e) = &result2 {
|
||||
eprintln!("Netflix请求错误: {}", e);
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
@@ -541,7 +539,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region_code)),
|
||||
region: Some(format!("{emoji}{region_code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -557,7 +555,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("获取Netflix区域信息失败: {}", e);
|
||||
eprintln!("获取Netflix区域信息失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes (但无法获取区域)".to_string(),
|
||||
@@ -570,7 +568,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
// 其他未知错误状态
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: format!("Failed (状态码: {}_{}", status1, status2),
|
||||
status: format!("Failed (状态码: {status1}_{status2}"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
@@ -614,7 +612,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, country)),
|
||||
region: Some(format!("{emoji}{country}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -631,7 +629,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("解析Fast.com API响应失败: {}", e);
|
||||
eprintln!("解析Fast.com API响应失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (解析错误)".to_string(),
|
||||
@@ -642,7 +640,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fast.com API请求失败: {}", e);
|
||||
eprintln!("Fast.com API请求失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (CDN API)".to_string(),
|
||||
@@ -884,7 +882,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{} (from main page)", emoji, region)),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -947,7 +945,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{} (from main page)", emoji, region)),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -968,7 +966,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -990,7 +988,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Soon".to_string(),
|
||||
region: Some(format!("{}{}(即将上线)", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}(即将上线)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
@@ -999,13 +997,13 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
None => UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!("Failed (Error: Unknown region status for {})", region),
|
||||
status: format!("Failed (Error: Unknown region status for {region})"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
@@ -1056,7 +1054,7 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -1170,7 +1168,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
.connection_verbose(true) // 详细连接信息
|
||||
.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => return Err(format!("创建HTTP客户端失败: {}", e)),
|
||||
Err(e) => return Err(format!("创建HTTP客户端失败: {e}")),
|
||||
};
|
||||
|
||||
// 创建共享的结果集
|
||||
@@ -1284,7 +1282,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
// 等待所有任务完成
|
||||
while let Some(res) = tasks.join_next().await {
|
||||
if let Err(e) = res {
|
||||
eprintln!("任务执行失败: {}", e);
|
||||
eprintln!("任务执行失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use super::CmdResult;
|
||||
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
|
||||
use crate::wrap_err;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml::Mapping;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
let current = spawn_blocking(Sysproxy::get_system_proxy)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to spawn blocking task for sysproxy: {}", e))?
|
||||
.map_err(|e| format!("Failed to get system proxy: {}", e))?;
|
||||
log::debug!(target: "app", "异步获取系统代理配置");
|
||||
|
||||
let current = AsyncProxyQuery::get_system_proxy().await;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
@@ -21,21 +19,28 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
/// 获取自动代理配置
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
let current = spawn_blocking(Autoproxy::get_auto_proxy)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to spawn blocking task for autoproxy: {}", e))?
|
||||
.map_err(|e| format!("Failed to get auto proxy: {}", e))?;
|
||||
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
let current = proxy_manager.get_auto_proxy_cached();
|
||||
// 异步请求更新,立即返回缓存数据
|
||||
tokio::spawn(async move {
|
||||
let _ = proxy_manager.get_auto_proxy_async().await;
|
||||
});
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.into());
|
||||
map.insert("url".into(), current.url.clone().into());
|
||||
|
||||
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
@@ -49,7 +54,7 @@ pub fn get_system_hostname() -> CmdResult<String> {
|
||||
Ok(name) => name,
|
||||
Err(os_string) => {
|
||||
// 对于包含非UTF-8的主机名,使用调试格式化
|
||||
let fallback = format!("{:?}", os_string);
|
||||
let fallback = format!("{os_string:?}");
|
||||
// 去掉可能存在的引号
|
||||
fallback.trim_matches('"').to_string()
|
||||
}
|
||||
|
||||
+225
-38
@@ -6,12 +6,31 @@ use crate::{
|
||||
utils::{dirs, help, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
// 添加全局互斥锁防止并发配置更新
|
||||
// 全局互斥锁防止并发配置更新
|
||||
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
||||
|
||||
// 全局请求序列号跟踪,用于避免队列化执行
|
||||
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
static CURRENT_PROCESSING_PROFILE: RwLock<Option<String>> = RwLock::const_new(None);
|
||||
|
||||
/// 清理配置处理状态
|
||||
async fn cleanup_processing_state(sequence: u64, reason: &str) {
|
||||
*CURRENT_PROCESSING_PROFILE.write().await = None;
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"{},清理状态,序列号: {}",
|
||||
reason,
|
||||
sequence
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取配置文件避免锁竞争
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
@@ -20,7 +39,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
Duration::from_millis(500),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let profiles = Config::profiles();
|
||||
let latest = profiles.latest();
|
||||
let latest = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: latest.current.clone(),
|
||||
items: latest.items.clone(),
|
||||
@@ -47,7 +66,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
Duration::from_secs(2),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let profiles = Config::profiles();
|
||||
let data = profiles.data();
|
||||
let data = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: data.current.clone(),
|
||||
items: data.items.clone(),
|
||||
@@ -111,20 +130,20 @@ pub async fn enhance_profiles() -> CmdResult {
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
wrap_err!(Config::profiles().data_mut().append_item(item))
|
||||
}
|
||||
|
||||
/// 重新排序配置文件
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||
wrap_err!(Config::profiles().data_mut().reorder(active_id, over_id))
|
||||
}
|
||||
|
||||
/// 创建配置文件
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
wrap_err!(Config::profiles().data_mut().append_item(item))
|
||||
}
|
||||
|
||||
/// 更新配置文件
|
||||
@@ -136,10 +155,10 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
|
||||
/// 删除配置文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||
let should_update = wrap_err!({ Config::profiles().data_mut().delete_item(index) })?;
|
||||
|
||||
// 删除后自动清理冗余文件
|
||||
let _ = Config::profiles().latest().auto_cleanup();
|
||||
let _ = Config::profiles().latest_ref().auto_cleanup();
|
||||
|
||||
if should_update {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
@@ -151,13 +170,63 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 获取互斥锁,防止并发执行
|
||||
let _guard = PROFILE_UPDATE_MUTEX.lock().await;
|
||||
// 为当前请求分配序列号
|
||||
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let target_profile = profiles.current.clone();
|
||||
|
||||
logging!(info, Type::Cmd, true, "开始修改配置文件");
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
|
||||
current_sequence,
|
||||
target_profile
|
||||
);
|
||||
|
||||
let mutex_result =
|
||||
tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await;
|
||||
|
||||
let _guard = match mutex_result {
|
||||
Ok(guard) => guard,
|
||||
Err(_) => {
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"检测到更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"强制获取锁以处理最新请求: {}",
|
||||
current_sequence
|
||||
);
|
||||
PROFILE_UPDATE_MUTEX.lock().await
|
||||
}
|
||||
};
|
||||
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().latest().current.clone();
|
||||
let current_profile = Config::profiles().latest_ref().current.clone();
|
||||
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
|
||||
|
||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||
@@ -168,7 +237,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 获取目标配置文件路径
|
||||
let config_file_result = {
|
||||
let profiles_config = Config::profiles();
|
||||
let profiles_data = profiles_config.latest();
|
||||
let profiles_data = profiles_config.latest_ref();
|
||||
match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
if let Some(file) = &item.file {
|
||||
@@ -221,7 +290,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!(" {}", err);
|
||||
let error_msg = format!(" {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
@@ -236,7 +305,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
return Ok(false);
|
||||
}
|
||||
Err(join_err) => {
|
||||
let error_msg = format!("YAML解析任务失败: {}", join_err);
|
||||
let error_msg = format!("YAML解析任务失败: {join_err}");
|
||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_parse_error",
|
||||
@@ -247,7 +316,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {}", err);
|
||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_error",
|
||||
@@ -269,14 +338,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(ref profile) = target_profile {
|
||||
*CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone());
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"设置当前处理profile: {}, 序列号: {}",
|
||||
profile,
|
||||
current_sequence
|
||||
);
|
||||
}
|
||||
|
||||
// 更新profiles配置
|
||||
logging!(info, Type::Cmd, true, "正在更新配置草稿");
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"正在更新配置草稿,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
|
||||
let current_value = profiles.current.clone();
|
||||
|
||||
let _ = Config::profiles().draft().patch_config(profiles);
|
||||
let _ = Config::profiles().draft_mut().patch_config(profiles);
|
||||
|
||||
// 在调用内核前再次验证请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 为配置更新添加超时保护
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"开始内核配置更新,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30), // 30秒超时
|
||||
CoreManager::global().update_config(),
|
||||
@@ -286,38 +409,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 更新配置并进行验证
|
||||
match update_result {
|
||||
Ok(Ok((true, _))) => {
|
||||
logging!(info, Type::Cmd, true, "配置更新成功");
|
||||
// 内核操作完成后再次检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"配置更新成功,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||
log::warn!(target: "app", "强制刷新代理缓存失败: {}", e);
|
||||
log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = Tray::global().update_tooltip() {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_menu() {
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {}", e);
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
|
||||
// 保存配置文件
|
||||
if let Err(e) = Config::profiles().data().save_file() {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {}", e);
|
||||
if let Err(e) = Config::profiles().data_mut().save_file() {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
// 立即通知前端配置变更
|
||||
if let Some(current) = ¤t_value {
|
||||
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"向前端发送配置变更事件: {}, 序列号: {}",
|
||||
current,
|
||||
current_sequence
|
||||
);
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
}
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置切换完成").await;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
Ok(Ok((false, error_msg))) => {
|
||||
@@ -337,12 +490,16 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
items: None,
|
||||
};
|
||||
// 静默恢复,不触发验证
|
||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||
wrap_err!({
|
||||
Config::profiles()
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
})?;
|
||||
Config::profiles().apply();
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = Config::profiles().data().save_file() {
|
||||
log::warn!(target: "app", "异步保存恢复配置文件失败: {}", e);
|
||||
if let Err(e) = Config::profiles().data_mut().save_file() {
|
||||
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -351,18 +508,38 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置验证失败").await;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"更新过程发生错误: {}, 序列号: {}",
|
||||
e,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
|
||||
cleanup_processing_state(current_sequence, "更新过程错误").await;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
Err(_) => {
|
||||
// 超时处理
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(error, Type::Cmd, true, "{}", timeout_msg);
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"{}, 序列号: {}",
|
||||
timeout_msg,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
|
||||
if let Some(prev_profile) = current_profile {
|
||||
@@ -370,18 +547,26 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"超时后尝试恢复到之前的配置: {}",
|
||||
prev_profile
|
||||
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
|
||||
prev_profile,
|
||||
current_sequence
|
||||
);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||
wrap_err!({
|
||||
Config::profiles()
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
})?;
|
||||
Config::profiles().apply();
|
||||
}
|
||||
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置更新超时").await;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
@@ -407,7 +592,7 @@ pub async fn patch_profiles_config_by_profile_index(
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
// 保存修改前检查是否有更新 update_interval
|
||||
let update_interval_changed =
|
||||
if let Ok(old_profile) = Config::profiles().latest().get_item(&index) {
|
||||
if let Ok(old_profile) = Config::profiles().latest_ref().get_item(&index) {
|
||||
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
old_interval != new_interval
|
||||
@@ -416,7 +601,9 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
};
|
||||
|
||||
// 保存修改
|
||||
wrap_err!(Config::profiles().data().patch_item(index.clone(), profile))?;
|
||||
wrap_err!(Config::profiles()
|
||||
.data_mut()
|
||||
.patch_item(index.clone(), profile))?;
|
||||
|
||||
// 如果更新间隔变更,异步刷新定时器
|
||||
if update_interval_changed {
|
||||
@@ -439,7 +626,7 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
#[tauri::command]
|
||||
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
let file = {
|
||||
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||
wrap_err!(Config::profiles().latest_ref().get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
@@ -457,7 +644,7 @@ pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let profiles = profiles.latest_ref();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
|
||||
+27
-83
@@ -1,99 +1,43 @@
|
||||
use super::CmdResult;
|
||||
use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState};
|
||||
use std::{
|
||||
sync::Mutex,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri::Manager;
|
||||
use crate::module::mihomo::MihomoManager;
|
||||
use std::time::Duration;
|
||||
|
||||
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3);
|
||||
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
|
||||
use crate::state::proxy::ProxyRequestCache;
|
||||
|
||||
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
let should_refresh = {
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL {
|
||||
state.need_refresh = true;
|
||||
state.last_refresh_time = now;
|
||||
}
|
||||
state.need_refresh
|
||||
};
|
||||
|
||||
if should_refresh {
|
||||
let proxies = manager.get_refresh_proxies().await?;
|
||||
{
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
state.proxies = Box::new(proxies);
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "proxies刷新成功");
|
||||
}
|
||||
|
||||
let proxies = {
|
||||
let state = cmd_proxy_state.lock().unwrap();
|
||||
state.proxies.clone()
|
||||
};
|
||||
Ok(*proxies)
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("proxies", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
|
||||
manager.get_refresh_proxies().await.expect("fetch failed")
|
||||
})
|
||||
.await;
|
||||
Ok((*value).clone())
|
||||
}
|
||||
|
||||
/// 强制刷新代理缓存用于profile切换
|
||||
#[tauri::command]
|
||||
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
log::debug!(target: "app", "强制刷新代理缓存");
|
||||
|
||||
let proxies = manager.get_refresh_proxies().await?;
|
||||
|
||||
{
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
state.proxies = Box::new(proxies.clone());
|
||||
state.need_refresh = false;
|
||||
state.last_refresh_time = Instant::now();
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "强制刷新代理缓存完成");
|
||||
Ok(proxies)
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("proxies", "default");
|
||||
cache.map.remove(&key);
|
||||
get_proxies().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
let should_refresh = {
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL {
|
||||
state.need_refresh = true;
|
||||
state.last_refresh_time = now;
|
||||
}
|
||||
state.need_refresh
|
||||
};
|
||||
|
||||
if should_refresh {
|
||||
let manager = MihomoManager::global();
|
||||
let providers = manager.get_providers_proxies().await?;
|
||||
{
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
state.providers_proxies = Box::new(providers);
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "providers_proxies刷新成功");
|
||||
}
|
||||
|
||||
let providers_proxies = {
|
||||
let state = cmd_proxy_state.lock().unwrap();
|
||||
state.providers_proxies.clone()
|
||||
};
|
||||
Ok(*providers_proxies)
|
||||
let manager = MihomoManager::global();
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("providers", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
|
||||
manager.get_providers_proxies().await.expect("fetch failed")
|
||||
})
|
||||
.await;
|
||||
Ok((*value).clone())
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ use std::collections::HashMap;
|
||||
/// 获取运行时配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest().config.clone())
|
||||
Ok(Config::runtime().latest_ref().config.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时YAML配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let runtime = runtime.latest_ref();
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
@@ -26,11 +26,11 @@ pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
/// 获取运行时存在的键
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest().exists_keys.clone())
|
||||
Ok(Config::runtime().latest_ref().exists_keys.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时日志
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest().chain_logs.clone())
|
||||
Ok(Config::runtime().latest_ref().chain_logs.clone())
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
// 在异步操作前完成所有文件操作
|
||||
let (file_path, original_content, is_merge_file) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles_guard = profiles.latest();
|
||||
let profiles_guard = profiles.latest_ref();
|
||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||
// 确定是否为merge类型文件
|
||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||
|
||||
@@ -24,7 +24,7 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||
#[tauri::command]
|
||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
let info = format!("{sysinfo:?}");
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cliboard = app_handle.clipboard();
|
||||
@@ -37,7 +37,7 @@ pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
#[tauri::command]
|
||||
pub async fn get_system_info() -> CmdResult<String> {
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
let info = format!("{sysinfo:?}");
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{config::*, feat, wrap_err};
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
let verge_data = verge.latest_ref().clone();
|
||||
Ok(IVergeResponse::from(*verge_data))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().draft_mut().patch_config(patch.clone());
|
||||
Config::verge().apply();
|
||||
Config::verge()
|
||||
.data()
|
||||
.latest_ref()
|
||||
.save_file()
|
||||
.map_err(|err| err.to_string())?;
|
||||
core::backup::WebDavClient::global().reset();
|
||||
|
||||
@@ -97,25 +97,6 @@ impl IClashTemp {
|
||||
config.insert("port".into(), port.into());
|
||||
config.insert("external-controller".into(), ctrl.into());
|
||||
|
||||
// 强制覆盖 external-controller-cors 字段,允许本地和 tauri 前端
|
||||
let mut cors_map = Mapping::new();
|
||||
cors_map.insert("allow-private-network".into(), true.into());
|
||||
cors_map.insert(
|
||||
"allow-origins".into(),
|
||||
vec![
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
// Only enable this in dev mode
|
||||
#[cfg(feature = "verge-dev")]
|
||||
"http://localhost:3000",
|
||||
"https://yacd.metacubex.one",
|
||||
"https://metacubex.github.io",
|
||||
"https://board.zash.run.place",
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
config.insert("external-controller-cors".into(), cors_map.into());
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
|
||||
@@ -52,20 +52,24 @@ impl Config {
|
||||
/// 初始化订阅
|
||||
pub async fn init_config() -> Result<()> {
|
||||
if Self::profiles()
|
||||
.data()
|
||||
.latest_ref()
|
||||
.get_item(&"Merge".to_string())
|
||||
.is_err()
|
||||
{
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
|
||||
Self::profiles().data().append_item(merge_item.clone())?;
|
||||
Self::profiles()
|
||||
.data_mut()
|
||||
.append_item(merge_item.clone())?;
|
||||
}
|
||||
if Self::profiles()
|
||||
.data()
|
||||
.latest_ref()
|
||||
.get_item(&"Script".to_string())
|
||||
.is_err()
|
||||
{
|
||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||
Self::profiles().data().append_item(script_item.clone())?;
|
||||
Self::profiles()
|
||||
.data_mut()
|
||||
.append_item(script_item.clone())?;
|
||||
}
|
||||
// 生成运行时配置
|
||||
if let Err(err) = Self::generate().await {
|
||||
@@ -135,7 +139,7 @@ impl Config {
|
||||
};
|
||||
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let runtime = runtime.latest_ref();
|
||||
let config = runtime
|
||||
.config
|
||||
.as_ref()
|
||||
@@ -149,7 +153,7 @@ impl Config {
|
||||
pub async fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||
|
||||
*Config::runtime().draft() = Box::new(IRuntime {
|
||||
*Config::runtime().draft_mut() = Box::new(IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
|
||||
+115
-103
@@ -1,135 +1,147 @@
|
||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::{
|
||||
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard,
|
||||
RwLockUpgradableReadGuard, RwLockWriteGuard,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Draft<T: Clone + ToOwned> {
|
||||
inner: Arc<Mutex<(T, Option<T>)>>,
|
||||
inner: Arc<RwLock<(T, Option<T>)>>,
|
||||
}
|
||||
|
||||
macro_rules! draft_define {
|
||||
($id: ident) => {
|
||||
impl From<$id> for Draft<$id> {
|
||||
fn from(data: $id) -> Self {
|
||||
Draft {
|
||||
inner: Arc::new(Mutex::new((data, None))),
|
||||
}
|
||||
}
|
||||
impl<T: Clone + ToOwned> From<T> for Draft<T> {
|
||||
fn from(data: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new((data, None))),
|
||||
}
|
||||
|
||||
impl Draft<Box<$id>> {
|
||||
#[allow(unused)]
|
||||
pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||
}
|
||||
|
||||
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
&mut inner.0
|
||||
} else {
|
||||
inner.1.as_mut().unwrap()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
inner.1 = Some(inner.0.clone());
|
||||
}
|
||||
|
||||
inner.1.as_mut().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(&self) -> Option<Box<$id>> {
|
||||
let mut inner = self.inner.lock();
|
||||
|
||||
match inner.1.take() {
|
||||
Some(draft) => {
|
||||
let old_value = inner.0.to_owned();
|
||||
inner.0 = draft.to_owned();
|
||||
Some(old_value)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn discard(&self) -> Option<Box<$id>> {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.1.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<$id>> for Draft<Box<$id>> {
|
||||
fn from(data: Box<$id>) -> Self {
|
||||
Draft {
|
||||
inner: Arc::new(Mutex::new((data, None))),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// draft_define!(IClash);
|
||||
draft_define!(IClashTemp);
|
||||
draft_define!(IProfiles);
|
||||
draft_define!(IRuntime);
|
||||
draft_define!(IVerge);
|
||||
/// Implements draft management for `Box<T>`, allowing for safe concurrent editing and committing of draft data.
|
||||
/// # Type Parameters
|
||||
/// - `T`: The underlying data type, which must implement `Clone` and `ToOwned`.
|
||||
///
|
||||
/// # Methods
|
||||
/// - `data_mut`: Returns a mutable reference to the committed data.
|
||||
/// - `data_ref`: Returns an immutable reference to the committed data.
|
||||
/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists.
|
||||
/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data.
|
||||
/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed.
|
||||
/// - `discard`: Discards the draft data and returns it if it existed.
|
||||
impl<T: Clone + ToOwned> Draft<Box<T>> {
|
||||
/// 可写正式数据
|
||||
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
|
||||
}
|
||||
|
||||
/// 返回正式数据的只读视图(不包含草稿)
|
||||
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
|
||||
}
|
||||
|
||||
/// 创建或获取草稿并返回可写引用
|
||||
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
let guard = self.inner.upgradable_read();
|
||||
if guard.1.is_none() {
|
||||
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
|
||||
guard.1 = Some(guard.0.clone());
|
||||
return RwLockWriteGuard::map(guard, |inner| inner.1.as_mut().unwrap());
|
||||
}
|
||||
// 已存在草稿,升级为写锁映射
|
||||
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
|
||||
inner.1.as_mut().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
/// 零拷贝只读视图:返回草稿(若存在)或正式值
|
||||
pub fn latest_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| {
|
||||
inner.1.as_ref().unwrap_or(&inner.0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 提交草稿,返回旧正式数据
|
||||
pub fn apply(&self) -> Option<Box<T>> {
|
||||
let mut inner = self.inner.write();
|
||||
inner
|
||||
.1
|
||||
.take()
|
||||
.map(|draft| std::mem::replace(&mut inner.0, draft))
|
||||
}
|
||||
|
||||
/// 丢弃草稿,返回被丢弃的草稿
|
||||
pub fn discard(&self) -> Option<Box<T>> {
|
||||
self.inner.write().1.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draft_box() {
|
||||
use super::IVerge;
|
||||
|
||||
// 1. 创建 Draft<Box<IVerge>>
|
||||
let verge = Box::new(IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..IVerge::default()
|
||||
});
|
||||
|
||||
let draft = Draft::from(verge);
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||
|
||||
// 2. 读取正式数据(data_mut)
|
||||
{
|
||||
let mut d = draft.draft();
|
||||
let data = draft.data_mut();
|
||||
assert_eq!(data.enable_auto_launch, Some(true));
|
||||
assert_eq!(data.enable_tun_mode, Some(false));
|
||||
}
|
||||
|
||||
// 3. 初次获取草稿(draft_mut 会自动 clone 一份)
|
||||
{
|
||||
let draft_view = draft.draft_mut();
|
||||
assert_eq!(draft_view.enable_auto_launch, Some(true));
|
||||
assert_eq!(draft_view.enable_tun_mode, Some(false));
|
||||
}
|
||||
|
||||
// 4. 修改草稿
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
d.enable_tun_mode = Some(true);
|
||||
}
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||
|
||||
assert_eq!(draft.latest().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.latest().enable_tun_mode, Some(true));
|
||||
|
||||
assert!(draft.apply().is_some());
|
||||
assert!(draft.apply().is_none());
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(true));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||
// 正式数据未变
|
||||
assert_eq!(draft.data_mut().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data_mut().enable_tun_mode, Some(false));
|
||||
|
||||
// 草稿已变
|
||||
{
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(true);
|
||||
let latest = draft.latest_ref();
|
||||
assert_eq!(latest.enable_auto_launch, Some(false));
|
||||
assert_eq!(latest.enable_tun_mode, Some(true));
|
||||
}
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||
// 5. 提交草稿
|
||||
assert!(draft.apply().is_some()); // 第一次提交应有返回
|
||||
assert!(draft.apply().is_none()); // 第二次提交返回 None
|
||||
|
||||
assert!(draft.discard().is_some());
|
||||
// 正式数据已更新
|
||||
{
|
||||
let data = draft.data_mut();
|
||||
assert_eq!(data.enable_auto_launch, Some(false));
|
||||
assert_eq!(data.enable_tun_mode, Some(true));
|
||||
}
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
assert!(draft.discard().is_none());
|
||||
// 6. 新建并修改下一轮草稿
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(true);
|
||||
}
|
||||
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
// 7. 丢弃草稿
|
||||
assert!(draft.discard().is_some()); // 第一次丢弃返回 Some
|
||||
assert!(draft.discard().is_none()); // 再次丢弃返回 None
|
||||
|
||||
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone
|
||||
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Encrypt data
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce.as_slice().into(), data.as_bytes())
|
||||
.map_err(|e| format!("Encryption failed: {}", e))?;
|
||||
.map_err(|e| format!("Encryption failed: {e}"))?;
|
||||
|
||||
// Concatenate nonce and ciphertext and encode them in base64
|
||||
let mut combined = nonce;
|
||||
@@ -46,7 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
|
||||
// Decrypt data
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce.into(), ciphertext)
|
||||
.map_err(|e| format!("Decryption failed: {}", e))?;
|
||||
.map_err(|e| format!("Decryption failed: {e}"))?;
|
||||
|
||||
String::from_utf8(plaintext).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
@@ -186,29 +186,37 @@ impl PrfItem {
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles().data().append_item(merge_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(merge_item.clone())?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles().data().append_item(script_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(script_item.clone())?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles().data().append_item(rules_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(rules_item.clone())?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data()
|
||||
.data_mut()
|
||||
.append_item(proxies_item.clone())?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles().data().append_item(groups_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(groups_item.clone())?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
Ok(PrfItem {
|
||||
@@ -366,29 +374,37 @@ impl PrfItem {
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles().data().append_item(merge_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(merge_item.clone())?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles().data().append_item(script_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(script_item.clone())?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles().data().append_item(rules_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(rules_item.clone())?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data()
|
||||
.data_mut()
|
||||
.append_item(proxies_item.clone())?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles().data().append_item(groups_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(groups_item.clone())?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
|
||||
|
||||
@@ -131,9 +131,9 @@ impl IProfiles {
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
}
|
||||
|
||||
if self.current.is_none()
|
||||
@@ -234,9 +234,9 @@ impl IProfiles {
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -531,7 +531,7 @@ impl IProfiles {
|
||||
if Self::is_profile_file(file_name) {
|
||||
// 检查是否为全局扩展文件
|
||||
if protected_files.contains(file_name) {
|
||||
log::debug!(target: "app", "保护全局扩展配置文件: {}", file_name);
|
||||
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -540,11 +540,11 @@ impl IProfiles {
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(_) => {
|
||||
deleted_files.push(file_name.to_string());
|
||||
log::info!(target: "app", "已清理冗余文件: {}", file_name);
|
||||
log::info!(target: "app", "已清理冗余文件: {file_name}");
|
||||
}
|
||||
Err(e) => {
|
||||
failed_deletions.push(format!("{}: {}", file_name, e));
|
||||
log::warn!(target: "app", "清理文件失败: {} - {}", file_name, e);
|
||||
failed_deletions.push(format!("{file_name}: {e}"));
|
||||
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -681,7 +681,7 @@ impl IProfiles {
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "自动清理失败: {}", e);
|
||||
log::warn!(target: "app", "自动清理失败: {e}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ impl IVerge {
|
||||
use crate::config::Config;
|
||||
|
||||
let config_draft = Config::verge();
|
||||
*config_draft.draft() = Box::new(updated_config.clone());
|
||||
*config_draft.draft_mut() = Box::new(updated_config.clone());
|
||||
config_draft.apply();
|
||||
|
||||
logging!(
|
||||
@@ -391,7 +391,7 @@ impl IVerge {
|
||||
auto_close_connection: Some(true),
|
||||
auto_check_update: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
auto_log_clean: Some(3),
|
||||
auto_log_clean: Some(2),
|
||||
webdav_url: None,
|
||||
webdav_username: None,
|
||||
webdav_password: None,
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use anyhow::anyhow;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct AsyncAutoproxy {
|
||||
pub enable: bool,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AsyncSysproxy {
|
||||
pub enable: bool,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub bypass: String,
|
||||
}
|
||||
|
||||
impl Default for AsyncSysproxy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable: false,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 7890,
|
||||
bypass: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AsyncProxyQuery;
|
||||
|
||||
impl AsyncProxyQuery {
|
||||
/// 异步获取自动代理配置(PAC)
|
||||
pub async fn get_auto_proxy() -> AsyncAutoproxy {
|
||||
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
|
||||
Ok(Ok(proxy)) => {
|
||||
log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "异步获取自动代理失败: {e}");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "异步获取自动代理超时");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步获取系统代理配置
|
||||
pub async fn get_system_proxy() -> AsyncSysproxy {
|
||||
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
|
||||
Ok(Ok(proxy)) => {
|
||||
log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "异步获取系统代理失败: {e}");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "异步获取系统代理超时");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||
// Windows: 从注册表读取PAC配置
|
||||
tokio::task::spawn_blocking(move || -> Result<AsyncAutoproxy> {
|
||||
Self::get_pac_config_from_registry()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_pac_config_from_registry() -> Result<AsyncAutoproxy> {
|
||||
use std::ptr;
|
||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
||||
|
||||
unsafe {
|
||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||
.encode_utf16()
|
||||
.collect::<Vec<u16>>();
|
||||
|
||||
let mut hkey: HKEY = ptr::null_mut();
|
||||
let result =
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||
|
||||
if result != 0 {
|
||||
log::debug!(target: "app", "无法打开注册表项");
|
||||
return Ok(AsyncAutoproxy::default());
|
||||
}
|
||||
|
||||
// 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用)
|
||||
let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut url_buffer = vec![0u16; 1024];
|
||||
let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD;
|
||||
let mut url_value_type: DWORD = 0;
|
||||
|
||||
let url_query_result = RegQueryValueExW(
|
||||
hkey,
|
||||
auto_config_url_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut url_value_type,
|
||||
url_buffer.as_mut_ptr() as *mut u8,
|
||||
&mut url_buffer_size,
|
||||
);
|
||||
|
||||
let mut pac_url = String::new();
|
||||
if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 {
|
||||
let end_pos = url_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(url_buffer.len());
|
||||
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
|
||||
log::debug!(target: "app", "从注册表读取到PAC URL: {pac_url}");
|
||||
}
|
||||
|
||||
// 2. 检查自动检测设置是否启用
|
||||
let auto_detect_name = "AutoDetect\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut auto_detect: DWORD = 0;
|
||||
let mut detect_buffer_size: DWORD = 4;
|
||||
let mut detect_value_type: DWORD = 0;
|
||||
|
||||
let detect_query_result = RegQueryValueExW(
|
||||
hkey,
|
||||
auto_detect_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut detect_value_type,
|
||||
&mut auto_detect as *mut DWORD as *mut u8,
|
||||
&mut detect_buffer_size,
|
||||
);
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
// PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用
|
||||
let pac_enabled = !pac_url.is_empty()
|
||||
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
|
||||
|
||||
if pac_enabled {
|
||||
log::debug!(target: "app", "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}");
|
||||
|
||||
if pac_url.is_empty() && auto_detect != 0 {
|
||||
pac_url = "auto-detect".to_string();
|
||||
}
|
||||
|
||||
Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: pac_url,
|
||||
})
|
||||
} else {
|
||||
log::debug!(target: "app", "PAC配置未启用");
|
||||
Ok(AsyncAutoproxy::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||
// macOS: 使用 scutil --proxy 命令
|
||||
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(AsyncAutoproxy::default());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
log::debug!(target: "app", "scutil output: {stdout}");
|
||||
|
||||
let mut pac_enabled = false;
|
||||
let mut pac_url = String::new();
|
||||
|
||||
// 解析 scutil 输出
|
||||
for line in stdout.lines() {
|
||||
let line = line.trim();
|
||||
if line.contains("ProxyAutoConfigEnable") && line.contains("1") {
|
||||
pac_enabled = true;
|
||||
} else if line.contains("ProxyAutoConfigURLString") {
|
||||
// 正确解析包含冒号的URL
|
||||
// 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac"
|
||||
if let Some(colon_pos) = line.find(" : ") {
|
||||
pac_url = line[colon_pos + 3..].trim().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
|
||||
|
||||
Ok(AsyncAutoproxy {
|
||||
enable: pac_enabled && !pac_url.is_empty(),
|
||||
url: pac_url,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||
// Linux: 检查环境变量和GNOME设置
|
||||
|
||||
// 首先检查环境变量
|
||||
if let Ok(auto_proxy) = std::env::var("auto_proxy") {
|
||||
if !auto_proxy.is_empty() {
|
||||
return Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: auto_proxy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||
let output = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy", "mode"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(output) = output {
|
||||
if output.status.success() {
|
||||
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if mode.contains("auto") {
|
||||
// 获取 PAC URL
|
||||
let pac_output = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(pac_output) = pac_output {
|
||||
if pac_output.status.success() {
|
||||
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
|
||||
if !pac_url.is_empty() {
|
||||
return Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: pac_url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AsyncAutoproxy::default())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||
// Windows: 使用注册表直接读取代理设置
|
||||
tokio::task::spawn_blocking(move || -> Result<AsyncSysproxy> {
|
||||
Self::get_system_proxy_from_registry()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_system_proxy_from_registry() -> Result<AsyncSysproxy> {
|
||||
use std::ptr;
|
||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
||||
|
||||
unsafe {
|
||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||
.encode_utf16()
|
||||
.collect::<Vec<u16>>();
|
||||
|
||||
let mut hkey: HKEY = ptr::null_mut();
|
||||
let result =
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||
|
||||
if result != 0 {
|
||||
return Ok(AsyncSysproxy::default());
|
||||
}
|
||||
|
||||
// 检查代理是否启用
|
||||
let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut proxy_enable: DWORD = 0;
|
||||
let mut buffer_size: DWORD = 4;
|
||||
let mut value_type: DWORD = 0;
|
||||
|
||||
let enable_result = RegQueryValueExW(
|
||||
hkey,
|
||||
proxy_enable_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut value_type,
|
||||
&mut proxy_enable as *mut DWORD as *mut u8,
|
||||
&mut buffer_size,
|
||||
);
|
||||
|
||||
if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 {
|
||||
RegCloseKey(hkey);
|
||||
return Ok(AsyncSysproxy::default());
|
||||
}
|
||||
|
||||
// 读取代理服务器设置
|
||||
let proxy_server_name = "ProxyServer\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut buffer = vec![0u16; 1024];
|
||||
let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD;
|
||||
let mut value_type: DWORD = 0;
|
||||
|
||||
let server_result = RegQueryValueExW(
|
||||
hkey,
|
||||
proxy_server_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut value_type,
|
||||
buffer.as_mut_ptr() as *mut u8,
|
||||
&mut buffer_size,
|
||||
);
|
||||
|
||||
let mut proxy_server = String::new();
|
||||
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
||||
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
|
||||
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
|
||||
}
|
||||
|
||||
// 读取代理绕过列表
|
||||
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut bypass_buffer = vec![0u16; 1024];
|
||||
let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD;
|
||||
let mut bypass_value_type: DWORD = 0;
|
||||
|
||||
let override_result = RegQueryValueExW(
|
||||
hkey,
|
||||
proxy_override_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut bypass_value_type,
|
||||
bypass_buffer.as_mut_ptr() as *mut u8,
|
||||
&mut bypass_buffer_size,
|
||||
);
|
||||
|
||||
let mut bypass_list = String::new();
|
||||
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
|
||||
let end_pos = bypass_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(bypass_buffer.len());
|
||||
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
|
||||
}
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
if !proxy_server.is_empty() {
|
||||
// 解析服务器地址和端口
|
||||
let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') {
|
||||
let host = proxy_server[..colon_pos].to_string();
|
||||
let port = proxy_server[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||
(host, port)
|
||||
} else {
|
||||
(proxy_server, 8080)
|
||||
};
|
||||
|
||||
log::debug!(target: "app", "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}");
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: bypass_list,
|
||||
})
|
||||
} else {
|
||||
Ok(AsyncSysproxy::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(AsyncSysproxy::default());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
log::debug!(target: "app", "scutil proxy output: {stdout}");
|
||||
|
||||
let mut http_enabled = false;
|
||||
let mut http_host = String::new();
|
||||
let mut http_port = 8080u16;
|
||||
let mut exceptions = Vec::new();
|
||||
|
||||
for line in stdout.lines() {
|
||||
let line = line.trim();
|
||||
if line.contains("HTTPEnable") && line.contains("1") {
|
||||
http_enabled = true;
|
||||
} else if line.contains("HTTPProxy") && !line.contains("Port") {
|
||||
if let Some(host_part) = line.split(':').nth(1) {
|
||||
http_host = host_part.trim().to_string();
|
||||
}
|
||||
} else if line.contains("HTTPPort") {
|
||||
if let Some(port_part) = line.split(':').nth(1) {
|
||||
if let Ok(port) = port_part.trim().parse::<u16>() {
|
||||
http_port = port;
|
||||
}
|
||||
}
|
||||
} else if line.contains("ExceptionsList") {
|
||||
// 解析异常列表
|
||||
if let Some(list_part) = line.split(':').nth(1) {
|
||||
let list = list_part.trim();
|
||||
if !list.is_empty() {
|
||||
exceptions.push(list.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: http_enabled && !http_host.is_empty(),
|
||||
host: http_host,
|
||||
port: http_port,
|
||||
bypass: exceptions.join(","),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||
// Linux: 检查环境变量和桌面环境设置
|
||||
|
||||
// 首先检查环境变量
|
||||
if let Ok(http_proxy) = std::env::var("http_proxy") {
|
||||
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) {
|
||||
return Ok(proxy_info);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(https_proxy) = std::env::var("https_proxy") {
|
||||
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) {
|
||||
return Ok(proxy_info);
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||
let mode_output = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy", "mode"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(mode_output) = mode_output {
|
||||
if mode_output.status.success() {
|
||||
let mode = String::from_utf8_lossy(&mode_output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
if mode.contains("manual") {
|
||||
// 获取HTTP代理设置
|
||||
let host_result = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy.http", "host"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let port_result = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy.http", "port"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) {
|
||||
if host_output.status.success() && port_output.status.success() {
|
||||
let host = String::from_utf8_lossy(&host_output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
|
||||
let port = String::from_utf8_lossy(&port_output.stdout)
|
||||
.trim()
|
||||
.parse::<u16>()
|
||||
.unwrap_or(8080);
|
||||
|
||||
if !host.is_empty() {
|
||||
return Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy::default())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn parse_proxy_url(proxy_url: &str) -> Result<AsyncSysproxy> {
|
||||
// 解析形如 "http://proxy.example.com:8080" 的URL
|
||||
let url = proxy_url.trim();
|
||||
|
||||
// 移除协议前缀
|
||||
let url = if let Some(stripped) = url.strip_prefix("http://") {
|
||||
stripped
|
||||
} else if let Some(stripped) = url.strip_prefix("https://") {
|
||||
stripped
|
||||
} else {
|
||||
url
|
||||
};
|
||||
|
||||
// 解析主机和端口
|
||||
let (host, port) = if let Some(colon_pos) = url.rfind(':') {
|
||||
let host = url[..colon_pos].to_string();
|
||||
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||
(host, port)
|
||||
} else {
|
||||
(url.to_string(), 8080)
|
||||
};
|
||||
|
||||
if host.is_empty() {
|
||||
return Err(anyhow!("无效的代理URL"));
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: std::env::var("no_proxy").unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ impl WebDavClient {
|
||||
if let Some(cfg) = lock.as_ref() {
|
||||
cfg.clone()
|
||||
} else {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
if verge.webdav_url.is_none()
|
||||
|| verge.webdav_username.is_none()
|
||||
|| verge.webdav_password.is_none()
|
||||
@@ -108,10 +108,7 @@ impl WebDavClient {
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(op.timeout()))
|
||||
.user_agent(format!(
|
||||
"clash-verge/{} ({} WebDAV-Client)",
|
||||
APP_VERSION, OS
|
||||
))
|
||||
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
|
||||
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
||||
// 允许所有请求类型的重定向,包括PUT
|
||||
if attempt.previous().len() >= 5 {
|
||||
@@ -177,7 +174,7 @@ impl WebDavClient {
|
||||
}
|
||||
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("Upload failed, retrying once: {}", e);
|
||||
log::warn!("Upload failed, retrying once: {e}");
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
timeout(
|
||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||
@@ -237,7 +234,7 @@ impl WebDavClient {
|
||||
|
||||
pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
|
||||
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
|
||||
let zip_file_name = format!("{OS}-backup-{now}.zip");
|
||||
let zip_path = temp_dir().join(&zip_file_name);
|
||||
|
||||
let file = fs::File::create(&zip_path)?;
|
||||
|
||||
+330
-21
@@ -1,5 +1,3 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::core::tray::Tray;
|
||||
use crate::{
|
||||
config::*,
|
||||
core::{
|
||||
@@ -140,14 +138,14 @@ impl CoreManager {
|
||||
/// 使用默认配置
|
||||
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||
*Config::runtime().draft() = Box::new(IRuntime {
|
||||
config: Some(Config::clash().latest().0.clone()),
|
||||
*Config::runtime().draft_mut() = Box::new(IRuntime {
|
||||
config: Some(Config::clash().latest_ref().0.clone()),
|
||||
exists_keys: vec![],
|
||||
chain_logs: Default::default(),
|
||||
});
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
&Config::clash().latest_ref().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
)?;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
@@ -174,7 +172,7 @@ impl CoreManager {
|
||||
|
||||
// 检查文件是否存在
|
||||
if !std::path::Path::new(config_path).exists() {
|
||||
let error_msg = format!("File not found: {}", config_path);
|
||||
let error_msg = format!("File not found: {config_path}");
|
||||
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
@@ -249,7 +247,7 @@ impl CoreManager {
|
||||
config_path
|
||||
);
|
||||
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
|
||||
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
@@ -286,7 +284,7 @@ impl CoreManager {
|
||||
} else if !stderr.is_empty() {
|
||||
stderr.to_string()
|
||||
} else if let Some(code) = output.status.code() {
|
||||
format!("验证进程异常退出,退出码: {}", code)
|
||||
format!("验证进程异常退出,退出码: {code}")
|
||||
} else {
|
||||
"验证进程被终止".to_string()
|
||||
};
|
||||
@@ -307,7 +305,7 @@ impl CoreManager {
|
||||
let content = match std::fs::read_to_string(config_path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read file: {}", err);
|
||||
let error_msg = format!("Failed to read file: {err}");
|
||||
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
@@ -321,7 +319,7 @@ impl CoreManager {
|
||||
}
|
||||
Err(err) => {
|
||||
// 使用标准化的前缀,以便错误处理函数能正确识别
|
||||
let error_msg = format!("YAML syntax error: {}", err);
|
||||
let error_msg = format!("YAML syntax error: {err}");
|
||||
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
@@ -333,7 +331,7 @@ impl CoreManager {
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read script file: {}", err);
|
||||
let error_msg = format!("Failed to read script file: {err}");
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
@@ -366,7 +364,7 @@ impl CoreManager {
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Script syntax error: {}", err);
|
||||
let error_msg = format!("Script syntax error: {err}");
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
Ok((false, error_msg))
|
||||
@@ -435,13 +433,313 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
/// 清理多余的 mihomo 进程
|
||||
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
|
||||
logging!(info, Type::Core, true, "开始清理多余的 mihomo 进程");
|
||||
|
||||
// 获取当前管理的进程 PID
|
||||
let current_pid = {
|
||||
let child_guard = self.child_sidecar.lock().await;
|
||||
child_guard.as_ref().map(|child| child.pid())
|
||||
};
|
||||
|
||||
let target_processes = ["verge-mihomo", "verge-mihomo-alpha"];
|
||||
|
||||
// 并行查找所有目标进程
|
||||
let mut process_futures = Vec::new();
|
||||
for &target in &target_processes {
|
||||
let process_name = if cfg!(windows) {
|
||||
format!("{target}.exe")
|
||||
} else {
|
||||
target.to_string()
|
||||
};
|
||||
process_futures.push(self.find_processes_by_name(process_name, target));
|
||||
}
|
||||
|
||||
let process_results = futures::future::join_all(process_futures).await;
|
||||
|
||||
// 收集所有需要终止的进程PID
|
||||
let mut pids_to_kill = Vec::new();
|
||||
for result in process_results {
|
||||
match result {
|
||||
Ok((pids, process_name)) => {
|
||||
for pid in pids {
|
||||
// 跳过当前管理的进程
|
||||
if let Some(current) = current_pid {
|
||||
if pid == current {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
true,
|
||||
"跳过当前管理的进程: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pids_to_kill.push((pid, process_name.clone()));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(debug, Type::Core, true, "查找进程时发生错误: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pids_to_kill.is_empty() {
|
||||
logging!(debug, Type::Core, true, "未发现多余的 mihomo 进程");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut kill_futures = Vec::new();
|
||||
for (pid, process_name) in &pids_to_kill {
|
||||
kill_futures.push(self.kill_process_with_verification(*pid, process_name.clone()));
|
||||
}
|
||||
|
||||
let kill_results = futures::future::join_all(kill_futures).await;
|
||||
|
||||
let killed_count = kill_results.into_iter().filter(|&success| success).count();
|
||||
|
||||
if killed_count > 0 {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"清理完成,共终止了 {} 个多余的 mihomo 进程",
|
||||
killed_count
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 根据进程名查找进程PID列
|
||||
async fn find_processes_by_name(
|
||||
&self,
|
||||
process_name: String,
|
||||
_target: &str,
|
||||
) -> Result<(Vec<u32>, String)> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::mem;
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::um::tlhelp32::{
|
||||
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
||||
TH32CS_SNAPPROCESS,
|
||||
};
|
||||
use winapi::um::winnt::HANDLE;
|
||||
|
||||
let process_name_clone = process_name.clone();
|
||||
let pids = tokio::task::spawn_blocking(move || -> Result<Vec<u32>> {
|
||||
let mut pids = Vec::new();
|
||||
|
||||
unsafe {
|
||||
// 创建进程快照
|
||||
let snapshot: HANDLE = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
|
||||
return Err(anyhow::anyhow!("Failed to create process snapshot"));
|
||||
}
|
||||
|
||||
let mut pe32: PROCESSENTRY32W = mem::zeroed();
|
||||
pe32.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
|
||||
|
||||
// 获取第一个进程
|
||||
if Process32FirstW(snapshot, &mut pe32) != 0 {
|
||||
loop {
|
||||
// 将宽字符转换为String
|
||||
let end_pos = pe32
|
||||
.szExeFile
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(pe32.szExeFile.len());
|
||||
let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]);
|
||||
|
||||
// 检查进程名是否匹配
|
||||
if exe_file.eq_ignore_ascii_case(&process_name_clone) {
|
||||
pids.push(pe32.th32ProcessID);
|
||||
}
|
||||
if Process32NextW(snapshot, &mut pe32) == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭句柄
|
||||
CloseHandle(snapshot);
|
||||
}
|
||||
|
||||
Ok(pids)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok((pids, process_name))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let output = if cfg!(target_os = "macos") {
|
||||
tokio::process::Command::new("pgrep")
|
||||
.arg(&process_name)
|
||||
.output()
|
||||
.await?
|
||||
} else {
|
||||
// Linux
|
||||
tokio::process::Command::new("pidof")
|
||||
.arg(&process_name)
|
||||
.output()
|
||||
.await?
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok((Vec::new(), process_name));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut pids = Vec::new();
|
||||
|
||||
// Unix系统直接解析PID列表
|
||||
for pid_str in stdout.split_whitespace() {
|
||||
if let Ok(pid) = pid_str.parse::<u32>() {
|
||||
pids.push(pid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((pids, process_name))
|
||||
}
|
||||
}
|
||||
|
||||
/// 终止进程并验证结果 - 使用Windows API直接终止,更优雅高效
|
||||
async fn kill_process_with_verification(&self, pid: u32, process_name: String) -> bool {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"尝试终止进程: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
|
||||
#[cfg(windows)]
|
||||
let success = {
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
|
||||
use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE};
|
||||
|
||||
tokio::task::spawn_blocking(move || -> bool {
|
||||
unsafe {
|
||||
let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
|
||||
if process_handle.is_null() {
|
||||
return false;
|
||||
}
|
||||
let result = TerminateProcess(process_handle, 1);
|
||||
CloseHandle(process_handle);
|
||||
|
||||
result != 0
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let success = {
|
||||
tokio::process::Command::new("kill")
|
||||
.args(["-9", &pid.to_string()])
|
||||
.output()
|
||||
.await
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if success {
|
||||
// 短暂等待并验证进程是否真正终止
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
let still_running = self.is_process_running(pid).await.unwrap_or(false);
|
||||
if still_running {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"进程 {} (PID: {}) 终止命令成功但进程仍在运行",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
false
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"成功终止进程: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
true
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"无法终止进程: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows API检查进程
|
||||
async fn is_process_running(&self, pid: u32) -> Result<bool> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use winapi::shared::minwindef::DWORD;
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::um::processthreadsapi::GetExitCodeProcess;
|
||||
use winapi::um::processthreadsapi::OpenProcess;
|
||||
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<bool> {
|
||||
unsafe {
|
||||
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
|
||||
if process_handle.is_null() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut exit_code: DWORD = 0;
|
||||
let result = GetExitCodeProcess(process_handle, &mut exit_code);
|
||||
CloseHandle(process_handle);
|
||||
|
||||
if result == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(exit_code == 259)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let output = tokio::process::Command::new("ps")
|
||||
.args(["-p", &pid.to_string()])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
Ok(output.status.success() && !output.stdout.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_core_by_sidecar(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, true, "Running core by sidecar");
|
||||
let config_file = &Config::generate_file(ConfigType::Run)?;
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
|
||||
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
|
||||
@@ -450,7 +748,7 @@ impl CoreManager {
|
||||
let now = Local::now();
|
||||
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
|
||||
|
||||
let log_path = service_log_dir.join(format!("sidecar_{}.log", timestamp));
|
||||
let log_path = service_log_dir.join(format!("sidecar_{timestamp}.log"));
|
||||
|
||||
let mut log_file = File::create(log_path)?;
|
||||
|
||||
@@ -566,7 +864,7 @@ impl CoreManager {
|
||||
let mut state = service::ServiceState::get();
|
||||
if !state.prefer_sidecar {
|
||||
state.prefer_sidecar = true;
|
||||
state.last_error = Some(format!("通过服务启动核心失败: {}", e));
|
||||
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
|
||||
if let Err(save_err) = state.save() {
|
||||
logging!(
|
||||
error,
|
||||
@@ -585,6 +883,17 @@ impl CoreManager {
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, "Initializing core");
|
||||
|
||||
// 应用启动时先清理任何遗留的 mihomo 进程
|
||||
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"应用初始化时清理多余 mihomo 进程失败: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
let mut core_started_successfully = false;
|
||||
|
||||
if service::is_service_available().await.is_ok() {
|
||||
@@ -732,9 +1041,8 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
logging!(trace, Type::Core, "Initied core logic completed");
|
||||
#[cfg(target_os = "macos")]
|
||||
logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
|
||||
|
||||
// #[cfg(target_os = "macos")]
|
||||
// logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -787,6 +1095,7 @@ impl CoreManager {
|
||||
/// 重启内核
|
||||
pub async fn restart_core(&self) -> Result<()> {
|
||||
self.stop_core().await?;
|
||||
|
||||
self.start_core().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -800,14 +1109,14 @@ impl CoreManager {
|
||||
}
|
||||
let core: &str = &clash_core.clone().unwrap();
|
||||
if !IVerge::VALID_CLASH_CORES.contains(&core) {
|
||||
let error_message = format!("Clash core invalid name: {}", core);
|
||||
let error_message = format!("Clash core invalid name: {core}");
|
||||
logging!(error, Type::Core, true, "{}", error_message);
|
||||
return Err(error_message);
|
||||
}
|
||||
|
||||
Config::verge().draft().clash_core = clash_core.clone();
|
||||
Config::verge().draft_mut().clash_core = clash_core.clone();
|
||||
Config::verge().apply();
|
||||
logging_error!(Type::Core, true, Config::verge().latest().save_file());
|
||||
logging_error!(Type::Core, true, Config::verge().latest_ref().save_file());
|
||||
|
||||
let run_path = Config::generate_file(ConfigType::Run).map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::time::{sleep, timeout, Duration};
|
||||
|
||||
use crate::config::{Config, IVerge};
|
||||
use crate::core::async_proxy_query::AsyncProxyQuery;
|
||||
use crate::logging_error;
|
||||
use crate::utils::logging::Type;
|
||||
use once_cell::sync::Lazy;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProxyEvent {
|
||||
/// 配置变更事件
|
||||
ConfigChanged,
|
||||
/// 强制检查代理状态
|
||||
#[allow(dead_code)]
|
||||
ForceCheck,
|
||||
/// 启用系统代理
|
||||
#[allow(dead_code)]
|
||||
EnableProxy,
|
||||
/// 禁用系统代理
|
||||
#[allow(dead_code)]
|
||||
DisableProxy,
|
||||
/// 切换到PAC模式
|
||||
#[allow(dead_code)]
|
||||
SwitchToPac,
|
||||
/// 切换到HTTP代理模式
|
||||
#[allow(dead_code)]
|
||||
SwitchToHttp,
|
||||
/// 应用启动事件
|
||||
AppStarted,
|
||||
/// 应用关闭事件
|
||||
#[allow(dead_code)]
|
||||
AppStopping,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyState {
|
||||
pub sys_enabled: bool,
|
||||
pub pac_enabled: bool,
|
||||
pub auto_proxy: Autoproxy,
|
||||
pub sys_proxy: Sysproxy,
|
||||
pub last_updated: std::time::Instant,
|
||||
pub is_healthy: bool,
|
||||
}
|
||||
|
||||
impl Default for ProxyState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sys_enabled: false,
|
||||
pac_enabled: false,
|
||||
auto_proxy: Autoproxy {
|
||||
enable: false,
|
||||
url: "".to_string(),
|
||||
},
|
||||
sys_proxy: Sysproxy {
|
||||
enable: false,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 7890,
|
||||
bypass: "".to_string(),
|
||||
},
|
||||
last_updated: std::time::Instant::now(),
|
||||
is_healthy: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventDrivenProxyManager {
|
||||
state: Arc<RwLock<ProxyState>>,
|
||||
event_sender: mpsc::UnboundedSender<ProxyEvent>,
|
||||
query_sender: mpsc::UnboundedSender<QueryRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct QueryRequest {
|
||||
response_tx: oneshot::Sender<Autoproxy>,
|
||||
}
|
||||
|
||||
// 配置结构体移到外部
|
||||
struct ProxyConfig {
|
||||
sys_enabled: bool,
|
||||
pac_enabled: bool,
|
||||
guard_enabled: bool,
|
||||
}
|
||||
|
||||
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
|
||||
|
||||
impl EventDrivenProxyManager {
|
||||
pub fn global() -> &'static EventDrivenProxyManager {
|
||||
&PROXY_MANAGER
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
let state = Arc::new(RwLock::new(ProxyState::default()));
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
let (query_tx, query_rx) = mpsc::unbounded_channel();
|
||||
|
||||
Self::start_event_loop(state.clone(), event_rx, query_rx);
|
||||
|
||||
Self {
|
||||
state,
|
||||
event_sender: event_tx,
|
||||
query_sender: query_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取自动代理配置(缓存)
|
||||
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
|
||||
self.state.read().auto_proxy.clone()
|
||||
}
|
||||
|
||||
/// 异步获取最新的自动代理配置
|
||||
pub async fn get_auto_proxy_async(&self) -> Autoproxy {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let query = QueryRequest { response_tx: tx };
|
||||
|
||||
if self.query_sender.send(query).is_err() {
|
||||
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
|
||||
return self.get_auto_proxy_cached();
|
||||
}
|
||||
|
||||
match timeout(Duration::from_secs(5), rx).await {
|
||||
Ok(Ok(result)) => result,
|
||||
_ => {
|
||||
log::warn!(target: "app", "查询超时,返回缓存数据");
|
||||
self.get_auto_proxy_cached()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知配置变更
|
||||
pub fn notify_config_changed(&self) {
|
||||
self.send_event(ProxyEvent::ConfigChanged);
|
||||
}
|
||||
|
||||
/// 通知应用启动
|
||||
pub fn notify_app_started(&self) {
|
||||
self.send_event(ProxyEvent::AppStarted);
|
||||
}
|
||||
|
||||
/// 通知应用即将关闭
|
||||
#[allow(dead_code)]
|
||||
pub fn notify_app_stopping(&self) {
|
||||
self.send_event(ProxyEvent::AppStopping);
|
||||
}
|
||||
|
||||
/// 启用系统代理
|
||||
#[allow(dead_code)]
|
||||
pub fn enable_proxy(&self) {
|
||||
self.send_event(ProxyEvent::EnableProxy);
|
||||
}
|
||||
|
||||
/// 禁用系统代理
|
||||
#[allow(dead_code)]
|
||||
pub fn disable_proxy(&self) {
|
||||
self.send_event(ProxyEvent::DisableProxy);
|
||||
}
|
||||
|
||||
/// 强制检查代理状态
|
||||
#[allow(dead_code)]
|
||||
pub fn force_check(&self) {
|
||||
self.send_event(ProxyEvent::ForceCheck);
|
||||
}
|
||||
|
||||
fn send_event(&self, event: ProxyEvent) {
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
log::error!(target: "app", "发送代理事件失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn start_event_loop(
|
||||
state: Arc<RwLock<ProxyState>>,
|
||||
mut event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
|
||||
mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
log::info!(target: "app", "事件驱动代理管理器启动");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = event_rx.recv() => {
|
||||
match event {
|
||||
Some(event) => {
|
||||
log::debug!(target: "app", "处理代理事件: {event:?}");
|
||||
Self::handle_event(&state, event).await;
|
||||
}
|
||||
None => {
|
||||
log::info!(target: "app", "事件通道关闭,代理管理器停止");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
query = query_rx.recv() => {
|
||||
match query {
|
||||
Some(query) => {
|
||||
let result = Self::handle_query(&state).await;
|
||||
let _ = query.response_tx.send(result);
|
||||
}
|
||||
None => {
|
||||
log::info!(target: "app", "查询通道关闭");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
|
||||
match event {
|
||||
ProxyEvent::ConfigChanged | ProxyEvent::ForceCheck => {
|
||||
Self::update_proxy_config(state).await;
|
||||
}
|
||||
ProxyEvent::EnableProxy => {
|
||||
Self::enable_system_proxy(state).await;
|
||||
}
|
||||
ProxyEvent::DisableProxy => {
|
||||
Self::disable_system_proxy(state).await;
|
||||
}
|
||||
ProxyEvent::SwitchToPac => {
|
||||
Self::switch_proxy_mode(state, true).await;
|
||||
}
|
||||
ProxyEvent::SwitchToHttp => {
|
||||
Self::switch_proxy_mode(state, false).await;
|
||||
}
|
||||
ProxyEvent::AppStarted => {
|
||||
Self::initialize_proxy_state(state).await;
|
||||
}
|
||||
ProxyEvent::AppStopping => {
|
||||
log::info!(target: "app", "清理代理状态");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_query(state: &Arc<RwLock<ProxyState>>) -> Autoproxy {
|
||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.auto_proxy = auto_proxy.clone();
|
||||
});
|
||||
|
||||
auto_proxy
|
||||
}
|
||||
|
||||
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "初始化代理状态");
|
||||
|
||||
let config = Self::get_proxy_config();
|
||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||
let sys_proxy = Self::get_sys_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_enabled = config.sys_enabled;
|
||||
s.pac_enabled = config.pac_enabled;
|
||||
s.auto_proxy = auto_proxy;
|
||||
s.sys_proxy = sys_proxy;
|
||||
s.is_healthy = true;
|
||||
});
|
||||
|
||||
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
||||
}
|
||||
|
||||
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::debug!(target: "app", "更新代理配置");
|
||||
|
||||
let config = Self::get_proxy_config();
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_enabled = config.sys_enabled;
|
||||
s.pac_enabled = config.pac_enabled;
|
||||
});
|
||||
|
||||
if config.guard_enabled && config.sys_enabled {
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let (sys_enabled, pac_enabled) = {
|
||||
let s = state.read();
|
||||
(s.sys_enabled, s.pac_enabled)
|
||||
};
|
||||
|
||||
if !sys_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "检查代理状态");
|
||||
|
||||
if pac_enabled {
|
||||
Self::check_and_restore_pac_proxy(state).await;
|
||||
} else {
|
||||
Self::check_and_restore_sys_proxy(state).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let current = Self::get_auto_proxy_with_timeout().await;
|
||||
let expected = Self::get_expected_pac_config();
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.auto_proxy = current.clone();
|
||||
});
|
||||
|
||||
if !current.enable || current.url != expected.url {
|
||||
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
|
||||
Self::restore_pac_proxy(&expected.url).await;
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let restored = Self::get_auto_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.is_healthy = restored.enable && restored.url == expected.url;
|
||||
s.auto_proxy = restored;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let current = Self::get_sys_proxy_with_timeout().await;
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_proxy = current.clone();
|
||||
});
|
||||
|
||||
if !current.enable || current.host != expected.host || current.port != expected.port {
|
||||
log::info!(target: "app", "系统代理设置异常,正在恢复...");
|
||||
Self::restore_sys_proxy(&expected).await;
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let restored = Self::get_sys_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.is_healthy = restored.enable
|
||||
&& restored.host == expected.host
|
||||
&& restored.port == expected.port;
|
||||
s.sys_proxy = restored;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "启用系统代理");
|
||||
|
||||
let pac_enabled = state.read().pac_enabled;
|
||||
|
||||
if pac_enabled {
|
||||
let expected = Self::get_expected_pac_config();
|
||||
Self::restore_pac_proxy(&expected.url).await;
|
||||
} else {
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
Self::restore_sys_proxy(&expected).await;
|
||||
}
|
||||
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
}
|
||||
|
||||
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "禁用系统代理");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let disabled_sys = Sysproxy::default();
|
||||
let disabled_auto = Autoproxy::default();
|
||||
|
||||
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
||||
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
||||
}
|
||||
}
|
||||
|
||||
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
|
||||
log::info!(target: "app", "切换到{}模式", if to_pac { "PAC" } else { "HTTP代理" });
|
||||
|
||||
if to_pac {
|
||||
let disabled_sys = Sysproxy::default();
|
||||
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
||||
|
||||
let expected = Self::get_expected_pac_config();
|
||||
Self::restore_pac_proxy(&expected.url).await;
|
||||
} else {
|
||||
let disabled_auto = Autoproxy::default();
|
||||
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
||||
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
Self::restore_sys_proxy(&expected).await;
|
||||
}
|
||||
|
||||
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac);
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
}
|
||||
|
||||
async fn get_auto_proxy_with_timeout() -> Autoproxy {
|
||||
let async_proxy = AsyncProxyQuery::get_auto_proxy().await;
|
||||
|
||||
// 转换为兼容的结构
|
||||
Autoproxy {
|
||||
enable: async_proxy.enable,
|
||||
url: async_proxy.url,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_sys_proxy_with_timeout() -> Sysproxy {
|
||||
let async_proxy = AsyncProxyQuery::get_system_proxy().await;
|
||||
|
||||
// 转换为兼容的结构
|
||||
Sysproxy {
|
||||
enable: async_proxy.enable,
|
||||
host: async_proxy.host,
|
||||
port: async_proxy.port,
|
||||
bypass: async_proxy.bypass,
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的状态更新方法
|
||||
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
|
||||
where
|
||||
F: FnOnce(&mut ProxyState),
|
||||
{
|
||||
let mut state_guard = state.write();
|
||||
update_fn(&mut state_guard);
|
||||
state_guard.last_updated = std::time::Instant::now();
|
||||
}
|
||||
|
||||
fn get_proxy_config() -> ProxyConfig {
|
||||
let (sys_enabled, pac_enabled, guard_enabled) = {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest_ref();
|
||||
(
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.proxy_auto_config.unwrap_or(false),
|
||||
verge.enable_proxy_guard.unwrap_or(false),
|
||||
)
|
||||
};
|
||||
ProxyConfig {
|
||||
sys_enabled,
|
||||
pac_enabled,
|
||||
guard_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expected_pac_config() -> Autoproxy {
|
||||
let proxy_host = {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest_ref();
|
||||
verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||
};
|
||||
let pac_port = IVerge::get_singleton_port();
|
||||
Autoproxy {
|
||||
enable: true,
|
||||
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expected_sys_proxy() -> Sysproxy {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest_ref();
|
||||
let port = verge
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
let proxy_host = verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||
|
||||
Sysproxy {
|
||||
enable: true,
|
||||
host: proxy_host,
|
||||
port,
|
||||
bypass: Self::get_bypass_config(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bypass_config() -> String {
|
||||
let (use_default, custom_bypass) = {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest_ref();
|
||||
(
|
||||
verge.use_default_bypass.unwrap_or(true),
|
||||
verge.system_proxy_bypass.clone().unwrap_or_default(),
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let default_bypass = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let default_bypass =
|
||||
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let default_bypass = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
if custom_bypass.is_empty() {
|
||||
default_bypass.to_string()
|
||||
} else if use_default {
|
||||
format!("{default_bypass},{custom_bypass}")
|
||||
} else {
|
||||
custom_bypass
|
||||
}
|
||||
}
|
||||
|
||||
async fn restore_pac_proxy(expected_url: &str) {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let new_autoproxy = Autoproxy {
|
||||
enable: true,
|
||||
url: expected_url.to_string(),
|
||||
};
|
||||
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Self::execute_sysproxy_command(&["pac", expected_url]).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn restore_sys_proxy(expected: &Sysproxy) {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
logging_error!(Type::System, true, expected.set_system_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let address = format!("{}:{}", expected.host, expected.port);
|
||||
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn execute_sysproxy_command(args: &[&str]) {
|
||||
use crate::utils::dirs;
|
||||
#[allow(unused_imports)] // creation_flags必须
|
||||
use std::os::windows::process::CommandExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "获取服务路径失败: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe 不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
let output = Command::new(sysproxy_exe)
|
||||
.args(args)
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "执行sysproxy命令失败: {args:?}");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.is_empty() {
|
||||
log::error!(target: "app", "sysproxy错误输出: {stderr}");
|
||||
}
|
||||
} else {
|
||||
log::debug!(target: "app", "成功执行sysproxy命令: {args:?}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "执行sysproxy命令出错: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ impl NotificationSystem {
|
||||
match serde_json::to_value((status, message)) {
|
||||
Ok(p) => ("verge://notice-message", Ok(p)),
|
||||
Err(e) => {
|
||||
log::error!("Failed to serialize NoticeMessage payload: {}", e);
|
||||
log::error!("Failed to serialize NoticeMessage payload: {e}");
|
||||
("verge://notice-message", Err(e))
|
||||
}
|
||||
}
|
||||
@@ -153,11 +153,11 @@ impl NotificationSystem {
|
||||
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||
// 记录成功发送的事件
|
||||
if log::log_enabled!(log::Level::Debug) {
|
||||
log::debug!("Successfully emitted event: {}", event_name_str);
|
||||
log::debug!("Successfully emitted event: {event_name_str}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to emit event {}: {}", event_name_str, e);
|
||||
log::warn!("Failed to emit event {event_name_str}: {e}");
|
||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||
|
||||
@@ -165,8 +165,7 @@ impl NotificationSystem {
|
||||
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
||||
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
||||
log::warn!(
|
||||
"Reached {} emit errors, entering emergency mode",
|
||||
EMIT_ERROR_THRESHOLD
|
||||
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
|
||||
);
|
||||
*system.emergency_mode.write() = true;
|
||||
}
|
||||
@@ -175,7 +174,7 @@ impl NotificationSystem {
|
||||
} else {
|
||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||
log::warn!("Skipped emitting event due to payload serialization error for {}", event_name_str);
|
||||
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
|
||||
}
|
||||
} else {
|
||||
log::warn!("No window found, skipping event emit.");
|
||||
@@ -215,7 +214,7 @@ impl NotificationSystem {
|
||||
match sender.send(event) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to send event to notification queue: {:?}", e);
|
||||
log::warn!("Failed to send event to notification queue: {e:?}");
|
||||
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*self.stats.last_error_time.write() = Some(Instant::now());
|
||||
false
|
||||
@@ -243,7 +242,7 @@ impl NotificationSystem {
|
||||
log::info!("NotificationSystem worker thread joined successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("NotificationSystem worker thread join failed: {:?}", e);
|
||||
log::error!("NotificationSystem worker thread join failed: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,7 +499,7 @@ impl Handle {
|
||||
});
|
||||
|
||||
if let Err(e) = thread_result {
|
||||
log::error!("Failed to spawn startup errors thread: {}", e);
|
||||
log::error!("Failed to spawn startup errors thread: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+85
-100
@@ -1,10 +1,7 @@
|
||||
use crate::utils::notification::{notify_event, NotificationEvent};
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::handle,
|
||||
feat, logging, logging_error,
|
||||
module::lightweight::entry_lightweight_mode,
|
||||
process::AsyncHandler,
|
||||
utils::{logging::Type, resolve},
|
||||
config::Config, core::handle, feat, logging, logging_error,
|
||||
module::lightweight::entry_lightweight_mode, utils::logging::Type,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
@@ -14,7 +11,7 @@ use tauri::Manager;
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
current: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Hotkey {
|
||||
@@ -28,7 +25,7 @@ impl Hotkey {
|
||||
|
||||
pub fn init(&self) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
||||
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
@@ -38,12 +35,11 @@ impl Hotkey {
|
||||
enable_global_hotkey
|
||||
);
|
||||
|
||||
// 如果全局热键被禁用,则不注册热键
|
||||
if !enable_global_hotkey {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||
if let Some(hotkeys) = verge.latest_ref().hotkeys.as_ref() {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
@@ -138,14 +134,11 @@ impl Hotkey {
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
let app_handle_clone = app_handle.clone();
|
||||
let f: Box<dyn Fn() + Send + Sync> = match func.trim() {
|
||||
"open_or_close_dashboard" => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Registering open_or_close_dashboard function"
|
||||
);
|
||||
|| {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
@@ -153,94 +146,89 @@ impl Hotkey {
|
||||
"=== Hotkey Dashboard Window Operation Start ==="
|
||||
);
|
||||
|
||||
// 检查是否在轻量模式下,如果是,需要同步处理
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"In lightweight mode, calling open_or_close_dashboard directly"
|
||||
);
|
||||
crate::feat::open_or_close_dashboard();
|
||||
} else {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Toggle dashboard window visibility (async)"
|
||||
);
|
||||
logging!(
|
||||
info,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Using unified WindowManager for hotkey operation (bypass debounce)"
|
||||
);
|
||||
|
||||
// 检查窗口是否存在
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
// 如果窗口可见,则隐藏
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
if visible {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window is visible, hiding it"
|
||||
);
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
// 如果窗口不可见,则显示
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window is hidden, showing it"
|
||||
);
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"Failed to check window visibility: {}",
|
||||
e
|
||||
);
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果窗口不存在,创建一个新窗口
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window does not exist, creating a new one"
|
||||
);
|
||||
resolve::create_window(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
crate::feat::open_or_close_dashboard_hotkey();
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"=== Hotkey Dashboard Window Operation End ==="
|
||||
);
|
||||
}
|
||||
notify_event(&app_handle, NotificationEvent::DashboardToggled);
|
||||
})
|
||||
}
|
||||
"clash_mode_rule" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::change_clash_mode("rule".into());
|
||||
notify_event(
|
||||
&app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
||||
);
|
||||
})
|
||||
}
|
||||
"clash_mode_global" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::change_clash_mode("global".into());
|
||||
notify_event(
|
||||
&app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Global" },
|
||||
);
|
||||
})
|
||||
}
|
||||
"clash_mode_direct" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::change_clash_mode("direct".into());
|
||||
notify_event(
|
||||
&app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
||||
);
|
||||
})
|
||||
}
|
||||
"toggle_system_proxy" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::toggle_system_proxy();
|
||||
notify_event(&app_handle, NotificationEvent::SystemProxyToggled);
|
||||
})
|
||||
}
|
||||
"toggle_tun_mode" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::toggle_tun_mode(None);
|
||||
notify_event(&app_handle, NotificationEvent::TunModeToggled);
|
||||
})
|
||||
}
|
||||
"entry_lightweight_mode" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
entry_lightweight_mode();
|
||||
notify_event(&app_handle, NotificationEvent::LightweightModeEntered);
|
||||
})
|
||||
}
|
||||
"quit" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::quit();
|
||||
notify_event(&app_handle, NotificationEvent::AppQuit);
|
||||
})
|
||||
}
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
||||
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
||||
"quit" => || feat::quit(),
|
||||
#[cfg(target_os = "macos")]
|
||||
"hide" => || feat::hide(),
|
||||
|
||||
"hide" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::hide();
|
||||
notify_event(&app_handle, NotificationEvent::AppHidden);
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
||||
bail!("invalid function \"{func}\"");
|
||||
@@ -261,12 +249,10 @@ impl Hotkey {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接执行函数,不做任何状态检查
|
||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||
|
||||
// 获取全局热键状态
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
|
||||
@@ -274,7 +260,6 @@ impl Hotkey {
|
||||
f();
|
||||
} else {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||
let is_visible = WindowManager::is_main_window_visible();
|
||||
let is_focused = WindowManager::is_main_window_focused();
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod async_proxy_query;
|
||||
pub mod backup;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod core;
|
||||
pub mod event_driven_proxy;
|
||||
pub mod handle;
|
||||
pub mod hotkey;
|
||||
pub mod service;
|
||||
@@ -10,4 +12,4 @@ pub mod timer;
|
||||
pub mod tray;
|
||||
pub mod win_uwp;
|
||||
|
||||
pub use self::{core::*, timer::Timer};
|
||||
pub use self::{core::*, event_driven_proxy::EventDrivenProxyManager, timer::Timer};
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct ServiceState {
|
||||
impl ServiceState {
|
||||
// 获取当前的服务状态
|
||||
pub fn get() -> Self {
|
||||
if let Some(state) = Config::verge().latest().service_state.clone() {
|
||||
if let Some(state) = Config::verge().latest_ref().service_state.clone() {
|
||||
return state;
|
||||
}
|
||||
Self::default()
|
||||
@@ -41,11 +41,11 @@ impl ServiceState {
|
||||
// 保存服务状态
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let config = Config::verge();
|
||||
let mut latest = config.latest().clone();
|
||||
let mut latest = config.latest_ref().clone();
|
||||
latest.service_state = Some(self.clone());
|
||||
*config.draft() = latest;
|
||||
*config.draft_mut() = latest;
|
||||
config.apply();
|
||||
let result = config.latest().save_file();
|
||||
let result = config.latest_ref().save_file();
|
||||
result
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -346,7 +346,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -466,7 +466,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -686,7 +686,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
// 检查版本和可用性
|
||||
match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
|
||||
log::info!(target: "app", "服务版本检测:当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
|
||||
/* logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
@@ -698,8 +698,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
|
||||
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
||||
if needs_reinstall {
|
||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
|
||||
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装");
|
||||
|
||||
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
||||
@@ -717,7 +716,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
// 检查服务是否可用
|
||||
match is_service_available().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务正在运行但版本检查失败: {}", err);
|
||||
log::info!(target: "app", "服务正在运行但版本检查失败: {err}");
|
||||
/* logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
@@ -742,7 +741,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
|
||||
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
|
||||
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
|
||||
|
||||
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
|
||||
let clash_bin = format!("{clash_core}{bin_ext}");
|
||||
@@ -827,8 +826,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
// 先检查服务版本,不受冷却期限制
|
||||
let version_check = match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
log::info!(target: "app", "检测到服务版本: {version}, 要求版本: {REQUIRED_SERVICE_VERSION}");
|
||||
|
||||
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
||||
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
||||
@@ -839,7 +837,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "无法获取服务版本: {}", err);
|
||||
log::warn!(target: "app", "无法获取服务版本: {err}");
|
||||
false
|
||||
}
|
||||
};
|
||||
@@ -865,7 +863,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
|
||||
log::info!(target: "app", "开始重装服务");
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
log::warn!(target: "app", "服务重装失败: {err}");
|
||||
|
||||
log::info!(target: "app", "尝试使用现有服务");
|
||||
return start_with_existing_service(config_file).await;
|
||||
@@ -884,7 +882,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "服务检查失败: {}", err);
|
||||
log::warn!(target: "app", "服务检查失败: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,7 +891,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target: "app", "服务需要重装");
|
||||
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
log::warn!(target: "app", "服务重装失败: {err}");
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
|
||||
@@ -986,7 +984,7 @@ pub async fn force_reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "强制重装服务失败: {}", err);
|
||||
log::error!(target: "app", "强制重装服务失败: {err}");
|
||||
bail!("强制重装服务失败: {}", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ pub async fn send_ipc_request(
|
||||
|
||||
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
|
||||
|
||||
let command_type = format!("{:?}", command);
|
||||
let command_type = format!("{command:?}");
|
||||
|
||||
let request = match create_signed_request(command, payload) {
|
||||
Ok(req) => req,
|
||||
@@ -257,7 +257,7 @@ pub async fn send_ipc_request(
|
||||
|
||||
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
|
||||
|
||||
let command_type = format!("{:?}", command);
|
||||
let command_type = format!("{command:?}");
|
||||
|
||||
let request = match create_signed_request(command, payload) {
|
||||
Ok(req) => req,
|
||||
|
||||
+48
-189
@@ -2,25 +2,21 @@
|
||||
use crate::utils::autostart as startup_shortcut;
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle::Handle,
|
||||
core::{handle::Handle, EventDrivenProxyManager},
|
||||
logging, logging_error,
|
||||
process::AsyncHandler,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tauri::async_runtime::Mutex as TokioMutex;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub struct Sysopt {
|
||||
update_sysproxy: Arc<TokioMutex<bool>>,
|
||||
reset_sysproxy: Arc<TokioMutex<bool>>,
|
||||
/// record whether the guard async is running or not
|
||||
guard_state: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -33,10 +29,13 @@ static DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
fn get_bypass() -> String {
|
||||
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true);
|
||||
let use_default = Config::verge()
|
||||
.latest_ref()
|
||||
.use_default_bypass
|
||||
.unwrap_or(true);
|
||||
let res = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
verge.system_proxy_bypass.clone()
|
||||
};
|
||||
let custom_bypass = match res {
|
||||
@@ -47,7 +46,7 @@ fn get_bypass() -> String {
|
||||
if custom_bypass.is_empty() {
|
||||
DEFAULT_BYPASS.to_string()
|
||||
} else if use_default {
|
||||
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
|
||||
format!("{DEFAULT_BYPASS},{custom_bypass}")
|
||||
} else {
|
||||
custom_bypass
|
||||
}
|
||||
@@ -59,12 +58,15 @@ impl Sysopt {
|
||||
SYSOPT.get_or_init(|| Sysopt {
|
||||
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||
guard_state: Arc::new(false.into()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
||||
self.guard_proxy();
|
||||
// 使用事件驱动代理管理器
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_app_started();
|
||||
|
||||
log::info!(target: "app", "已启用事件驱动代理守卫");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -73,14 +75,14 @@ impl Sysopt {
|
||||
let _lock = self.update_sysproxy.lock().await;
|
||||
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
let pac_port = IVerge::get_singleton_port();
|
||||
|
||||
let (sys_enable, pac_enable, proxy_host) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
(
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.proxy_auto_config.unwrap_or(false),
|
||||
@@ -101,12 +103,14 @@ impl Sysopt {
|
||||
};
|
||||
let mut auto = Autoproxy {
|
||||
enable: false,
|
||||
url: format!("http://{}:{}/commands/pac", proxy_host, pac_port),
|
||||
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
||||
};
|
||||
|
||||
if !sys_enable {
|
||||
sys.set_system_proxy()?;
|
||||
auto.set_auto_proxy()?;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -115,6 +119,8 @@ impl Sysopt {
|
||||
auto.enable = true;
|
||||
sys.set_system_proxy()?;
|
||||
auto.set_auto_proxy()?;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -123,13 +129,18 @@ impl Sysopt {
|
||||
sys.enable = true;
|
||||
auto.set_auto_proxy()?;
|
||||
sys.set_system_proxy()?;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if !sys_enable {
|
||||
return self.reset_sysproxy().await;
|
||||
let result = self.reset_sysproxy().await;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return result;
|
||||
}
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use anyhow::bail;
|
||||
@@ -145,7 +156,7 @@ impl Sysopt {
|
||||
|
||||
let shell = app_handle.shell();
|
||||
let output = if pac_enable {
|
||||
let address = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
||||
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
.args(["pac", address.as_str()])
|
||||
@@ -154,7 +165,7 @@ impl Sysopt {
|
||||
.unwrap();
|
||||
output
|
||||
} else {
|
||||
let address = format!("{}:{}", proxy_host, port);
|
||||
let address = format!("{proxy_host}:{port}");
|
||||
let bypass = get_bypass();
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
@@ -169,6 +180,8 @@ impl Sysopt {
|
||||
bail!("sysproxy exe run failed");
|
||||
}
|
||||
}
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -180,7 +193,16 @@ impl Sysopt {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
|
||||
let mut autoproxy = Autoproxy::get_auto_proxy()?;
|
||||
let mut autoproxy = match Autoproxy::get_auto_proxy() {
|
||||
Ok(ap) => ap,
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
|
||||
Autoproxy {
|
||||
enable: false,
|
||||
url: "".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
sysproxy.enable = false;
|
||||
autoproxy.enable = false;
|
||||
autoproxy.set_auto_proxy()?;
|
||||
@@ -220,7 +242,7 @@ impl Sysopt {
|
||||
|
||||
/// update the startup
|
||||
pub fn update_launch(&self) -> Result<()> {
|
||||
let enable_auto_launch = { Config::verge().latest().enable_auto_launch };
|
||||
let enable_auto_launch = { Config::verge().latest_ref().enable_auto_launch };
|
||||
let is_enable = enable_auto_launch.unwrap_or(false);
|
||||
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
|
||||
|
||||
@@ -229,14 +251,14 @@ impl Sysopt {
|
||||
{
|
||||
if is_enable {
|
||||
if let Err(e) = startup_shortcut::create_shortcut() {
|
||||
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
|
||||
log::error!(target: "app", "创建启动快捷方式失败: {e}");
|
||||
// 如果快捷方式创建失败,回退到原来的方法
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Err(e) = startup_shortcut::remove_shortcut() {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {e}");
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
@@ -271,11 +293,11 @@ impl Sysopt {
|
||||
{
|
||||
match startup_shortcut::is_shortcut_enabled() {
|
||||
Ok(enabled) => {
|
||||
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
|
||||
log::info!(target: "app", "快捷方式自启动状态: {enabled}");
|
||||
return Ok(enabled);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
|
||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,176 +308,13 @@ impl Sysopt {
|
||||
|
||||
match autostart_manager.is_enabled() {
|
||||
Ok(status) => {
|
||||
log::info!(target: "app", "Auto launch status: {}", status);
|
||||
log::info!(target: "app", "Auto launch status: {status}");
|
||||
Ok(status)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get auto launch status: {}", e);
|
||||
log::error!(target: "app", "Failed to get auto launch status: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn guard_proxy(&self) {
|
||||
let _lock = self.guard_state.lock();
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
// default duration is 10s
|
||||
let mut wait_secs = 10u64;
|
||||
|
||||
loop {
|
||||
sleep(Duration::from_secs(wait_secs)).await;
|
||||
|
||||
let (enable, guard, guard_duration, pac, proxy_host) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.enable_proxy_guard.unwrap_or(false),
|
||||
verge.proxy_guard_duration.unwrap_or(10),
|
||||
verge.proxy_auto_config.unwrap_or(false),
|
||||
verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("127.0.0.1")),
|
||||
)
|
||||
};
|
||||
|
||||
// stop loop
|
||||
if !enable || !guard {
|
||||
continue;
|
||||
}
|
||||
|
||||
// update duration
|
||||
wait_secs = guard_duration;
|
||||
|
||||
log::debug!(target: "app", "try to guard the system proxy");
|
||||
|
||||
// 获取期望的代理端口
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let pac_port = IVerge::get_singleton_port();
|
||||
let bypass = get_bypass();
|
||||
|
||||
// 检查系统代理配置
|
||||
if pac {
|
||||
// 检查 PAC 代理设置
|
||||
let expected_url = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
||||
let autoproxy = match Autoproxy::get_auto_proxy() {
|
||||
Ok(ap) => ap,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get the auto proxy: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查自动代理是否启用且URL是否正确
|
||||
if !autoproxy.enable || autoproxy.url != expected_url {
|
||||
log::info!(target: "app", "auto proxy settings changed, restoring...");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let new_autoproxy = Autoproxy {
|
||||
enable: true,
|
||||
url: expected_url,
|
||||
};
|
||||
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get service path: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
let shell = app_handle.shell();
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
.args(["pac", expected_url.as_str()])
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "failed to set auto proxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 检查常规系统代理设置
|
||||
let sysproxy = match Sysproxy::get_system_proxy() {
|
||||
Ok(sp) => sp,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get the system proxy: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查系统代理是否启用且配置是否匹配
|
||||
if !sysproxy.enable || sysproxy.host != proxy_host || sysproxy.port != port {
|
||||
log::info!(target: "app", "system proxy settings changed, restoring...");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let new_sysproxy = Sysproxy {
|
||||
enable: true,
|
||||
host: proxy_host.clone(),
|
||||
port,
|
||||
bypass: bypass.clone(),
|
||||
};
|
||||
logging_error!(Type::System, true, new_sysproxy.set_system_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get service path: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
let address = format!("{}:{}", proxy_host, port);
|
||||
let shell = app_handle.shell();
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
.args(["global", address.as_str(), bypass.as_ref()])
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "failed to set system proxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ impl Timer {
|
||||
let cur_timestamp = chrono::Local::now().timestamp();
|
||||
|
||||
// Collect profiles that need immediate update
|
||||
let profiles_to_update = if let Some(items) = Config::profiles().latest().get_items() {
|
||||
let profiles_to_update = if let Some(items) = Config::profiles().latest_ref().get_items() {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
@@ -229,7 +229,7 @@ impl Timer {
|
||||
fn gen_map(&self) -> HashMap<String, u64> {
|
||||
let mut new_map = HashMap::new();
|
||||
|
||||
if let Some(items) = Config::profiles().latest().get_items() {
|
||||
if let Some(items) = Config::profiles().latest_ref().get_items() {
|
||||
for item in items.iter() {
|
||||
if let Some(option) = item.option.as_ref() {
|
||||
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
|
||||
@@ -376,7 +376,7 @@ impl Timer {
|
||||
|
||||
// Get the profile updated timestamp
|
||||
let profiles_config = Config::profiles();
|
||||
let profiles = profiles_config.latest();
|
||||
let profiles = profiles_config.latest_ref();
|
||||
let items = match profiles.get_items() {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
@@ -438,7 +438,7 @@ impl Timer {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
|
||||
Self::emit_update_event(&uid, true);
|
||||
|
||||
let is_current = Config::profiles().latest().current.as_ref() == Some(&uid);
|
||||
let is_current = Config::profiles().latest_ref().current.as_ref() == Some(&uid);
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
|
||||
+65
-239
@@ -12,15 +12,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
#[cfg(target_os = "macos")]
|
||||
use futures::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(target_os = "macos")]
|
||||
use parking_lot::RwLock;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use speed_rate::{SpeedRate, Traffic};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fs,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
@@ -31,20 +23,37 @@ use tauri::{
|
||||
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
||||
AppHandle, Wry,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use super::handle;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TrayState {}
|
||||
|
||||
// 托盘点击防抖机制
|
||||
static TRAY_CLICK_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
|
||||
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
||||
|
||||
fn get_tray_click_debounce() -> &'static Mutex<Instant> {
|
||||
TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
|
||||
}
|
||||
|
||||
fn should_handle_tray_click() -> bool {
|
||||
let debounce_lock = get_tray_click_debounce();
|
||||
let mut last_click = debounce_lock.lock();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
||||
*last_click = now;
|
||||
true
|
||||
} else {
|
||||
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
|
||||
now.duration_since(*last_click).as_millis());
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub struct Tray {
|
||||
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||
is_subscribed: Arc<RwLock<bool>>,
|
||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
||||
last_menu_update: Mutex<Option<Instant>>,
|
||||
menu_updating: AtomicBool,
|
||||
}
|
||||
@@ -57,7 +66,7 @@ pub struct Tray {
|
||||
|
||||
impl TrayState {
|
||||
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
|
||||
if is_common_tray_icon {
|
||||
if let Some(common_icon_path) = find_target_icons("common").unwrap() {
|
||||
@@ -91,7 +100,7 @@ impl TrayState {
|
||||
}
|
||||
|
||||
pub fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
|
||||
if is_sysproxy_tray_icon {
|
||||
if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() {
|
||||
@@ -105,7 +114,7 @@ impl TrayState {
|
||||
if tray_icon_colorful == "monochrome" {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
|
||||
include_bytes!("../../../icons/tray-icon-sys-mono-new.ico").to_vec(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
@@ -125,7 +134,7 @@ impl TrayState {
|
||||
}
|
||||
|
||||
pub fn get_tun_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
|
||||
if is_tun_tray_icon {
|
||||
if let Some(tun_icon_path) = find_target_icons("tun").unwrap() {
|
||||
@@ -139,7 +148,7 @@ impl TrayState {
|
||||
if tray_icon_colorful == "monochrome" {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
|
||||
include_bytes!("../../../icons/tray-icon-tun-mono-new.ico").to_vec(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
@@ -164,10 +173,6 @@ impl Tray {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return TRAY.get_or_init(|| Tray {
|
||||
speed_rate: Arc::new(Mutex::new(None)),
|
||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||
is_subscribed: Arc::new(RwLock::new(false)),
|
||||
rate_cache: Arc::new(Mutex::new(None)),
|
||||
last_menu_update: Mutex::new(None),
|
||||
menu_updating: AtomicBool::new(false),
|
||||
});
|
||||
@@ -180,18 +185,13 @@ impl Tray {
|
||||
}
|
||||
|
||||
pub fn init(&self) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut speed_rate = self.speed_rate.lock();
|
||||
*speed_rate = Some(SpeedRate::new());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘点击行为
|
||||
pub fn update_click_behavior(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
match tray_event.as_str() {
|
||||
@@ -251,12 +251,12 @@ impl Tray {
|
||||
}
|
||||
|
||||
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
let mode = {
|
||||
Config::clash()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.0
|
||||
.get("mode")
|
||||
.map(|val| val.as_str().unwrap_or("rule"))
|
||||
@@ -264,7 +264,7 @@ impl Tray {
|
||||
.to_owned()
|
||||
};
|
||||
let profile_uid_and_name = Config::profiles()
|
||||
.data()
|
||||
.data_mut()
|
||||
.all_profile_uid_and_name()
|
||||
.unwrap_or_default();
|
||||
let is_lightweight_mode = is_in_lightweight_mode();
|
||||
@@ -291,7 +291,7 @@ impl Tray {
|
||||
|
||||
/// 更新托盘图标
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
|
||||
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
@@ -308,59 +308,22 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
(true, true) => TrayState::get_tun_tray_icon(),
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
||||
(false, true) => TrayState::get_tun_tray_icon(),
|
||||
(false, false) => TrayState::get_common_tray_icon(),
|
||||
};
|
||||
|
||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false);
|
||||
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
||||
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
let is_colorful = colorful == "colorful";
|
||||
|
||||
if !enable_tray_speed {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rate = if let Some(rate) = rate {
|
||||
Some(rate)
|
||||
} else {
|
||||
let guard = self.speed_rate.lock();
|
||||
if let Some(guard) = guard.as_ref() {
|
||||
if let Some(rate) = guard.get_curent_rate() {
|
||||
Some(rate)
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
};
|
||||
|
||||
let mut rate_guard = self.rate_cache.lock();
|
||||
if *rate_guard != rate {
|
||||
*rate_guard = rate;
|
||||
|
||||
let bytes = if enable_tray_icon {
|
||||
Some(icon_bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rate = rate_guard.as_ref();
|
||||
if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
|
||||
}
|
||||
}
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -382,7 +345,7 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
@@ -426,7 +389,7 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
@@ -439,7 +402,7 @@ impl Tray {
|
||||
|
||||
let mut current_profile_name = "None".to_string();
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let profiles = profiles.latest_ref();
|
||||
if let Some(current_profile_uid) = profiles.get_current() {
|
||||
if let Ok(profile) = profiles.get_item(¤t_profile_uid) {
|
||||
current_profile_name = match &profile.name {
|
||||
@@ -475,155 +438,9 @@ impl Tray {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 订阅流量数据
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn subscribe_traffic(&self) -> Result<()> {
|
||||
log::info!(target: "app", "subscribe traffic");
|
||||
|
||||
// 如果已经订阅,先取消订阅
|
||||
if *self.is_subscribed.read() {
|
||||
self.unsubscribe_traffic();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = broadcast::channel(3);
|
||||
*self.shutdown_tx.write() = Some(shutdown_tx);
|
||||
*self.is_subscribed.write() = true;
|
||||
|
||||
let speed_rate = Arc::clone(&self.speed_rate);
|
||||
let is_subscribed = Arc::clone(&self.is_subscribed);
|
||||
|
||||
// 使用单线程防止阻塞主线程
|
||||
std::thread::Builder::new()
|
||||
.name("traffic-monitor".into())
|
||||
.spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to build tokio runtime for traffic monitor");
|
||||
// 在单独的运行时中执行异步任务
|
||||
rt.block_on(async move {
|
||||
let mut shutdown = shutdown_rx;
|
||||
let speed_rate = speed_rate.clone();
|
||||
let is_subscribed = is_subscribed.clone();
|
||||
let mut consecutive_errors = 0;
|
||||
let max_consecutive_errors = 5;
|
||||
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
|
||||
|
||||
'outer: loop {
|
||||
if !*is_subscribed.read() {
|
||||
log::info!(target: "app", "Traffic subscription has been cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
Traffic::get_traffic_stream()
|
||||
).await {
|
||||
Ok(stream_result) => {
|
||||
match stream_result {
|
||||
Ok(mut stream) => {
|
||||
consecutive_errors = 0;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
traffic_result = stream.next() => {
|
||||
match traffic_result {
|
||||
Some(Ok(traffic)) => {
|
||||
if let Ok(Some(rate)) = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(50),
|
||||
async {
|
||||
let guard = speed_rate.try_lock();
|
||||
if let Some(guard) = guard {
|
||||
if let Some(sr) = guard.as_ref() {
|
||||
sr.update_and_check_changed(traffic.up, traffic.down)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
).await {
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(100),
|
||||
async { let _ = Tray::global().update_icon(Some(rate)); }
|
||||
).await;
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => {
|
||||
log::error!(target: "app", "Traffic stream error: {}", e);
|
||||
consecutive_errors += 1;
|
||||
if consecutive_errors >= max_consecutive_errors {
|
||||
log::error!(target: "app", "Too many errors, reconnecting traffic stream");
|
||||
break;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
log::info!(target: "app", "Traffic stream ended, reconnecting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ = shutdown.recv() => {
|
||||
log::info!(target: "app", "Received shutdown signal for traffic stream");
|
||||
break 'outer;
|
||||
},
|
||||
_ = interval.tick() => {
|
||||
if !*is_subscribed.read() {
|
||||
log::info!(target: "app", "Traffic monitor detected subscription cancelled");
|
||||
break 'outer;
|
||||
}
|
||||
log::debug!(target: "app", "Traffic subscription periodic health check");
|
||||
},
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
|
||||
log::info!(target: "app", "Traffic stream max active time reached, reconnecting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get traffic stream: {}", e);
|
||||
consecutive_errors += 1;
|
||||
if consecutive_errors >= max_consecutive_errors {
|
||||
log::error!(target: "app", "Too many consecutive errors, pausing traffic monitoring");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||
consecutive_errors = 0;
|
||||
} else {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
log::error!(target: "app", "Traffic stream initialization timed out");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
|
||||
if !*is_subscribed.read() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
log::info!(target: "app", "Traffic subscription thread terminated");
|
||||
});
|
||||
})
|
||||
.expect("Failed to spawn traffic monitor thread");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 取消订阅 traffic 数据
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn unsubscribe_traffic(&self) {
|
||||
log::info!(target: "app", "unsubscribe traffic");
|
||||
*self.is_subscribed.write() = false;
|
||||
if let Some(tx) = self.shutdown_tx.write().take() {
|
||||
drop(tx);
|
||||
}
|
||||
}
|
||||
pub fn unsubscribe_traffic(&self) {}
|
||||
|
||||
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
log::info!(target: "app", "正在从AppHandle创建系统托盘");
|
||||
@@ -644,7 +461,7 @@ impl Tray {
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
{
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
if tray_event.as_str() != "tray_menu" {
|
||||
builder = builder.show_menu_on_left_click(false);
|
||||
@@ -654,9 +471,9 @@ impl Tray {
|
||||
let tray = builder.build(app_handle)?;
|
||||
|
||||
tray.on_tray_icon_event(|_, event| {
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
log::debug!(target: "app","tray event: {:?}", tray_event);
|
||||
log::debug!(target: "app","tray event: {tray_event:?}");
|
||||
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
@@ -664,6 +481,11 @@ impl Tray {
|
||||
..
|
||||
} = event
|
||||
{
|
||||
// 添加防抖检查,防止快速连击
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
@@ -675,7 +497,7 @@ impl Tray {
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {:?}", result);
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -712,7 +534,7 @@ fn create_tray_menu(
|
||||
let version = VERSION.get().unwrap_or(&unknown_version);
|
||||
|
||||
let hotkeys = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.hotkeys
|
||||
.as_ref()
|
||||
.map(|h| {
|
||||
@@ -732,11 +554,11 @@ fn create_tray_menu(
|
||||
.iter()
|
||||
.map(|(profile_uid, profile_name)| {
|
||||
let is_current_profile = Config::profiles()
|
||||
.data()
|
||||
.data_mut()
|
||||
.is_current_profile_index(profile_uid.to_string());
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
format!("profiles_{}", profile_uid),
|
||||
format!("profiles_{profile_uid}"),
|
||||
t(profile_name),
|
||||
true,
|
||||
is_current_profile,
|
||||
@@ -949,14 +771,17 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
"open_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
||||
// 如果在轻量模式中,先退出轻量模式
|
||||
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
// 使用统一的窗口管理器显示窗口
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {:?}", result);
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
}
|
||||
"system_proxy" => {
|
||||
feat::toggle_system_proxy();
|
||||
@@ -977,7 +802,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"entry_lightweight_mode" => {
|
||||
// 处理轻量模式的切换
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
|
||||
if was_lightweight {
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
@@ -985,11 +813,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
crate::module::lightweight::entry_lightweight_mode();
|
||||
}
|
||||
|
||||
// 退出轻量模式后显示主窗口
|
||||
if was_lightweight {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {:?}", result);
|
||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
@@ -1002,8 +829,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 统一调用状态更新
|
||||
if let Err(e) = Tray::global().update_all_states() {
|
||||
log::warn!(target: "app", "更新托盘状态失败: {}", e);
|
||||
log::warn!(target: "app", "更新托盘状态失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,336 +1 @@
|
||||
use crate::{
|
||||
module::mihomo::{MihomoManager, Rate},
|
||||
utils::help::format_bytes_speed,
|
||||
};
|
||||
use ab_glyph::FontArc;
|
||||
use anyhow::Result;
|
||||
use futures::Stream;
|
||||
use image::{GenericImageView, Rgba, RgbaImage};
|
||||
use imageproc::drawing::draw_text_mut;
|
||||
use parking_lot::Mutex;
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
use tokio_tungstenite::tungstenite::http;
|
||||
use tungstenite::client::IntoClientRequest;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpeedRate {
|
||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||
last_update: Arc<Mutex<std::time::Instant>>,
|
||||
}
|
||||
|
||||
impl SpeedRate {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
|
||||
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option<Rate> {
|
||||
let mut rates = self.rate.lock();
|
||||
let mut last_update = self.last_update.lock();
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
// 限制更新频率为每秒最多2次(500ms)
|
||||
if now.duration_since(*last_update).as_millis() < 500 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (current, previous) = &mut *rates;
|
||||
|
||||
// Avoid unnecessary float conversions for small value checks
|
||||
let should_update = if current.up < 1000 && down < 1000 {
|
||||
// For small values, always update to ensure accuracy
|
||||
current.up != up || current.down != down
|
||||
} else {
|
||||
// For larger values, use integer math to check for >5% change
|
||||
// Multiply by 20 instead of dividing by 0.05 to avoid floating point
|
||||
let up_threshold = current.up / 20;
|
||||
let down_threshold = current.down / 20;
|
||||
|
||||
(up > current.up && up - current.up > up_threshold)
|
||||
|| (up < current.up && current.up - up > up_threshold)
|
||||
|| (down > current.down && down - current.down > down_threshold)
|
||||
|| (down < current.down && current.down - down > down_threshold)
|
||||
};
|
||||
|
||||
if !should_update {
|
||||
return None;
|
||||
}
|
||||
|
||||
*previous = current.clone();
|
||||
current.up = up;
|
||||
current.down = down;
|
||||
*last_update = now;
|
||||
|
||||
if previous != current {
|
||||
Some(current.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_curent_rate(&self) -> Option<Rate> {
|
||||
let rates = self.rate.lock();
|
||||
let (current, _) = &*rates;
|
||||
Some(current.clone())
|
||||
}
|
||||
|
||||
// 分离图标加载和速率渲染
|
||||
pub fn add_speed_text(
|
||||
is_custom_icon: bool,
|
||||
icon_bytes: Option<Vec<u8>>,
|
||||
rate: Option<&Rate>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
|
||||
|
||||
let (mut icon_width, mut icon_height) = (0, 256);
|
||||
let icon_image = if let Some(bytes) = icon_bytes.clone() {
|
||||
let icon_image = image::load_from_memory(&bytes)?;
|
||||
icon_width = icon_image.width();
|
||||
icon_height = icon_image.height();
|
||||
icon_image
|
||||
} else {
|
||||
// 返回一个空的 RGBA 图像
|
||||
image::DynamicImage::new_rgba8(0, 0)
|
||||
};
|
||||
|
||||
let total_width = match (is_custom_icon, icon_bytes.is_some()) {
|
||||
(true, true) => 510,
|
||||
(true, false) => 740,
|
||||
(false, false) => 740,
|
||||
(false, true) => icon_width + 740,
|
||||
};
|
||||
|
||||
// println!(
|
||||
// "icon_height: {}, icon_wight: {}, total_width: {}",
|
||||
// icon_height, icon_width, total_width
|
||||
// );
|
||||
|
||||
// 创建新的透明画布
|
||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||
|
||||
// 将原始图标绘制到新画布的左侧
|
||||
if icon_bytes.is_some() {
|
||||
for y in 0..icon_height {
|
||||
for x in 0..icon_width {
|
||||
let pixel = icon_image.get_pixel(x, y);
|
||||
combined_image.put_pixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_colorful = if let Some(bytes) = icon_bytes.clone() {
|
||||
!crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 选择文本颜色
|
||||
let (text_color, shadow_color) = if is_colorful {
|
||||
(
|
||||
Rgba([144u8, 144u8, 144u8, 255u8]),
|
||||
// Rgba([255u8, 255u8, 255u8, 128u8]),
|
||||
Rgba([0u8, 0u8, 0u8, 128u8]),
|
||||
)
|
||||
// (
|
||||
// Rgba([160u8, 160u8, 160u8, 255u8]),
|
||||
// // Rgba([255u8, 255u8, 255u8, 128u8]),
|
||||
// Rgba([0u8, 0u8, 0u8, 255u8]),
|
||||
// )
|
||||
} else {
|
||||
(
|
||||
Rgba([255u8, 255u8, 255u8, 255u8]),
|
||||
Rgba([0u8, 0u8, 0u8, 128u8]),
|
||||
)
|
||||
};
|
||||
// 减小字体大小以适应文本区域
|
||||
let font_data = include_bytes!("../../../assets/fonts/SF-Pro.ttf");
|
||||
let font = FontArc::try_from_vec(font_data.to_vec()).unwrap();
|
||||
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
|
||||
let scale = ab_glyph::PxScale::from(font_size);
|
||||
|
||||
// 使用更简洁的速率格式
|
||||
let up_text = format!("↑ {}", format_bytes_speed(rate.up));
|
||||
let down_text = format!("↓ {}", format_bytes_speed(rate.down));
|
||||
|
||||
// For test rate display
|
||||
// let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024));
|
||||
|
||||
// 计算文本位置,确保垂直间距合适
|
||||
// 修改文本位置为居右显示
|
||||
// 计算右对齐的文本位置
|
||||
// let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
|
||||
// let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
|
||||
// let up_text_x = total_width - up_text_width;
|
||||
// let down_text_x = total_width - down_text_width;
|
||||
|
||||
// 计算左对齐的文本位置
|
||||
let (up_text_x, down_text_x) = {
|
||||
if is_custom_icon || icon_bytes.is_some() {
|
||||
let text_left_offset = 30;
|
||||
let left_begin = icon_width + text_left_offset;
|
||||
(left_begin, left_begin)
|
||||
} else {
|
||||
(icon_width, icon_width)
|
||||
}
|
||||
};
|
||||
|
||||
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
|
||||
let text_height = font_size as i32;
|
||||
let total_text_height = text_height * 2;
|
||||
let up_y = (icon_height as i32 - total_text_height) / 2;
|
||||
let down_y = up_y + text_height;
|
||||
|
||||
// 绘制速率文本(先阴影后文字)
|
||||
let shadow_offset = 1;
|
||||
|
||||
// 绘制上行速率
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
shadow_color,
|
||||
up_text_x as i32 + shadow_offset,
|
||||
up_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
);
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
text_color,
|
||||
up_text_x as i32,
|
||||
up_y,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
);
|
||||
|
||||
// 绘制下行速率
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
shadow_color,
|
||||
down_text_x as i32 + shadow_offset,
|
||||
down_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
);
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
text_color,
|
||||
down_text_x as i32,
|
||||
down_y,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
);
|
||||
|
||||
// 将结果转换为 PNG 数据
|
||||
let mut bytes = Vec::new();
|
||||
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Traffic {
|
||||
pub up: u64,
|
||||
pub down: u64,
|
||||
}
|
||||
|
||||
impl Traffic {
|
||||
pub async fn get_traffic_stream() -> Result<impl Stream<Item = Result<Traffic, anyhow::Error>>>
|
||||
{
|
||||
use futures::{
|
||||
future::FutureExt,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
// 先处理错误和超时情况
|
||||
let stream = Box::pin(
|
||||
stream::unfold((), move |_| async move {
|
||||
'retry: loop {
|
||||
log::info!(target: "app", "establishing traffic websocket connection");
|
||||
let (url, token) = MihomoManager::get_traffic_ws_url();
|
||||
let mut request = match url.into_client_request() {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to create websocket request: {}", e);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
continue 'retry;
|
||||
}
|
||||
};
|
||||
|
||||
request.headers_mut().insert(http::header::AUTHORIZATION, token);
|
||||
|
||||
match tokio::time::timeout(Duration::from_secs(3),
|
||||
tokio_tungstenite::connect_async(request)
|
||||
).await {
|
||||
Ok(Ok((ws_stream, _))) => {
|
||||
log::info!(target: "app", "traffic websocket connection established");
|
||||
// 设置流超时控制
|
||||
let traffic_stream = ws_stream
|
||||
.take_while(|msg| {
|
||||
let continue_stream = msg.is_ok();
|
||||
async move { continue_stream }.boxed()
|
||||
})
|
||||
.filter_map(|msg| async move {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
if !msg.is_text() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match tokio::time::timeout(
|
||||
Duration::from_millis(200),
|
||||
async { msg.into_text() }
|
||||
).await {
|
||||
Ok(Ok(text)) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&text) {
|
||||
Ok(json) => {
|
||||
let up = json["up"].as_u64().unwrap_or(0);
|
||||
let down = json["down"].as_u64().unwrap_or(0);
|
||||
Some(Ok(Traffic { up, down }))
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "traffic json parse error: {} for {}", e, text);
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "traffic text conversion error: {}", e);
|
||||
None
|
||||
},
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "traffic text processing timeout");
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "traffic websocket error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Some((traffic_stream, ()));
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
log::error!(target: "app", "traffic websocket connection failed: {}", e);
|
||||
},
|
||||
Err(_) => {
|
||||
log::error!(target: "app", "traffic websocket connection timed out");
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ type ResultLog = Vec<(String, String)>;
|
||||
/// 返回最终订阅、该订阅包含的键、和script执行的结果
|
||||
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// config.yaml 的订阅
|
||||
let clash_config = { Config::clash().latest().0.clone() };
|
||||
let clash_config = { Config::clash().latest_ref().0.clone() };
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
(
|
||||
Some(verge.get_valid_clash_core()),
|
||||
verge.enable_tun_mode.unwrap_or(false),
|
||||
@@ -33,13 +33,13 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_enabled = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
verge.verge_redir_enabled.unwrap_or(false)
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_enabled = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
verge.verge_tproxy_enabled.unwrap_or(false)
|
||||
};
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
profile_name,
|
||||
) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let profiles = profiles.latest_ref();
|
||||
|
||||
let current = profiles.current_mapping().unwrap_or_default();
|
||||
let merge = profiles
|
||||
@@ -214,6 +214,12 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
config.remove("port");
|
||||
continue;
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if key.as_str() == Some("redir-port") || key.as_str() == Some("tproxy-port") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
if key.as_str() == Some("redir-port") && !redir_enabled {
|
||||
|
||||
@@ -8,7 +8,8 @@ pub fn use_script(
|
||||
name: String,
|
||||
) -> Result<(Mapping, Vec<(String, String)>)> {
|
||||
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
let mut context = Context::default();
|
||||
|
||||
let outputs = Arc::new(Mutex::new(vec![]));
|
||||
@@ -24,7 +25,7 @@ pub fn use_script(
|
||||
let level = level.to_std_string().unwrap();
|
||||
let data = args.get(1).unwrap().to_string(context)?;
|
||||
let data = data.to_std_string().unwrap();
|
||||
let mut out = copy_outputs.lock().unwrap();
|
||||
let mut out = copy_outputs.lock();
|
||||
out.push((level, data));
|
||||
Ok(JsValue::undefined())
|
||||
},
|
||||
@@ -67,7 +68,7 @@ pub fn use_script(
|
||||
// 直接解析JSON结果,不做其他解析
|
||||
let res: Result<Mapping, Error> = parse_json_safely(&result);
|
||||
|
||||
let mut out = outputs.lock().unwrap();
|
||||
let mut out = outputs.lock();
|
||||
match res {
|
||||
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||
Err(err) => {
|
||||
@@ -141,8 +142,8 @@ fn test_script() {
|
||||
fn test_escape_unescape() {
|
||||
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
||||
let escaped = escape_js_string_for_single_quote(test_string);
|
||||
println!("Original: {}", test_string);
|
||||
println!("Escaped: {}", escaped);
|
||||
println!("Original: {test_string}");
|
||||
println!("Escaped: {escaped}");
|
||||
|
||||
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
||||
let parsed = parse_json_safely(json_str).unwrap();
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::fs;
|
||||
/// Create a backup and upload to WebDAV
|
||||
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
||||
log::error!(target: "app", "Failed to create backup: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to create backup: {err:#?}");
|
||||
err
|
||||
})?;
|
||||
|
||||
@@ -19,12 +19,12 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
.upload(temp_file_path.clone(), file_name)
|
||||
.await
|
||||
{
|
||||
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to upload to WebDAV: {err:#?}");
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::remove_file(&temp_file_path) {
|
||||
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
|
||||
log::warn!(target: "app", "Failed to remove temp file: {err:#?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -33,7 +33,7 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
/// List WebDAV backups
|
||||
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
|
||||
backup::WebDavClient::global().list().await.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to list WebDAV backup files: {err:#?}");
|
||||
err
|
||||
})
|
||||
}
|
||||
@@ -44,7 +44,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
.delete(filename)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {err:#?}");
|
||||
err
|
||||
})
|
||||
}
|
||||
@@ -52,7 +52,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
/// Restore WebDAV backup
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
let verge_data = verge.latest_ref().clone();
|
||||
let webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
@@ -62,7 +62,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
.download(filename, backup_storage_path.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {err:#?}");
|
||||
err
|
||||
})?;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ fn after_change_clash_mode() {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to get connections: {}", err);
|
||||
log::error!(target: "app", "Failed to get connections: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -68,16 +68,16 @@ pub fn change_clash_mode(mode: String) {
|
||||
match MihomoManager::global().patch_configs(json_value).await {
|
||||
Ok(_) => {
|
||||
// 更新订阅
|
||||
Config::clash().data().patch_config(mapping);
|
||||
Config::clash().data_mut().patch_config(mapping);
|
||||
|
||||
if Config::clash().data().save_config().is_ok() {
|
||||
if Config::clash().data_mut().save_config().is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
||||
}
|
||||
|
||||
let is_auto_close_connection = Config::verge()
|
||||
.data()
|
||||
.data_mut()
|
||||
.auto_close_connection
|
||||
.unwrap_or(false);
|
||||
if is_auto_close_connection {
|
||||
@@ -94,7 +94,10 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
use crate::utils::network::{NetworkManager, ProxyType};
|
||||
use tokio::time::Instant;
|
||||
|
||||
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||
let tun_mode = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_tun_mode
|
||||
.unwrap_or(false);
|
||||
|
||||
// 如果是TUN模式,不使用代理,否则使用自身代理
|
||||
let proxy_type = if !tun_mode {
|
||||
@@ -113,7 +116,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
||||
log::trace!(target: "app", "test_delay response: {response:#?}");
|
||||
if response.status().is_success() {
|
||||
Ok(start.elapsed().as_millis() as u32)
|
||||
} else {
|
||||
@@ -121,7 +124,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
||||
log::trace!(target: "app", "test_delay error: {err:#?}");
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use serde_yaml::Mapping;
|
||||
|
||||
/// Patch Clash configuration
|
||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
Config::clash().draft().patch_config(patch.clone());
|
||||
Config::clash().draft_mut().patch_config(patch.clone());
|
||||
|
||||
let res = {
|
||||
// 激活订阅
|
||||
@@ -22,7 +22,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
||||
}
|
||||
Config::runtime().latest().patch_config(patch);
|
||||
Config::runtime().draft_mut().patch_config(patch);
|
||||
CoreManager::global().update_config().await?;
|
||||
}
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -31,7 +31,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::clash().apply();
|
||||
Config::clash().data().save_config()?;
|
||||
Config::clash().data_mut().save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -60,7 +60,7 @@ enum UpdateFlags {
|
||||
|
||||
/// Patch Verge configuration
|
||||
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().draft_mut().patch_config(patch.clone());
|
||||
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
@@ -175,7 +175,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
|
||||
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
|
||||
Config::verge().draft_mut().enable_global_hotkey = enable_global_hotkey;
|
||||
handle::Handle::refresh_verge();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
|
||||
@@ -213,7 +213,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
Ok(()) => {
|
||||
Config::verge().apply();
|
||||
if !not_save_file {
|
||||
Config::verge().data().save_file()?;
|
||||
Config::verge().data_mut().save_file()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -36,15 +36,15 @@ pub async fn update_profile(
|
||||
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let profiles = profiles.latest_ref();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
log::info!(target: "app", "[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
log::warn!(target: "app", "[订阅更新] {} 缺少URL,无法更新", uid);
|
||||
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL,无法更新");
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
log::info!(target: "app",
|
||||
@@ -66,16 +66,16 @@ pub async fn update_profile(
|
||||
Ok(item) => {
|
||||
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
let mut profiles = profiles.draft_mut();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(err) => {
|
||||
// 首次更新失败,尝试使用Clash代理
|
||||
log::warn!(target: "app", "[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
|
||||
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err},尝试使用Clash代理更新");
|
||||
|
||||
// 发送通知
|
||||
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
||||
@@ -102,7 +102,7 @@ pub async fn update_profile(
|
||||
|
||||
// 更新到配置
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
let mut profiles = profiles.draft_mut();
|
||||
profiles.update_item(uid.clone(), item.clone())?;
|
||||
|
||||
// 获取配置名称用于通知
|
||||
@@ -112,14 +112,14 @@ pub async fn update_profile(
|
||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(retry_err) => {
|
||||
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
|
||||
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
|
||||
handle::Handle::notice_message(
|
||||
"update_failed_even_with_clash",
|
||||
format!("{}", retry_err),
|
||||
format!("{retry_err}"),
|
||||
);
|
||||
return Err(retry_err);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
/// Toggle system proxy on/off
|
||||
pub fn toggle_system_proxy() {
|
||||
let enable = Config::verge().draft().enable_system_proxy;
|
||||
let enable = Config::verge().draft_mut().enable_system_proxy;
|
||||
let enable = enable.unwrap_or(false);
|
||||
let auto_close_connection = Config::verge()
|
||||
.data()
|
||||
.data_mut()
|
||||
.auto_close_connection
|
||||
.unwrap_or(false);
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn toggle_system_proxy() {
|
||||
.close_all_connections()
|
||||
.await
|
||||
{
|
||||
log::error!(target: "app", "Failed to close all connections: {}", err);
|
||||
log::error!(target: "app", "Failed to close all connections: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn toggle_system_proxy() {
|
||||
|
||||
/// Toggle TUN mode on/off
|
||||
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
let enable = Config::verge().data().enable_tun_mode;
|
||||
let enable = Config::verge().data_mut().enable_tun_mode;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
AsyncHandler::spawn(async move || {
|
||||
@@ -67,19 +67,24 @@ pub fn copy_clash_env() {
|
||||
// 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1
|
||||
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| {
|
||||
Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||
});
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
||||
let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
|
||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
|
||||
let port = {
|
||||
Config::verge()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(7897)
|
||||
};
|
||||
let http_proxy = format!("http://{clash_verge_rev_ip}:{port}");
|
||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
|
||||
|
||||
let cliboard = app_handle.clipboard();
|
||||
let env_type = { Config::verge().latest().env_type.clone() };
|
||||
let env_type = { Config::verge().latest_ref().env_type.clone() };
|
||||
let env_type = match env_type {
|
||||
Some(env_type) => env_type,
|
||||
None => {
|
||||
|
||||
@@ -11,23 +11,54 @@ use crate::{
|
||||
/// Open or close the dashboard window
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard() {
|
||||
open_or_close_dashboard_internal(false)
|
||||
}
|
||||
|
||||
/// Open or close the dashboard window (hotkey call, dispatched to main thread)
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard_hotkey() {
|
||||
open_or_close_dashboard_internal(true)
|
||||
}
|
||||
|
||||
/// Internal implementation for opening/closing dashboard
|
||||
fn open_or_close_dashboard_internal(bypass_debounce: bool) {
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
|
||||
|
||||
// 检查是否在轻量模式下
|
||||
// 热键调用调度到主线程执行,避免 WebView 创建死锁
|
||||
if bypass_debounce {
|
||||
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
log::info!(target: "app", "主线程中执行热键窗口操作");
|
||||
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "Window operation result: {result:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
let result = WindowManager::toggle_main_window();
|
||||
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||
});
|
||||
return;
|
||||
}
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "Window operation result: {:?}", result);
|
||||
log::info!(target: "app", "Window operation result: {result:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用统一的窗口管理器切换窗口状态
|
||||
let result = WindowManager::toggle_main_window();
|
||||
log::info!(target: "app", "Window toggle result: {:?}", result);
|
||||
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||
}
|
||||
|
||||
/// 异步优化的应用退出函数
|
||||
@@ -68,7 +99,7 @@ async fn clean_async() -> bool {
|
||||
|
||||
// 1. 处理TUN模式
|
||||
let tun_task = async {
|
||||
if Config::verge().data().enable_tun_mode.unwrap_or(false) {
|
||||
if Config::verge().data_mut().enable_tun_mode.unwrap_or(false) {
|
||||
let disable_tun = serde_json::json!({
|
||||
"tun": {
|
||||
"enable": false
|
||||
@@ -209,7 +240,7 @@ pub fn hide() {
|
||||
use crate::module::lightweight::add_light_weight_timer;
|
||||
|
||||
let enable_auto_light_weight_mode = Config::verge()
|
||||
.data()
|
||||
.data_mut()
|
||||
.enable_auto_light_weight_mode
|
||||
.unwrap_or(false);
|
||||
|
||||
|
||||
+13
-12
@@ -1,5 +1,5 @@
|
||||
mod cmd;
|
||||
mod config;
|
||||
pub mod config;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod feat;
|
||||
@@ -13,8 +13,10 @@ use crate::{
|
||||
utils::{resolve, resolve::resolve_scheme, server},
|
||||
};
|
||||
use config::Config;
|
||||
use std::sync::{Mutex, Once};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Once;
|
||||
use tauri::AppHandle;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::Manager;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
@@ -41,14 +43,14 @@ impl AppHandleManager {
|
||||
/// Initialize the app handle manager with an app handle.
|
||||
pub fn init(&self, handle: AppHandle) {
|
||||
self.init.call_once(|| {
|
||||
let mut app_handle = self.inner.lock().unwrap();
|
||||
let mut app_handle = self.inner.lock();
|
||||
*app_handle = Some(handle);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the app handle if it has been initialized.
|
||||
pub fn get(&self) -> Option<AppHandle> {
|
||||
self.inner.lock().unwrap().clone()
|
||||
self.inner.lock().clone()
|
||||
}
|
||||
|
||||
/// Get the app handle, panics if it hasn't been initialized.
|
||||
@@ -59,7 +61,7 @@ impl AppHandleManager {
|
||||
pub fn set_activation_policy_regular(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = self.inner.lock();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||
}
|
||||
@@ -68,7 +70,7 @@ impl AppHandleManager {
|
||||
pub fn set_activation_policy_accessory(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = self.inner.lock();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
}
|
||||
@@ -77,7 +79,7 @@ impl AppHandleManager {
|
||||
pub fn set_activation_policy_prohibited(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = self.inner.lock();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited);
|
||||
}
|
||||
@@ -125,6 +127,7 @@ pub fn run() {
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
@@ -133,6 +136,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.manage(Mutex::new(state::lightweight::LightWeightState::default()))
|
||||
.setup(|app| {
|
||||
logging!(info, Type::Setup, true, "开始应用初始化...");
|
||||
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||
@@ -212,9 +216,6 @@ pub fn run() {
|
||||
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
|
||||
}
|
||||
|
||||
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
|
||||
app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||
Ok(())
|
||||
})
|
||||
@@ -401,7 +402,7 @@ pub fn run() {
|
||||
}
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
@@ -425,7 +426,7 @@ pub fn run() {
|
||||
}
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
|
||||
@@ -13,41 +13,48 @@ use crate::AppHandleManager;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::TaskBuilder;
|
||||
use std::sync::Mutex;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{Listener, Manager};
|
||||
|
||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||
|
||||
// 添加退出轻量模式的锁,防止并发调用
|
||||
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn with_lightweight_status<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut LightWeightState) -> R,
|
||||
{
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let state = app_handle.state::<Mutex<LightWeightState>>();
|
||||
let mut guard = state.lock().unwrap();
|
||||
let mut guard = state.lock();
|
||||
f(&mut guard)
|
||||
}
|
||||
|
||||
pub fn run_once_auto_lightweight() {
|
||||
LightWeightState::default().run_once_time(|| {
|
||||
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true);
|
||||
let is_silent_start = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_silent_start
|
||||
.unwrap_or(false);
|
||||
let enable_auto = Config::verge()
|
||||
.data()
|
||||
.data_mut()
|
||||
.enable_auto_light_weight_mode
|
||||
.unwrap_or(true);
|
||||
.unwrap_or(false);
|
||||
if enable_auto && is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"正常创建窗口和添加定时器监听器"
|
||||
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
|
||||
);
|
||||
set_lightweight_mode(false);
|
||||
disable_auto_light_weight_mode();
|
||||
enable_auto_light_weight_mode();
|
||||
|
||||
// 触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {}", e);
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -56,17 +63,23 @@ pub fn run_once_auto_lightweight() {
|
||||
pub fn auto_lightweight_mode_init() {
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
let _ = app_handle.state::<Mutex<LightWeightState>>();
|
||||
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
|
||||
let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
|
||||
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false);
|
||||
let enable_auto =
|
||||
{ Config::verge().latest_ref().enable_auto_light_weight_mode }.unwrap_or(false);
|
||||
|
||||
if enable_auto && is_silent_start {
|
||||
logging!(info, Type::Lightweight, true, "自动轻量模式静默启动");
|
||||
if enable_auto && !is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"非静默启动直接挂载自动进入轻量模式监听器!"
|
||||
);
|
||||
set_lightweight_mode(true);
|
||||
enable_auto_light_weight_mode();
|
||||
|
||||
// 确保托盘状态更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {}", e);
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,14 +91,14 @@ pub fn is_in_lightweight_mode() -> bool {
|
||||
}
|
||||
|
||||
// 设置轻量模式状态
|
||||
fn set_lightweight_mode(value: bool) {
|
||||
pub fn set_lightweight_mode(value: bool) {
|
||||
with_lightweight_status(|state| {
|
||||
state.set_lightweight_mode(value);
|
||||
});
|
||||
|
||||
// 触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {}", e);
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +133,6 @@ pub fn entry_lightweight_mode() {
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
}
|
||||
set_lightweight_mode(true);
|
||||
let _ = cancel_light_weight_timer();
|
||||
@@ -131,6 +143,25 @@ pub fn entry_lightweight_mode() {
|
||||
|
||||
// 添加从轻量模式恢复的函数
|
||||
pub fn exit_lightweight_mode() {
|
||||
// 使用原子操作检查是否已经在退出过程中,防止并发调用
|
||||
if EXITING_LIGHTWEIGHT
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_err()
|
||||
{
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"轻量模式退出操作已在进行中,跳过重复调用"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用defer确保无论如何都会重置标志
|
||||
let _guard = scopeguard::guard((), |_| {
|
||||
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
// 确保当前确实处于轻量模式才执行退出操作
|
||||
if !is_in_lightweight_mode() {
|
||||
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
|
||||
@@ -138,7 +169,6 @@ pub fn exit_lightweight_mode() {
|
||||
}
|
||||
|
||||
set_lightweight_mode(false);
|
||||
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
|
||||
|
||||
// macOS激活策略
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -197,7 +227,7 @@ fn cancel_window_close_listener() {
|
||||
fn setup_light_weight_timer() -> Result<()> {
|
||||
Timer::global().init()?;
|
||||
let once_by_minutes = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.auto_light_weight_minutes
|
||||
.unwrap_or(10);
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ use once_cell::sync::Lazy;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::http::HeaderMap;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::http::HeaderValue;
|
||||
|
||||
// 缓存的最大有效期(5秒)
|
||||
const CACHE_TTL: Duration = Duration::from_secs(5);
|
||||
@@ -94,43 +92,17 @@ impl MihomoManager {
|
||||
|
||||
impl MihomoManager {
|
||||
pub fn get_clash_client_info() -> Option<(String, HeaderMap)> {
|
||||
let client = { Config::clash().data().get_client_info() };
|
||||
let client = { Config::clash().latest_ref().get_client_info() };
|
||||
let server = format!("http://{}", client.server);
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||
if let Some(secret) = client.secret {
|
||||
let secret = format!("Bearer {}", secret).parse().unwrap();
|
||||
let secret = format!("Bearer {secret}").parse().unwrap();
|
||||
headers.insert("Authorization", secret);
|
||||
}
|
||||
|
||||
Some((server, headers))
|
||||
}
|
||||
|
||||
// 提供默认值的版本,避免在connection_info为None时panic
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_clash_client_info_or_default() -> (String, HeaderMap) {
|
||||
Self::get_clash_client_info().unwrap_or_else(|| {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||
("http://127.0.0.1:9090".to_string(), headers)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_traffic_ws_url() -> (String, HeaderValue) {
|
||||
let (url, headers) = MihomoManager::get_clash_client_info_or_default();
|
||||
let ws_url = url.replace("http://", "ws://") + "/traffic";
|
||||
let auth = headers
|
||||
.get("Authorization")
|
||||
.map(|val| val.to_str().unwrap_or("").to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 创建默认的空HeaderValue而不是使用unwrap_or_default
|
||||
let token = match HeaderValue::from_str(&auth) {
|
||||
Ok(v) => v,
|
||||
Err(_) => HeaderValue::from_static(""),
|
||||
};
|
||||
|
||||
(ws_url, token)
|
||||
}
|
||||
// 已移除未使用的 get_clash_client_info_or_default 和 get_traffic_ws_url 方法
|
||||
}
|
||||
|
||||
@@ -1,19 +1,65 @@
|
||||
use std::time::{Duration, Instant};
|
||||
pub struct CacheEntry {
|
||||
pub value: Arc<Value>,
|
||||
pub expires_at: Instant,
|
||||
}
|
||||
use dashmap::DashMap;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
pub struct CmdProxyState {
|
||||
pub last_refresh_time: std::time::Instant,
|
||||
pub need_refresh: bool,
|
||||
pub proxies: Box<Value>,
|
||||
pub providers_proxies: Box<Value>,
|
||||
pub struct ProxyRequestCache {
|
||||
pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>,
|
||||
}
|
||||
|
||||
impl Default for CmdProxyState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_refresh_time: std::time::Instant::now(),
|
||||
need_refresh: true,
|
||||
proxies: Box::new(Value::Null),
|
||||
providers_proxies: Box::new(Value::Null),
|
||||
impl ProxyRequestCache {
|
||||
pub fn global() -> &'static Self {
|
||||
static INSTANCE: once_cell::sync::OnceCell<ProxyRequestCache> =
|
||||
once_cell::sync::OnceCell::new();
|
||||
INSTANCE.get_or_init(|| ProxyRequestCache {
|
||||
map: DashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn make_key(prefix: &str, id: &str) -> String {
|
||||
format!("{prefix}:{id}")
|
||||
}
|
||||
|
||||
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<Value>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: std::future::Future<Output = Value>,
|
||||
{
|
||||
let now = Instant::now();
|
||||
let key_cloned = key.clone();
|
||||
let cell = self
|
||||
.map
|
||||
.entry(key)
|
||||
.or_insert_with(|| Arc::new(OnceCell::new()))
|
||||
.clone();
|
||||
|
||||
if let Some(entry) = cell.get() {
|
||||
if entry.expires_at > now {
|
||||
return Arc::clone(&entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(entry) = cell.get() {
|
||||
if entry.expires_at <= now {
|
||||
self.map
|
||||
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
|
||||
let new_cell = Arc::new(OnceCell::new());
|
||||
self.map.insert(key_cloned.clone(), new_cell.clone());
|
||||
return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let value = fetch_fn().await;
|
||||
let entry = CacheEntry {
|
||||
value: Arc::new(value),
|
||||
expires_at: Instant::now() + ttl,
|
||||
};
|
||||
let _ = cell.set(entry);
|
||||
Arc::clone(&cell.get().unwrap().value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Result};
|
||||
use log::info;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::{fs, path::Path, path::PathBuf};
|
||||
use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf};
|
||||
|
||||
/// Windows 下的开机启动文件夹路径
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -39,10 +39,20 @@ pub fn get_exe_path() -> Result<PathBuf> {
|
||||
pub fn create_shortcut() -> Result<()> {
|
||||
let exe_path = get_exe_path()?;
|
||||
let startup_dir = get_startup_dir()?;
|
||||
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let old_shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let new_shortcut_path = startup_dir.join("Clash Verge.lnk");
|
||||
|
||||
// 如果快捷方式已存在,直接返回成功
|
||||
if shortcut_path.exists() {
|
||||
// 移除旧的快捷方式
|
||||
if old_shortcut_path.exists() {
|
||||
if let Err(e) = fs::remove_file(&old_shortcut_path) {
|
||||
info!(target: "app", "移除旧快捷方式失败: {e}");
|
||||
} else {
|
||||
info!(target: "app", "成功移除旧快捷方式");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果新快捷方式已存在,直接返回成功
|
||||
if new_shortcut_path.exists() {
|
||||
info!(target: "app", "启动快捷方式已存在");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -53,12 +63,14 @@ pub fn create_shortcut() -> Result<()> {
|
||||
$Shortcut = $WshShell.CreateShortcut('{}'); \
|
||||
$Shortcut.TargetPath = '{}'; \
|
||||
$Shortcut.Save()",
|
||||
shortcut_path.to_string_lossy().replace("\\", "\\\\"),
|
||||
new_shortcut_path.to_string_lossy().replace("\\", "\\\\"),
|
||||
exe_path.to_string_lossy().replace("\\", "\\\\")
|
||||
);
|
||||
|
||||
let output = std::process::Command::new("powershell")
|
||||
.args(["-Command", &powershell_command])
|
||||
// 隐藏 PowerShell 窗口
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||
.output()
|
||||
.map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?;
|
||||
|
||||
@@ -75,18 +87,29 @@ pub fn create_shortcut() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn remove_shortcut() -> Result<()> {
|
||||
let startup_dir = get_startup_dir()?;
|
||||
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let old_shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let new_shortcut_path = startup_dir.join("Clash Verge.lnk");
|
||||
|
||||
// 如果快捷方式不存在,直接返回成功
|
||||
if !shortcut_path.exists() {
|
||||
info!(target: "app", "启动快捷方式不存在,无需删除");
|
||||
return Ok(());
|
||||
let mut removed_any = false;
|
||||
|
||||
// 删除旧的快捷方式
|
||||
if old_shortcut_path.exists() {
|
||||
fs::remove_file(&old_shortcut_path).map_err(|e| anyhow!("删除旧快捷方式失败: {}", e))?;
|
||||
info!(target: "app", "成功删除旧启动快捷方式");
|
||||
removed_any = true;
|
||||
}
|
||||
|
||||
// 删除快捷方式
|
||||
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("删除快捷方式失败: {}", e))?;
|
||||
// 删除新的快捷方式
|
||||
if new_shortcut_path.exists() {
|
||||
fs::remove_file(&new_shortcut_path).map_err(|e| anyhow!("删除快捷方式失败: {}", e))?;
|
||||
info!(target: "app", "成功删除启动快捷方式");
|
||||
removed_any = true;
|
||||
}
|
||||
|
||||
if !removed_any {
|
||||
info!(target: "app", "启动快捷方式不存在,无需删除");
|
||||
}
|
||||
|
||||
info!(target: "app", "成功删除启动快捷方式");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -94,9 +117,9 @@ pub fn remove_shortcut() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_shortcut_enabled() -> Result<bool> {
|
||||
let startup_dir = get_startup_dir()?;
|
||||
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let new_shortcut_path = startup_dir.join("Clash Verge.lnk");
|
||||
|
||||
Ok(shortcut_path.exists())
|
||||
Ok(new_shortcut_path.exists())
|
||||
}
|
||||
|
||||
// 非 Windows 平台使用的空方法
|
||||
|
||||
@@ -94,7 +94,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
|
||||
// 如果无法获取系统目录,则回退到可执行文件目录
|
||||
let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID);
|
||||
log::warn!(target: "app", "Using fallback data directory: {:?}", fallback_dir);
|
||||
log::warn!(target: "app", "Using fallback data directory: {fallback_dir:?}");
|
||||
return Ok(fallback_dir);
|
||||
}
|
||||
};
|
||||
@@ -102,7 +102,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
match app_handle.path().data_dir() {
|
||||
Ok(dir) => Ok(dir.join(APP_ID)),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get the app home directory: {}", e);
|
||||
log::error!(target: "app", "Failed to get the app home directory: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ pub fn app_resources_dir() -> Result<PathBuf> {
|
||||
match app_handle.path().resource_dir() {
|
||||
Ok(dir) => Ok(dir.join("resources")),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get the resource directory: {}", e);
|
||||
log::error!(target: "app", "Failed to get the resource directory: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get the resource directory"))
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,7 @@ pub fn service_log_file() -> Result<PathBuf> {
|
||||
let log_dir = app_logs_dir()?.join("service");
|
||||
|
||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||
let log_file = format!("{}.log", local_time);
|
||||
let log_file = format!("{local_time}.log");
|
||||
let log_file = log_dir.join(log_file);
|
||||
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
|
||||
@@ -125,19 +125,6 @@ pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn is_monochrome_image_from_bytes(data: &[u8]) -> anyhow::Result<bool> {
|
||||
let img = image::load_from_memory(data)?;
|
||||
let rgb_img = img.to_rgb8();
|
||||
|
||||
for pixel in rgb_img.pixels() {
|
||||
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn linux_elevator() -> String {
|
||||
use std::process::Command;
|
||||
@@ -176,39 +163,3 @@ macro_rules! t {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// 将字节数转换为可读的流量字符串
|
||||
/// 支持 B/s、KB/s、MB/s、GB/s 的自动转换
|
||||
///
|
||||
/// # Examples
|
||||
/// ```not_run
|
||||
/// format_bytes_speed(1000) // returns "1000B/s"
|
||||
/// format_bytes_speed(1024) // returns "1.0KB/s"
|
||||
/// format_bytes_speed(1024 * 1024) // returns "1.0MB/s"
|
||||
/// ```
|
||||
/// ```
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn format_bytes_speed(speed: u64) -> String {
|
||||
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
||||
let mut size = speed as f64;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size >= 1000.0 && unit_index < UNITS.len() - 1 {
|
||||
size /= 1024.0;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
format!("{:.1}{}/s", size, UNITS[unit_index])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_format_bytes_speed() {
|
||||
assert_eq!(format_bytes_speed(0), "0.0B/s");
|
||||
assert_eq!(format_bytes_speed(1023), "1.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 500), "500.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024 * 2), "2.0MB/s");
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ static TRANSLATIONS: Lazy<HashMap<String, Value>> = Lazy::new(|| {
|
||||
|
||||
if let Some(locales_dir) = get_locales_dir() {
|
||||
for lang in get_supported_languages() {
|
||||
let file_path = locales_dir.join(format!("{}.json", lang));
|
||||
let file_path = locales_dir.join(format!("{lang}.json"));
|
||||
if let Ok(content) = fs::read_to_string(file_path) {
|
||||
if let Ok(json) = serde_json::from_str(&content) {
|
||||
translations.insert(lang.to_string(), json);
|
||||
@@ -59,7 +59,7 @@ fn get_system_language() -> String {
|
||||
|
||||
pub fn t(key: &str) -> String {
|
||||
let current_lang = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.language
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
|
||||
@@ -25,13 +25,13 @@ fn init_log() -> Result<()> {
|
||||
let _ = fs::create_dir_all(&log_dir);
|
||||
}
|
||||
|
||||
let log_level = Config::verge().data().get_log_level();
|
||||
let log_level = Config::verge().latest_ref().get_log_level();
|
||||
if log_level == LevelFilter::Off {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||
let log_file = format!("{}.log", local_time);
|
||||
let log_file = format!("{local_time}.log");
|
||||
let log_file = log_dir.join(log_file);
|
||||
|
||||
let log_pattern = match log_level {
|
||||
@@ -74,7 +74,7 @@ pub fn delete_log() -> Result<()> {
|
||||
|
||||
let auto_log_clean = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.data();
|
||||
let verge = verge.latest_ref();
|
||||
verge.auto_log_clean.unwrap_or(0)
|
||||
};
|
||||
|
||||
@@ -405,7 +405,7 @@ pub async fn startup_script() -> Result<()> {
|
||||
|
||||
let script_path = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
verge.startup_script.clone().unwrap_or("".to_string())
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod i18n;
|
||||
pub mod init;
|
||||
pub mod logging;
|
||||
pub mod network;
|
||||
pub mod notification;
|
||||
pub mod resolve;
|
||||
pub mod server;
|
||||
pub mod tmpl;
|
||||
|
||||
+15
-123
@@ -1,8 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest::{Client, ClientBuilder, Proxy, RequestBuilder, Response};
|
||||
use std::{
|
||||
sync::{Arc, Mutex, Once},
|
||||
sync::{Arc, Once},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
@@ -78,7 +79,7 @@ impl NetworkManager {
|
||||
.build()
|
||||
.expect("Failed to build no_proxy client");
|
||||
|
||||
let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock().unwrap();
|
||||
let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock();
|
||||
*no_proxy_guard = Some(no_proxy_client);
|
||||
|
||||
logging!(info, Type::Network, true, "网络管理器初始化完成");
|
||||
@@ -87,16 +88,16 @@ impl NetworkManager {
|
||||
}
|
||||
|
||||
fn record_connection_error(&self, error: &str) {
|
||||
let mut last_error = self.last_connection_error.lock().unwrap();
|
||||
let mut last_error = self.last_connection_error.lock();
|
||||
*last_error = Some((Instant::now(), error.to_string()));
|
||||
|
||||
let mut error_count = self.connection_error_count.lock().unwrap();
|
||||
let mut error_count = self.connection_error_count.lock();
|
||||
*error_count += 1;
|
||||
}
|
||||
|
||||
fn should_reset_clients(&self) -> bool {
|
||||
let error_count = *self.connection_error_count.lock().unwrap();
|
||||
let last_error = self.last_connection_error.lock().unwrap();
|
||||
let error_count = *self.connection_error_count.lock();
|
||||
let last_error = self.last_connection_error.lock();
|
||||
|
||||
if error_count > 5 {
|
||||
return true;
|
||||
@@ -114,132 +115,23 @@ impl NetworkManager {
|
||||
pub fn reset_clients(&self) {
|
||||
logging!(info, Type::Network, true, "正在重置所有HTTP客户端");
|
||||
{
|
||||
let mut client = self.self_proxy_client.lock().unwrap();
|
||||
let mut client = self.self_proxy_client.lock();
|
||||
*client = None;
|
||||
}
|
||||
{
|
||||
let mut client = self.system_proxy_client.lock().unwrap();
|
||||
let mut client = self.system_proxy_client.lock();
|
||||
*client = None;
|
||||
}
|
||||
{
|
||||
let mut client = self.no_proxy_client.lock().unwrap();
|
||||
let mut client = self.no_proxy_client.lock();
|
||||
*client = None;
|
||||
}
|
||||
{
|
||||
let mut error_count = self.connection_error_count.lock().unwrap();
|
||||
let mut error_count = self.connection_error_count.lock();
|
||||
*error_count = 0;
|
||||
}
|
||||
}
|
||||
/*
|
||||
/// 获取或创建自代理客户端
|
||||
fn get_or_create_self_proxy_client(&self) -> Client {
|
||||
if self.should_reset_clients() {
|
||||
self.reset_clients();
|
||||
}
|
||||
|
||||
let mut client_guard = self.self_proxy_client.lock().unwrap();
|
||||
|
||||
if client_guard.is_none() {
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
let mut builder = ClientBuilder::new()
|
||||
.use_rustls_tls()
|
||||
.pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
|
||||
.pool_idle_timeout(POOL_IDLE_TIMEOUT)
|
||||
.connect_timeout(DEFAULT_CONNECT_TIMEOUT)
|
||||
.timeout(DEFAULT_REQUEST_TIMEOUT)
|
||||
.http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE)
|
||||
.http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE)
|
||||
.http2_adaptive_window(true)
|
||||
.http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL))
|
||||
.http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT)
|
||||
.http2_max_frame_size(H2_MAX_FRAME_SIZE)
|
||||
.tcp_keepalive(Some(Duration::from_secs(10)))
|
||||
.http2_max_header_list_size(16 * 1024);
|
||||
|
||||
if let Ok(proxy) = Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
|
||||
let client = builder.build().expect("Failed to build self_proxy client");
|
||||
*client_guard = Some(client);
|
||||
}
|
||||
|
||||
client_guard.as_ref().unwrap().clone()
|
||||
}
|
||||
|
||||
/// 获取或创建系统代理客户端
|
||||
fn get_or_create_system_proxy_client(&self) -> Client {
|
||||
if self.should_reset_clients() {
|
||||
self.reset_clients();
|
||||
}
|
||||
|
||||
let mut client_guard = self.system_proxy_client.lock().unwrap();
|
||||
|
||||
if client_guard.is_none() {
|
||||
use sysproxy::Sysproxy;
|
||||
|
||||
let mut builder = ClientBuilder::new()
|
||||
.use_rustls_tls()
|
||||
.pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
|
||||
.pool_idle_timeout(POOL_IDLE_TIMEOUT)
|
||||
.connect_timeout(DEFAULT_CONNECT_TIMEOUT)
|
||||
.timeout(DEFAULT_REQUEST_TIMEOUT)
|
||||
.http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE)
|
||||
.http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE)
|
||||
.http2_adaptive_window(true)
|
||||
.http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL))
|
||||
.http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT)
|
||||
.http2_max_frame_size(H2_MAX_FRAME_SIZE)
|
||||
.tcp_keepalive(Some(Duration::from_secs(10)))
|
||||
.http2_max_header_list_size(16 * 1024);
|
||||
|
||||
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
|
||||
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
||||
|
||||
if let Ok(proxy) = Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
let client = builder
|
||||
.build()
|
||||
.expect("Failed to build system_proxy client");
|
||||
*client_guard = Some(client);
|
||||
}
|
||||
|
||||
client_guard.as_ref().unwrap().clone()
|
||||
}
|
||||
|
||||
/// 根据代理设置选择合适的客户端
|
||||
pub fn get_client(&self, proxy_type: ProxyType) -> Client {
|
||||
match proxy_type {
|
||||
ProxyType::NoProxy => {
|
||||
let client_guard = self.no_proxy_client.lock().unwrap();
|
||||
client_guard.as_ref().unwrap().clone()
|
||||
}
|
||||
ProxyType::SelfProxy => self.get_or_create_self_proxy_client(),
|
||||
ProxyType::SystemProxy => self.get_or_create_system_proxy_client(),
|
||||
}
|
||||
}
|
||||
*/
|
||||
/// 创建带有自定义选项的HTTP请求
|
||||
pub fn create_request(
|
||||
&self,
|
||||
@@ -279,9 +171,9 @@ impl NetworkManager {
|
||||
}
|
||||
ProxyType::Localhost => {
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
@@ -322,7 +214,7 @@ impl NetworkManager {
|
||||
use crate::utils::resolve::VERSION;
|
||||
|
||||
let version = match VERSION.get() {
|
||||
Some(v) => format!("clash-verge/v{}", v),
|
||||
Some(v) => format!("clash-verge/v{v}"),
|
||||
None => "clash-verge/unknown".to_string(),
|
||||
};
|
||||
|
||||
@@ -401,7 +293,7 @@ impl NetworkManager {
|
||||
let result = tokio::select! {
|
||||
result = request.send() => result,
|
||||
_ = cancel_rx => {
|
||||
self.record_connection_error(&format!("Request interrupted for: {}", url));
|
||||
self.record_connection_error(&format!("Request interrupted for: {url}"));
|
||||
return Err(anyhow::anyhow!("Request interrupted after {} seconds", timeout_duration));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
|
||||
pub enum NotificationEvent<'a> {
|
||||
DashboardToggled,
|
||||
ClashModeChanged {
|
||||
mode: &'a str,
|
||||
},
|
||||
SystemProxyToggled,
|
||||
TunModeToggled,
|
||||
LightweightModeEntered,
|
||||
AppQuit,
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHidden,
|
||||
}
|
||||
|
||||
fn notify(app: &AppHandle, title: &str, body: &str) {
|
||||
app.notification()
|
||||
.builder()
|
||||
.title(title)
|
||||
.body(body)
|
||||
.show()
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn notify_event(app: &AppHandle, event: NotificationEvent) {
|
||||
use crate::utils::i18n::t;
|
||||
match event {
|
||||
NotificationEvent::DashboardToggled => {
|
||||
notify(app, &t("DashboardToggledTitle"), &t("DashboardToggledBody"));
|
||||
}
|
||||
NotificationEvent::ClashModeChanged { mode } => {
|
||||
notify(
|
||||
app,
|
||||
&t("ClashModeChangedTitle"),
|
||||
&t_with_args("ClashModeChangedBody", mode),
|
||||
);
|
||||
}
|
||||
NotificationEvent::SystemProxyToggled => {
|
||||
notify(
|
||||
app,
|
||||
&t("SystemProxyToggledTitle"),
|
||||
&t("SystemProxyToggledBody"),
|
||||
);
|
||||
}
|
||||
NotificationEvent::TunModeToggled => {
|
||||
notify(app, &t("TunModeToggledTitle"), &t("TunModeToggledBody"));
|
||||
}
|
||||
NotificationEvent::LightweightModeEntered => {
|
||||
notify(
|
||||
app,
|
||||
&t("LightweightModeEnteredTitle"),
|
||||
&t("LightweightModeEnteredBody"),
|
||||
);
|
||||
}
|
||||
NotificationEvent::AppQuit => {
|
||||
notify(app, &t("AppQuitTitle"), &t("AppQuitBody"));
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
NotificationEvent::AppHidden => {
|
||||
notify(app, &t("AppHiddenTitle"), &t("AppHiddenBody"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数,带参数的i18n
|
||||
fn t_with_args(key: &str, mode: &str) -> String {
|
||||
use crate::utils::i18n::t;
|
||||
t(key).replace("{mode}", mode)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use scopeguard;
|
||||
use serde_yaml::Mapping;
|
||||
use std::{
|
||||
sync::Arc,
|
||||
@@ -51,14 +52,12 @@ pub enum UiReadyStage {
|
||||
#[derive(Debug)]
|
||||
struct UiReadyState {
|
||||
stage: RwLock<UiReadyStage>,
|
||||
last_update: RwLock<Instant>,
|
||||
}
|
||||
|
||||
impl Default for UiReadyState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stage: RwLock::new(UiReadyStage::NotStarted),
|
||||
last_update: RwLock::new(Instant::now()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,20 +81,8 @@ fn get_ui_ready_state() -> &'static Arc<UiReadyState> {
|
||||
pub fn update_ui_ready_stage(stage: UiReadyStage) {
|
||||
let state = get_ui_ready_state();
|
||||
let mut stage_lock = state.stage.write();
|
||||
let mut time_lock = state.last_update.write();
|
||||
|
||||
*stage_lock = stage;
|
||||
*time_lock = Instant::now();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI准备阶段更新: {:?}, 耗时: {:?}ms",
|
||||
stage,
|
||||
time_lock.elapsed().as_millis()
|
||||
);
|
||||
|
||||
// 如果是最终阶段,标记UI完全就绪
|
||||
if stage == UiReadyStage::Ready {
|
||||
mark_ui_ready();
|
||||
@@ -118,9 +105,7 @@ pub fn reset_ui_ready() {
|
||||
{
|
||||
let state = get_ui_ready_state();
|
||||
let mut stage = state.stage.write();
|
||||
let mut time = state.last_update.write();
|
||||
*stage = UiReadyStage::NotStarted;
|
||||
*time = Instant::now();
|
||||
}
|
||||
logging!(info, Type::Window, true, "UI就绪状态已重置");
|
||||
}
|
||||
@@ -133,10 +118,10 @@ pub async fn find_unused_port() -> Result<u16> {
|
||||
}
|
||||
Err(_) => {
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
log::warn!(target: "app", "use default port: {}", port);
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
log::warn!(target: "app", "use default port: {port}");
|
||||
Ok(port)
|
||||
}
|
||||
}
|
||||
@@ -175,7 +160,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
// 启动时清理冗余的 Profile 文件
|
||||
logging!(info, Type::Setup, true, "清理冗余的Profile文件...");
|
||||
let profiles = Config::profiles();
|
||||
if let Err(e) = profiles.latest().auto_cleanup() {
|
||||
if let Err(e) = profiles.latest_ref().auto_cleanup() {
|
||||
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
|
||||
} else {
|
||||
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
|
||||
@@ -219,7 +204,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
);
|
||||
|
||||
// 创建窗口
|
||||
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
|
||||
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if is_silent_start {
|
||||
@@ -294,6 +279,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
|
||||
if !is_show {
|
||||
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
|
||||
lightweight::set_lightweight_mode(true);
|
||||
handle::Handle::notify_startup_completed();
|
||||
return false;
|
||||
}
|
||||
@@ -337,6 +323,12 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
|
||||
*creating = (true, Instant::now());
|
||||
|
||||
// ScopeGuard 确保创建状态重置,防止 webview 卡死
|
||||
let _guard = scopeguard::guard(creating, |mut creating_guard| {
|
||||
*creating_guard = (false, Instant::now());
|
||||
logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置");
|
||||
});
|
||||
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
&handle::Handle::global().app_handle().unwrap(),
|
||||
"main", /* the unique window label */
|
||||
@@ -419,8 +411,6 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
Ok(newly_created_window) => {
|
||||
logging!(debug, Type::Window, true, "主窗口实例创建成功");
|
||||
|
||||
*creating = (false, Instant::now());
|
||||
|
||||
update_ui_ready_stage(UiReadyStage::NotStarted);
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
@@ -534,14 +524,13 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
|
||||
*creating = (false, Instant::now()); // Reset the creating state if window creation failed
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
log::info!(target:"app", "received deep link: {}", param);
|
||||
log::info!(target:"app", "received deep link: {param}");
|
||||
|
||||
let param_str = if param.starts_with("[") && param.len() > 4 {
|
||||
param
|
||||
@@ -579,13 +568,13 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
|
||||
match url_param {
|
||||
Some(url) => {
|
||||
log::info!(target:"app", "decoded subscription url: {}", url);
|
||||
log::info!(target:"app", "decoded subscription url: {url}");
|
||||
|
||||
create_window(false);
|
||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
||||
Ok(item) => {
|
||||
let uid = item.uid.clone().unwrap();
|
||||
let _ = wrap_err!(Config::profiles().data().append_item(item));
|
||||
let _ = wrap_err!(Config::profiles().data_mut().append_item(item));
|
||||
handle::Handle::notice_message("import_sub_url::ok", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -603,12 +592,15 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
async fn resolve_random_port_config() -> Result<()> {
|
||||
let verge_config = Config::verge();
|
||||
let clash_config = Config::clash();
|
||||
let enable_random_port = verge_config.latest().enable_random_port.unwrap_or(false);
|
||||
let enable_random_port = verge_config
|
||||
.latest_ref()
|
||||
.enable_random_port
|
||||
.unwrap_or(false);
|
||||
|
||||
let default_port = verge_config
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(clash_config.data().get_mixed_port());
|
||||
.unwrap_or(clash_config.latest_ref().get_mixed_port());
|
||||
|
||||
let port = if enable_random_port {
|
||||
find_unused_port().await.unwrap_or(default_port)
|
||||
@@ -620,7 +612,7 @@ async fn resolve_random_port_config() -> Result<()> {
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let verge_config_accessor = Config::verge();
|
||||
let mut verge_data = verge_config_accessor.data();
|
||||
let mut verge_data = verge_config_accessor.data_mut();
|
||||
verge_data.patch_config(IVerge {
|
||||
verge_mixed_port: Some(port_to_save),
|
||||
..IVerge::default()
|
||||
@@ -631,7 +623,7 @@ async fn resolve_random_port_config() -> Result<()> {
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let clash_config_accessor = Config::clash(); // Extend lifetime of the accessor
|
||||
let mut clash_data = clash_config_accessor.data(); // Access within blocking task, made mutable
|
||||
let mut clash_data = clash_config_accessor.data_mut(); // Access within blocking task, made mutable
|
||||
let mut mapping = Mapping::new();
|
||||
mapping.insert("mixed-port".into(), port_to_save.into());
|
||||
clash_data.patch_config(mapping);
|
||||
|
||||
@@ -56,15 +56,15 @@ pub fn embed_server() {
|
||||
|
||||
let pac = warp::path!("commands" / "pac").map(move || {
|
||||
let content = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.pac_file_content
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_PAC.to_string());
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let content = content.replace("%mixed-port%", &format!("{}", port));
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
let content = content.replace("%mixed-port%", &format!("{port}"));
|
||||
warp::http::Response::builder()
|
||||
.header("Content-Type", "application/x-ns-proxy-autoconfig")
|
||||
.body(content)
|
||||
|
||||
@@ -4,6 +4,14 @@ use tauri::{Manager, WebviewWindow, Wry};
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use scopeguard;
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// 窗口操作结果
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum WindowOperationResult {
|
||||
@@ -34,25 +42,71 @@ pub enum WindowState {
|
||||
NotExist,
|
||||
}
|
||||
|
||||
// 窗口操作防抖机制
|
||||
static WINDOW_OPERATION_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
|
||||
static WINDOW_OPERATION_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
|
||||
const WINDOW_OPERATION_DEBOUNCE_MS: u64 = 500;
|
||||
|
||||
fn get_window_operation_debounce() -> &'static Mutex<Instant> {
|
||||
WINDOW_OPERATION_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
|
||||
}
|
||||
|
||||
fn should_handle_window_operation() -> bool {
|
||||
if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) {
|
||||
log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用");
|
||||
return false;
|
||||
}
|
||||
|
||||
let debounce_lock = get_window_operation_debounce();
|
||||
let mut last_operation = debounce_lock.lock();
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(*last_operation);
|
||||
|
||||
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
|
||||
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||
|
||||
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
|
||||
*last_operation = now;
|
||||
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
|
||||
log::info!(target: "app", "[防抖] 窗口操作被允许执行");
|
||||
true
|
||||
} else {
|
||||
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
|
||||
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_window_operation() {
|
||||
WINDOW_OPERATION_IN_PROGRESS.store(false, Ordering::Release);
|
||||
}
|
||||
|
||||
/// 统一的窗口管理器
|
||||
pub struct WindowManager;
|
||||
|
||||
impl WindowManager {
|
||||
pub fn get_main_window_state() -> WindowState {
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
WindowState::Minimized
|
||||
} else if window.is_visible().unwrap_or(false) {
|
||||
if window.is_focused().unwrap_or(false) {
|
||||
match Self::get_main_window() {
|
||||
Some(window) => {
|
||||
let is_minimized = window.is_minimized().unwrap_or(false);
|
||||
let is_visible = window.is_visible().unwrap_or(false);
|
||||
let is_focused = window.is_focused().unwrap_or(false);
|
||||
|
||||
if is_minimized {
|
||||
return WindowState::Minimized;
|
||||
}
|
||||
|
||||
if !is_visible {
|
||||
return WindowState::Hidden;
|
||||
}
|
||||
|
||||
if is_focused {
|
||||
WindowState::VisibleFocused
|
||||
} else {
|
||||
WindowState::VisibleUnfocused
|
||||
}
|
||||
} else {
|
||||
WindowState::Hidden
|
||||
}
|
||||
} else {
|
||||
WindowState::NotExist
|
||||
None => WindowState::NotExist,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +119,14 @@ impl WindowManager {
|
||||
|
||||
/// 智能显示主窗口
|
||||
pub fn show_main_window() -> WindowOperationResult {
|
||||
// 防抖检查
|
||||
if !should_handle_window_operation() {
|
||||
return WindowOperationResult::NoAction;
|
||||
}
|
||||
let _guard = scopeguard::guard((), |_| {
|
||||
finish_window_operation();
|
||||
});
|
||||
|
||||
logging!(info, Type::Window, true, "开始智能显示主窗口");
|
||||
logging!(
|
||||
debug,
|
||||
@@ -80,8 +142,11 @@ impl WindowManager {
|
||||
WindowState::NotExist => {
|
||||
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
|
||||
if Self::create_new_window() {
|
||||
logging!(info, Type::Window, true, "窗口创建成功");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
WindowOperationResult::Created
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "窗口创建失败");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
@@ -91,6 +156,16 @@ impl WindowManager {
|
||||
}
|
||||
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
let state_after_check = Self::get_main_window_state();
|
||||
if state_after_check == WindowState::VisibleFocused {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"窗口在检查期间已变为可见和有焦点状态"
|
||||
);
|
||||
return WindowOperationResult::NoAction;
|
||||
}
|
||||
Self::activate_window(&window)
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
@@ -101,6 +176,14 @@ impl WindowManager {
|
||||
|
||||
/// 切换主窗口显示状态(显示/隐藏)
|
||||
pub fn toggle_main_window() -> WindowOperationResult {
|
||||
// 防抖检查
|
||||
if !should_handle_window_operation() {
|
||||
return WindowOperationResult::NoAction;
|
||||
}
|
||||
let _guard = scopeguard::guard((), |_| {
|
||||
finish_window_operation();
|
||||
});
|
||||
|
||||
logging!(info, Type::Window, true, "开始切换主窗口显示状态");
|
||||
|
||||
let current_state = Self::get_main_window_state();
|
||||
@@ -108,37 +191,61 @@ impl WindowManager {
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"当前窗口状态: {:?}",
|
||||
current_state
|
||||
"当前窗口状态: {:?} | 详细状态: {}",
|
||||
current_state,
|
||||
Self::get_window_status_info()
|
||||
);
|
||||
|
||||
match current_state {
|
||||
WindowState::NotExist => {
|
||||
// 窗口不存在,创建新窗口
|
||||
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
|
||||
// 由于已经有防抖保护,直接调用内部方法
|
||||
if Self::create_new_window() {
|
||||
WindowOperationResult::Created
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
WindowState::VisibleFocused => {
|
||||
// 窗口可见且有焦点,隐藏它
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
if window.hide().is_ok() {
|
||||
logging!(info, Type::Window, true, "窗口已隐藏");
|
||||
WindowOperationResult::Hidden
|
||||
WindowState::VisibleFocused | WindowState::VisibleUnfocused => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"窗口可见(焦点状态: {}),将隐藏窗口",
|
||||
if current_state == WindowState::VisibleFocused {
|
||||
"有焦点"
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
"无焦点"
|
||||
}
|
||||
);
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
match window.hide() {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Window, true, "窗口已成功隐藏");
|
||||
WindowOperationResult::Hidden
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "无法获取窗口实例");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
|
||||
// 窗口存在但不可见或无焦点,激活它
|
||||
WindowState::Minimized | WindowState::Hidden => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"窗口存在但被隐藏或最小化,将激活窗口"
|
||||
);
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
Self::activate_window(&window)
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "无法获取窗口实例");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
@@ -216,17 +323,21 @@ impl WindowManager {
|
||||
pub fn hide_main_window() -> WindowOperationResult {
|
||||
logging!(info, Type::Window, true, "开始隐藏主窗口");
|
||||
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
if window.hide().is_ok() {
|
||||
logging!(info, Type::Window, true, "窗口已隐藏");
|
||||
WindowOperationResult::Hidden
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "隐藏窗口失败");
|
||||
WindowOperationResult::Failed
|
||||
match Self::get_main_window() {
|
||||
Some(window) => match window.hide() {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Window, true, "窗口已隐藏");
|
||||
WindowOperationResult::Hidden
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
},
|
||||
None => {
|
||||
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
|
||||
WindowOperationResult::NoAction
|
||||
}
|
||||
} else {
|
||||
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
|
||||
WindowOperationResult::NoAction
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +362,7 @@ impl WindowManager {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 创建新窗口现有的实现
|
||||
/// 创建新窗口,防抖避免重复调用
|
||||
fn create_new_window() -> bool {
|
||||
use crate::utils::resolve;
|
||||
resolve::create_window(true)
|
||||
@@ -265,8 +376,7 @@ impl WindowManager {
|
||||
let is_minimized = Self::is_main_window_minimized();
|
||||
|
||||
format!(
|
||||
"窗口状态: {:?} | 可见: {} | 有焦点: {} | 最小化: {}",
|
||||
state, is_visible, is_focused, is_minimized
|
||||
"窗口状态: {state:?} | 可见: {is_visible} | 有焦点: {is_focused} | 最小化: {is_minimized}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ name = "mihomo_api"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12.20", features = ["json"] }
|
||||
reqwest = { version = "0.12.22", features = ["json"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
tokio = { version = "1.45.1", features = ["rt", "macros", "time"] }
|
||||
tokio = { version = "1.46.1", features = ["rt", "macros", "time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.3.1",
|
||||
"version": "2.3.2",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { AppDataProvider } from "./providers/app-data-provider";
|
||||
import Layout from "./pages/_layout";
|
||||
import { useNotificationPermission } from "./hooks/useNotificationPermission";
|
||||
|
||||
function App() {
|
||||
useNotificationPermission();
|
||||
return (
|
||||
<AppDataProvider>
|
||||
<Layout />
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Box, SvgIcon, TextField, styled } from "@mui/material";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { ChangeEvent, useEffect, useRef, useState, useMemo } from "react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
|
||||
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
|
||||
import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type SearchState = {
|
||||
text: string;
|
||||
@@ -56,9 +55,28 @@ export const BaseSearchBox = (props: SearchProps) => {
|
||||
inheritViewBox: true,
|
||||
};
|
||||
|
||||
// 验证正则表达式的辅助函数
|
||||
const validateRegex = (pattern: string) => {
|
||||
if (!pattern) return true;
|
||||
try {
|
||||
new RegExp(pattern);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const createMatcher = useMemo(() => {
|
||||
return (searchText: string) => {
|
||||
try {
|
||||
// 当启用正则表达式验证是否合规
|
||||
if (useRegularExpression && searchText) {
|
||||
const isValid = validateRegex(searchText);
|
||||
if (!isValid) {
|
||||
throw new Error(t("Invalid regular expression"));
|
||||
}
|
||||
}
|
||||
|
||||
return (content: string) => {
|
||||
if (!searchText) return true;
|
||||
|
||||
@@ -76,16 +94,15 @@ export const BaseSearchBox = (props: SearchProps) => {
|
||||
return item.includes(searchItem);
|
||||
};
|
||||
} catch (err) {
|
||||
setErrorMessage(`${err}`);
|
||||
return () => true;
|
||||
setErrorMessage(err instanceof Error ? err.message : `${err}`);
|
||||
return () => false; // 无效正则规则 不匹配值
|
||||
}
|
||||
};
|
||||
}, [matchCase, matchWholeWord, useRegularExpression]);
|
||||
}, [matchCase, matchWholeWord, useRegularExpression, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) return;
|
||||
const value = inputRef.current.value;
|
||||
setErrorMessage("");
|
||||
props.onSearch(createMatcher(value), {
|
||||
text: value,
|
||||
matchCase,
|
||||
@@ -97,6 +114,15 @@ export const BaseSearchBox = (props: SearchProps) => {
|
||||
const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const value = e.target?.value ?? "";
|
||||
setErrorMessage("");
|
||||
|
||||
// 验证正则表达式
|
||||
if (useRegularExpression && value) {
|
||||
const isValid = validateRegex(value);
|
||||
if (!isValid) {
|
||||
setErrorMessage(t("Invalid regular expression"));
|
||||
}
|
||||
}
|
||||
|
||||
props.onSearch(createMatcher(value), {
|
||||
text: value,
|
||||
matchCase,
|
||||
@@ -106,7 +132,7 @@ export const BaseSearchBox = (props: SearchProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title={errorMessage} placement="bottom-start">
|
||||
<Tooltip title={errorMessage || ""} placement="bottom-start">
|
||||
<StyledTextField
|
||||
autoComplete="new-password"
|
||||
inputRef={inputRef}
|
||||
@@ -118,6 +144,7 @@ export const BaseSearchBox = (props: SearchProps) => {
|
||||
placeholder={props.placeholder ?? t("Filter conditions")}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
onChange={onChange}
|
||||
error={!!errorMessage}
|
||||
slotProps={{
|
||||
input: {
|
||||
sx: { pr: 1 },
|
||||
|
||||
@@ -86,9 +86,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
{ label: t("Rule"), value: rule },
|
||||
{
|
||||
label: t("Process"),
|
||||
value: `${metadata.process}${
|
||||
metadata.processPath ? `(${metadata.processPath})` : ""
|
||||
}`,
|
||||
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ""}`,
|
||||
},
|
||||
{ label: t("Time"), value: dayjs(data.start).fromNow() },
|
||||
{
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
|
||||
import { useThemeMode } from "@/services/states";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { t } from "i18next";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
|
||||
interface Props {
|
||||
connections: IConnectionsItem[];
|
||||
@@ -21,11 +22,13 @@ export const ConnectionTable = (props: Props) => {
|
||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||
>({});
|
||||
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
|
||||
() => {
|
||||
const saved = localStorage.getItem("connection-table-widths");
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
},
|
||||
const [columnWidths, setColumnWidths] = useLocalStorage<
|
||||
Record<string, number>
|
||||
>(
|
||||
"connection-table-widths",
|
||||
// server-side value, this is the default value used by server-side rendering (if any)
|
||||
// Do not omit (otherwise a Suspense boundary will be triggered)
|
||||
{},
|
||||
);
|
||||
|
||||
const [columns] = useState<GridColDef[]>([
|
||||
@@ -116,14 +119,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Saving column widths:", columnWidths);
|
||||
localStorage.setItem(
|
||||
"connection-table-widths",
|
||||
JSON.stringify(columnWidths),
|
||||
);
|
||||
}, [columnWidths]);
|
||||
|
||||
const handleColumnResize = (params: GridColumnResizeParams) => {
|
||||
const { colDef, width } = params;
|
||||
console.log("Column resize:", colDef.field, width);
|
||||
|
||||
@@ -17,7 +17,7 @@ const formatUptime = (uptimeMs: number) => {
|
||||
export const ClashInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { version: clashVersion } = useClash();
|
||||
const { clashConfig, sysproxy, rules, uptime } = useAppData();
|
||||
const { clashConfig, rules, uptime, systemProxyAddress } = useAppData();
|
||||
|
||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
|
||||
@@ -42,7 +42,7 @@ export const ClashInfoCard = () => {
|
||||
{t("System Proxy Address")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{sysproxy?.server || "-"}
|
||||
{systemProxyAddress}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
@@ -74,7 +74,14 @@ export const ClashInfoCard = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}, [clashConfig, clashVersion, t, formattedUptime, rules.length, sysproxy]);
|
||||
}, [
|
||||
clashConfig,
|
||||
clashVersion,
|
||||
t,
|
||||
formattedUptime,
|
||||
rules.length,
|
||||
systemProxyAddress,
|
||||
]);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, Typography, Paper, Stack, Fade } from "@mui/material";
|
||||
import { Box, Typography, Paper, Stack } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { closeAllConnections } from "@/services/api";
|
||||
import { patchClashMode } from "@/services/cmds";
|
||||
|
||||
@@ -26,9 +26,10 @@ import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { createAuthSockette } from "@/utils/websocket";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { getConnections, isDebugEnabled, gc } from "@/services/api";
|
||||
import { isDebugEnabled, gc } from "@/services/api";
|
||||
import { ReactNode } from "react";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface MemoryUsage {
|
||||
inuse: number;
|
||||
@@ -161,7 +162,6 @@ export const EnhancedTrafficStats = () => {
|
||||
const { verge } = useVerge();
|
||||
const trafficRef = useRef<EnhancedTrafficGraphRef>(null);
|
||||
const pageVisible = useVisibility();
|
||||
const [isDebug, setIsDebug] = useState(false);
|
||||
|
||||
// 使用AppDataProvider
|
||||
const { connections, uptime } = useAppData();
|
||||
@@ -178,19 +178,16 @@ export const EnhancedTrafficStats = () => {
|
||||
// 是否显示流量图表
|
||||
const trafficGraph = verge?.traffic_graph ?? true;
|
||||
|
||||
// WebSocket引用
|
||||
const socketRefs = useRef<{
|
||||
traffic: ReturnType<typeof createAuthSockette> | null;
|
||||
memory: ReturnType<typeof createAuthSockette> | null;
|
||||
}>({
|
||||
traffic: null,
|
||||
memory: null,
|
||||
});
|
||||
|
||||
// 检查是否支持调试
|
||||
useEffect(() => {
|
||||
isDebugEnabled().then((flag) => setIsDebug(flag));
|
||||
}, []);
|
||||
// TODO: merge this hook with layout-traffic.tsx
|
||||
const { data: isDebug } = useSWR(
|
||||
`clash-verge-rev-internal://isDebugEnabled`,
|
||||
() => isDebugEnabled(),
|
||||
{
|
||||
// default value before is fetched
|
||||
fallbackData: false,
|
||||
},
|
||||
);
|
||||
|
||||
// 处理流量数据更新 - 使用节流控制更新频率
|
||||
const handleTrafficUpdate = useCallback((event: MessageEvent) => {
|
||||
@@ -260,14 +257,23 @@ export const EnhancedTrafficStats = () => {
|
||||
const { server, secret = "" } = clashInfo;
|
||||
if (!server) return;
|
||||
|
||||
// WebSocket 引用
|
||||
let sockets: {
|
||||
traffic: ReturnType<typeof createAuthSockette> | null;
|
||||
memory: ReturnType<typeof createAuthSockette> | null;
|
||||
} = {
|
||||
traffic: null,
|
||||
memory: null,
|
||||
};
|
||||
|
||||
// 清理现有连接的函数
|
||||
const cleanupSockets = () => {
|
||||
Object.values(socketRefs.current).forEach((socket) => {
|
||||
Object.values(sockets).forEach((socket) => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
socketRefs.current = { traffic: null, memory: null };
|
||||
sockets = { traffic: null, memory: null };
|
||||
};
|
||||
|
||||
// 关闭现有连接
|
||||
@@ -277,44 +283,40 @@ export const EnhancedTrafficStats = () => {
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
|
||||
);
|
||||
socketRefs.current.traffic = createAuthSockette(
|
||||
`${server}/traffic`,
|
||||
secret,
|
||||
{
|
||||
onmessage: handleTrafficUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
|
||||
event,
|
||||
);
|
||||
},
|
||||
onerror: (event) => {
|
||||
console.error(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
|
||||
event,
|
||||
sockets.traffic = createAuthSockette(`${server}/traffic`, secret, {
|
||||
onmessage: handleTrafficUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
|
||||
event,
|
||||
);
|
||||
},
|
||||
onerror: (event) => {
|
||||
console.error(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
|
||||
event,
|
||||
);
|
||||
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
},
|
||||
onclose: (event) => {
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
|
||||
event.code,
|
||||
event.reason,
|
||||
);
|
||||
if (event.code !== 1000 && event.code !== 1001) {
|
||||
console.warn(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
|
||||
);
|
||||
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
},
|
||||
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`,
|
||||
);
|
||||
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
|
||||
sockets.memory = createAuthSockette(`${server}/memory`, secret, {
|
||||
onmessage: handleMemoryUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(
|
||||
@@ -353,18 +355,6 @@ export const EnhancedTrafficStats = () => {
|
||||
return cleanupSockets;
|
||||
}, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]);
|
||||
|
||||
// 组件卸载时清理所有定时器/引用
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
Object.values(socketRefs.current).forEach((socket) => {
|
||||
if (socket) socket.close();
|
||||
});
|
||||
socketRefs.current = { traffic: null, memory: null };
|
||||
} catch {}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 执行垃圾回收
|
||||
const handleGarbageCollection = useCallback(async () => {
|
||||
if (isDebug) {
|
||||
|
||||
@@ -100,11 +100,9 @@ const ProfileDetails = ({
|
||||
}, [current.extra]);
|
||||
|
||||
const trafficPercentage = useMemo(() => {
|
||||
if (!current.extra || !current.extra.total) return 1;
|
||||
return Math.min(
|
||||
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
|
||||
100,
|
||||
);
|
||||
if (!current.extra || !current.extra.total || current.extra.total <= 0)
|
||||
return 0;
|
||||
return Math.min(Math.round((usedTraffic / current.extra.total) * 100), 100);
|
||||
}, [current.extra, usedTraffic]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Skeleton,
|
||||
IconButton,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { Box, Typography, Button, Skeleton, IconButton } from "@mui/material";
|
||||
import {
|
||||
LocationOnOutlined,
|
||||
RefreshOutlined,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useSystemState } from "@/hooks/use-system-state";
|
||||
import { useSystemProxyState } from "@/hooks/use-system-proxy-state";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { getRunningMode } from "@/services/cmds";
|
||||
import { mutate } from "swr";
|
||||
@@ -145,8 +146,9 @@ export const ProxyTunCard: FC = () => {
|
||||
|
||||
const { verge } = useVerge();
|
||||
const { isAdminMode } = useSystemState();
|
||||
const { indicator: systemProxyIndicator } = useSystemProxyState();
|
||||
|
||||
const { enable_system_proxy, enable_tun_mode } = verge ?? {};
|
||||
const { enable_tun_mode } = verge ?? {};
|
||||
|
||||
const updateLocalStatus = async () => {
|
||||
try {
|
||||
@@ -180,7 +182,7 @@ export const ProxyTunCard: FC = () => {
|
||||
const tabDescription = useMemo(() => {
|
||||
if (activeTab === "system") {
|
||||
return {
|
||||
text: enable_system_proxy
|
||||
text: systemProxyIndicator
|
||||
? t("System Proxy Enabled")
|
||||
: t("System Proxy Disabled"),
|
||||
tooltip: t("System Proxy Info"),
|
||||
@@ -195,7 +197,7 @@ export const ProxyTunCard: FC = () => {
|
||||
tooltip: t("TUN Mode Intercept Info"),
|
||||
};
|
||||
}
|
||||
}, [activeTab, enable_system_proxy, enable_tun_mode, isTunAvailable, t]);
|
||||
}, [activeTab, systemProxyIndicator, enable_tun_mode, isTunAvailable, t]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
@@ -214,7 +216,7 @@ export const ProxyTunCard: FC = () => {
|
||||
onClick={() => handleTabChange("system")}
|
||||
icon={ComputerRounded}
|
||||
label={t("System Proxy")}
|
||||
hasIndicator={enable_system_proxy}
|
||||
hasIndicator={systemProxyIndicator}
|
||||
/>
|
||||
<TabButton
|
||||
isActive={activeTab === "tun"}
|
||||
|
||||
@@ -11,9 +11,10 @@ import { TrafficGraph, type TrafficRef } from "./traffic-graph";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { createSockette, createAuthSockette } from "@/utils/websocket";
|
||||
import { createAuthSockette } from "@/utils/websocket";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isDebugEnabled, gc } from "@/services/api";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface MemoryUsage {
|
||||
inuse: number;
|
||||
@@ -31,12 +32,15 @@ export const LayoutTraffic = () => {
|
||||
|
||||
const trafficRef = useRef<TrafficRef>(null);
|
||||
const pageVisible = useVisibility();
|
||||
const [isDebug, setIsDebug] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
isDebugEnabled().then((flag) => setIsDebug(flag));
|
||||
return () => {};
|
||||
}, [isDebug]);
|
||||
const { data: isDebug } = useSWR(
|
||||
"clash-verge-rev-internal://isDebugEnabled",
|
||||
() => isDebugEnabled(),
|
||||
{
|
||||
// default value before is fetched
|
||||
fallbackData: false,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: traffic = { up: 0, down: 0 } } = useSWRSubscription<
|
||||
ITrafficItem,
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { defaultDarkTheme, defaultTheme } from "@/pages/_theme";
|
||||
import { useSetThemeMode, useThemeMode } from "@/services/states";
|
||||
import { alpha, createTheme, Theme as MuiTheme, Shadows } from "@mui/material";
|
||||
import {
|
||||
arSD as arXDataGrid,
|
||||
enUS as enXDataGrid,
|
||||
faIR as faXDataGrid,
|
||||
ruRU as ruXDataGrid,
|
||||
zhCN as zhXDataGrid,
|
||||
} from "@mui/x-data-grid/locales";
|
||||
import {
|
||||
getCurrentWebviewWindow,
|
||||
WebviewWindow,
|
||||
} from "@tauri-apps/api/webviewWindow";
|
||||
import { useSetThemeMode, useThemeMode } from "@/services/states";
|
||||
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import {
|
||||
zhCN as zhXDataGrid,
|
||||
enUS as enXDataGrid,
|
||||
ruRU as ruXDataGrid,
|
||||
faIR as faXDataGrid,
|
||||
arSD as arXDataGrid,
|
||||
} from "@mui/x-data-grid/locales";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Theme as TauriOsTheme } from "@tauri-apps/api/window";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const languagePackMap: Record<string, any> = {
|
||||
zh: { ...zhXDataGrid },
|
||||
@@ -39,6 +39,10 @@ export const useCustomTheme = () => {
|
||||
const mode = useThemeMode();
|
||||
const setMode = useSetThemeMode();
|
||||
|
||||
// 提取用户自定义的背景图URL
|
||||
const userBackgroundImage = theme_setting?.background_image || "";
|
||||
const hasUserBackground = !!userBackgroundImage;
|
||||
|
||||
useEffect(() => {
|
||||
if (theme_mode === "light" || theme_mode === "dark") {
|
||||
setMode(theme_mode);
|
||||
@@ -197,6 +201,22 @@ export const useCustomTheme = () => {
|
||||
"--scrollbar-thumb",
|
||||
mode === "light" ? "#c1c1c1" : "#555555",
|
||||
);
|
||||
|
||||
// 设置背景图相关变量
|
||||
rootEle.style.setProperty(
|
||||
"--user-background-image",
|
||||
hasUserBackground ? `url('${userBackgroundImage}')` : "none",
|
||||
);
|
||||
rootEle.style.setProperty(
|
||||
"--background-blend-mode",
|
||||
setting.background_blend_mode || "normal",
|
||||
);
|
||||
rootEle.style.setProperty(
|
||||
"--background-opacity",
|
||||
setting.background_opacity !== undefined
|
||||
? String(setting.background_opacity)
|
||||
: "1",
|
||||
);
|
||||
}
|
||||
|
||||
let styleElement = document.querySelector("style#verge-theme");
|
||||
@@ -207,7 +227,7 @@ export const useCustomTheme = () => {
|
||||
}
|
||||
|
||||
if (styleElement) {
|
||||
// 添加全局样式,确保所有元素都使用暗色主题
|
||||
// 改进的全局样式,支持用户自定义背景图
|
||||
const globalStyles = `
|
||||
/* 修复滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
@@ -223,9 +243,21 @@ export const useCustomTheme = () => {
|
||||
background-color: ${mode === "light" ? "#a1a1a1" : "#666666"};
|
||||
}
|
||||
|
||||
/* 确保所有元素都使用正确的背景色 */
|
||||
body, html {
|
||||
background-color: var(--background-color) !important;
|
||||
/* 背景图处理 */
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
${
|
||||
hasUserBackground
|
||||
? `
|
||||
background-image: var(--user-background-image);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
background-blend-mode: var(--background-blend-mode);
|
||||
opacity: var(--background-opacity);
|
||||
`
|
||||
: ""
|
||||
}
|
||||
}
|
||||
|
||||
/* 修复可能的白色边框 */
|
||||
@@ -261,7 +293,13 @@ export const useCustomTheme = () => {
|
||||
}, 0);
|
||||
|
||||
return muiTheme;
|
||||
}, [mode, theme_setting, i18n.language]);
|
||||
}, [
|
||||
mode,
|
||||
theme_setting,
|
||||
i18n.language,
|
||||
userBackgroundImage,
|
||||
hasUserBackground,
|
||||
]);
|
||||
|
||||
return { theme };
|
||||
};
|
||||
|
||||
@@ -48,6 +48,10 @@ import MonacoEditor from "react-monaco-editor";
|
||||
import { useThemeMode } from "@/services/states";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import {
|
||||
requestIdleCallback,
|
||||
cancelIdleCallback,
|
||||
} from "foxact/request-idle-callback";
|
||||
|
||||
interface Props {
|
||||
proxiesUid: string;
|
||||
@@ -195,11 +199,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
// 防止异常导致UI卡死
|
||||
}
|
||||
};
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(serialize);
|
||||
} else {
|
||||
setTimeout(serialize, 0);
|
||||
}
|
||||
|
||||
const handle = requestIdleCallback(serialize);
|
||||
return () => {
|
||||
cancelIdleCallback(handle);
|
||||
};
|
||||
}
|
||||
}, [prependSeq, appendSeq, deleteSeq]);
|
||||
|
||||
|
||||
@@ -59,7 +59,9 @@ export const ProfileItem = (props: Props) => {
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: props.id });
|
||||
} = useSortable({
|
||||
id: props.id,
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
@@ -463,7 +465,15 @@ export const ProfileItem = (props: Props) => {
|
||||
>
|
||||
<ProfileBox
|
||||
aria-selected={selected}
|
||||
onClick={() => onSelect(false)}
|
||||
onClick={(e) => {
|
||||
// 如果正在激活中,阻止重复点击
|
||||
if (activating) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
onSelect(false);
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
const { clientX, clientY } = event;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
@@ -484,9 +494,16 @@ export const ProfileItem = (props: Props) => {
|
||||
bottom: 2,
|
||||
zIndex: 10,
|
||||
backdropFilter: "blur(2px)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
>
|
||||
<CircularProgress color="inherit" size={20} />
|
||||
<CircularProgress
|
||||
color="inherit"
|
||||
size={20}
|
||||
sx={{
|
||||
animation: "pulse 1.5s ease-in-out infinite",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box position="relative">
|
||||
@@ -535,6 +552,10 @@ export const ProfileItem = (props: Props) => {
|
||||
disabled={loading}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// 如果正在激活或加载中,阻止更新操作
|
||||
if (activating || loading) {
|
||||
return;
|
||||
}
|
||||
onUpdate(1);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import yaml from "js-yaml";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import yaml from "js-yaml";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
deleteConnection,
|
||||
getGroupProxyDelays,
|
||||
} from "@/services/api";
|
||||
import { forceRefreshProxies } from "@/services/cmds";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseEmpty } from "../base";
|
||||
@@ -341,6 +342,9 @@ export const ProxyGroups = (props: Props) => {
|
||||
|
||||
const { name, now } = group;
|
||||
await updateProxy(name, proxy.name);
|
||||
|
||||
await forceRefreshProxies();
|
||||
|
||||
onProxies();
|
||||
|
||||
// 断开连接
|
||||
@@ -476,31 +480,34 @@ export const ProxyGroups = (props: Props) => {
|
||||
}
|
||||
}, [handleWheel]);
|
||||
|
||||
// 添加窗口大小变化监听和最大高度计算
|
||||
const updateMaxHeight = useCallback(() => {
|
||||
if (!alphabetSelectorRef.current) return;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
const bottomMargin = 60; // 底部边距
|
||||
const topMargin = bottomMargin * 2; // 顶部边距是底部的2倍
|
||||
const availableHeight = windowHeight - (topMargin + bottomMargin);
|
||||
|
||||
// 调整选择器的位置,使其偏下
|
||||
const offsetPercentage =
|
||||
(((topMargin - bottomMargin) / windowHeight) * 100) / 2;
|
||||
alphabetSelectorRef.current.style.top = `calc(48% + ${offsetPercentage}vh)`;
|
||||
|
||||
setMaxHeight(`${availableHeight}px`);
|
||||
}, []);
|
||||
|
||||
// 监听窗口大小变化
|
||||
// layout effect runs before paint
|
||||
useEffect(() => {
|
||||
// 添加窗口大小变化监听和最大高度计算
|
||||
const updateMaxHeight = () => {
|
||||
if (!alphabetSelectorRef.current) return;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
const bottomMargin = 60; // 底部边距
|
||||
const topMargin = bottomMargin * 2; // 顶部边距是底部的2倍
|
||||
const availableHeight = windowHeight - (topMargin + bottomMargin);
|
||||
|
||||
// 调整选择器的位置,使其偏下
|
||||
const offsetPercentage =
|
||||
(((topMargin - bottomMargin) / windowHeight) * 100) / 2;
|
||||
alphabetSelectorRef.current.style.top = `calc(48% + ${offsetPercentage}vh)`;
|
||||
|
||||
setMaxHeight(`${availableHeight}px`);
|
||||
};
|
||||
|
||||
updateMaxHeight();
|
||||
|
||||
window.addEventListener("resize", updateMaxHeight);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateMaxHeight);
|
||||
};
|
||||
}, [updateMaxHeight]);
|
||||
}, []);
|
||||
|
||||
if (mode === "direct") {
|
||||
return <BaseEmpty text={t("clash_mode_direct")} />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useCallback } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { filterSort } from "./use-filter-sort";
|
||||
import { useWindowWidth } from "./use-window-width";
|
||||
@@ -110,7 +110,8 @@ export const useRenderList = (mode: string) => {
|
||||
(mode === "rule" && !groups.length) ||
|
||||
(mode === "global" && proxies.length < 2)
|
||||
) {
|
||||
setTimeout(() => refreshProxy(), 500);
|
||||
const handle = setTimeout(() => refreshProxy(), 500);
|
||||
return () => clearTimeout(handle);
|
||||
}
|
||||
}, [proxiesData, mode, refreshProxy]);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
@@ -30,7 +30,6 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
|
||||
const [dataSource, setDataSource] = useState<BackupFile[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
@@ -91,14 +90,14 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(
|
||||
const dataSource = useMemo<BackupFile[]>(
|
||||
() =>
|
||||
backupFiles.slice(
|
||||
page * DEFAULT_ROWS_PER_PAGE,
|
||||
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE,
|
||||
),
|
||||
);
|
||||
}, [page, backupFiles]);
|
||||
[backupFiles, page],
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
@@ -116,18 +115,10 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<Paper elevation={2} sx={{ padding: 2 }}>
|
||||
<BackupConfigViewer
|
||||
setLoading={setIsLoading}
|
||||
onBackupSuccess={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onSaveSuccess={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onRefresh={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onInit={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onBackupSuccess={fetchAndSetBackupFiles}
|
||||
onSaveSuccess={fetchAndSetBackupFiles}
|
||||
onRefresh={fetchAndSetBackupFiles}
|
||||
onInit={fetchAndSetBackupFiles}
|
||||
/>
|
||||
<Divider sx={{ marginY: 2 }} />
|
||||
<BackupTableViewer
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
@@ -332,54 +332,51 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化nameserver-policy为字符串
|
||||
const formatNameserverPolicy = (policy: any): string => {
|
||||
if (!policy) return "";
|
||||
|
||||
let result: string[] = [];
|
||||
|
||||
Object.entries(policy).forEach(([domain, servers]) => {
|
||||
if (Array.isArray(servers)) {
|
||||
// 处理数组格式的服务器
|
||||
const serversStr = servers.join(";");
|
||||
result.push(`${domain}=${serversStr}`);
|
||||
} else {
|
||||
// 处理单个服务器
|
||||
result.push(`${domain}=${servers}`);
|
||||
}
|
||||
});
|
||||
|
||||
return result.join(", ");
|
||||
};
|
||||
|
||||
// 解析nameserver-policy为对象
|
||||
const parseNameserverPolicy = (str: string): Record<string, any> => {
|
||||
const result: Record<string, any> = {};
|
||||
if (!str) return result;
|
||||
|
||||
str.split(",").forEach((item) => {
|
||||
const parts = item.trim().split("=");
|
||||
if (parts.length < 2) return;
|
||||
// 处理geosite:xxx,yyy格式
|
||||
const ruleRegex = /\s*([^=]+?)\s*=\s*([^,]+)(?:,|$)/g;
|
||||
let match;
|
||||
|
||||
const domain = parts[0].trim();
|
||||
const serversStr = parts.slice(1).join("=").trim();
|
||||
while ((match = ruleRegex.exec(str)) !== null) {
|
||||
const [, domainsPart, serversPart] = match;
|
||||
|
||||
// 检查是否包含多个分号分隔的服务器
|
||||
if (serversStr.includes(";")) {
|
||||
// 多个服务器,作为数组处理
|
||||
result[domain] = serversStr
|
||||
.split(";")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
// 处理域名部分
|
||||
let domains;
|
||||
if (domainsPart.startsWith("geosite:")) {
|
||||
domains = [domainsPart.trim()];
|
||||
} else {
|
||||
// 单个服务器
|
||||
result[domain] = serversStr;
|
||||
domains = [domainsPart.trim()];
|
||||
}
|
||||
});
|
||||
|
||||
// 处理服务器部分
|
||||
const servers = serversPart.split(";").map((s) => s.trim());
|
||||
|
||||
// 为每个域名组分配相同的服务器列表
|
||||
domains.forEach((domain) => {
|
||||
result[domain] = servers;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 格式化nameserver-policy为字符串
|
||||
const formatNameserverPolicy = (policy: any): string => {
|
||||
if (!policy || typeof policy !== "object") return "";
|
||||
|
||||
// 直接将对象转换为字符串格式
|
||||
return Object.entries(policy)
|
||||
.map(([domain, servers]) => {
|
||||
const serversStr = Array.isArray(servers) ? servers.join(";") : servers;
|
||||
return `${domain}=${serversStr}`;
|
||||
})
|
||||
.join(", ");
|
||||
};
|
||||
|
||||
// 格式化hosts为字符串
|
||||
const formatHosts = (hosts: any): string => {
|
||||
if (!hosts || typeof hosts !== "object") return "";
|
||||
@@ -442,7 +439,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
// 解析列表字符串为数组
|
||||
const parseList = (str: string): string[] => {
|
||||
if (!str) return [];
|
||||
if (!str?.trim()) return [];
|
||||
return str
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
@@ -472,28 +469,17 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
ipcidr: parseList(values.fallbackIpcidr),
|
||||
domain: parseList(values.fallbackDomain),
|
||||
},
|
||||
};
|
||||
|
||||
// 只在有值时添加其他可选字段
|
||||
if (values.fallback) {
|
||||
dnsConfig["fallback"] = parseList(values.fallback);
|
||||
}
|
||||
fallback: parseList(values.fallback),
|
||||
"proxy-server-nameserver": parseList(values.proxyServerNameserver),
|
||||
"direct-nameserver": parseList(values.directNameserver),
|
||||
};
|
||||
|
||||
const policy = parseNameserverPolicy(values.nameserverPolicy);
|
||||
if (Object.keys(policy).length > 0) {
|
||||
dnsConfig["nameserver-policy"] = policy;
|
||||
}
|
||||
|
||||
if (values.proxyServerNameserver) {
|
||||
dnsConfig["proxy-server-nameserver"] = parseList(
|
||||
values.proxyServerNameserver,
|
||||
);
|
||||
}
|
||||
|
||||
if (values.directNameserver) {
|
||||
dnsConfig["direct-nameserver"] = parseList(values.directNameserver);
|
||||
}
|
||||
|
||||
return dnsConfig;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
import { BaseDialog } from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { Delete as DeleteIcon } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
styled,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { useLockFn, useRequest } from "ahooks";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// 自定义开关按钮样式
|
||||
const ToggleButton = styled("label")`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #e0e0e0;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196f3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
`;
|
||||
|
||||
// 定义开发环境的URL列表
|
||||
// 这些URL在开发模式下会被自动包含在允许的来源中
|
||||
// 在生产环境中,这些URL会被过滤掉
|
||||
// 这样可以确保在生产环境中不会意外暴露开发环境的URL
|
||||
const DEV_URLS = [
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
"http://localhost:3000",
|
||||
];
|
||||
|
||||
// 判断是否处于开发模式
|
||||
const isDevMode = import.meta.env.MODE === "development";
|
||||
|
||||
// 过滤开发环境URL
|
||||
const filterDevOrigins = (origins: string[]) => {
|
||||
if (isDevMode) {
|
||||
return origins;
|
||||
}
|
||||
return origins.filter((origin: string) => !DEV_URLS.includes(origin.trim()));
|
||||
};
|
||||
|
||||
// 获取完整的源列表,包括开发URL
|
||||
const getFullOrigins = (origins: string[]) => {
|
||||
if (!isDevMode) {
|
||||
return origins;
|
||||
}
|
||||
|
||||
// 合并现有源和开发URL,并去重
|
||||
const allOrigins = [...origins, ...DEV_URLS];
|
||||
const uniqueOrigins = [...new Set(allOrigins)];
|
||||
return uniqueOrigins;
|
||||
};
|
||||
|
||||
// 统一使用的按钮样式
|
||||
const buttonStyle = {
|
||||
borderRadius: "8px",
|
||||
textTransform: "none",
|
||||
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
||||
transition: "all 0.3s ease",
|
||||
"&:hover": {
|
||||
boxShadow: "0 4px 8px rgba(0,0,0,0.15)",
|
||||
transform: "translateY(-1px)",
|
||||
},
|
||||
"&:active": {
|
||||
transform: "translateY(0)",
|
||||
},
|
||||
};
|
||||
|
||||
// 保存按钮样式
|
||||
const saveButtonStyle = {
|
||||
...buttonStyle,
|
||||
backgroundColor: "#165DFF",
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: "#0E42D2",
|
||||
},
|
||||
};
|
||||
|
||||
// 添加按钮样式
|
||||
const addButtonStyle = {
|
||||
...buttonStyle,
|
||||
backgroundColor: "#4CAF50",
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: "#388E3C",
|
||||
},
|
||||
};
|
||||
|
||||
// 删除按钮样式
|
||||
const deleteButtonStyle = {
|
||||
...buttonStyle,
|
||||
backgroundColor: "#FF5252",
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: "#D32F2F",
|
||||
},
|
||||
};
|
||||
|
||||
interface ClashHeaderConfigingRef {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { clash, mutateClash, patchClash } = useClash();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// CORS配置状态管理
|
||||
const [corsConfig, setCorsConfig] = useState<{
|
||||
allowPrivateNetwork: boolean;
|
||||
allowOrigins: string[];
|
||||
}>(() => {
|
||||
const cors = clash?.["external-controller-cors"];
|
||||
const origins = cors?.["allow-origins"] ?? ["*"];
|
||||
return {
|
||||
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
|
||||
allowOrigins: filterDevOrigins(origins),
|
||||
};
|
||||
});
|
||||
|
||||
// 处理CORS配置变更
|
||||
const handleCorsConfigChange = (
|
||||
key: "allowPrivateNetwork" | "allowOrigins",
|
||||
value: boolean | string[],
|
||||
) => {
|
||||
setCorsConfig((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// 添加新的允许来源
|
||||
const handleAddOrigin = () => {
|
||||
handleCorsConfigChange("allowOrigins", [...corsConfig.allowOrigins, ""]);
|
||||
};
|
||||
|
||||
// 更新允许来源列表中的某一项
|
||||
const handleUpdateOrigin = (index: number, value: string) => {
|
||||
const newOrigins = [...corsConfig.allowOrigins];
|
||||
newOrigins[index] = value;
|
||||
handleCorsConfigChange("allowOrigins", newOrigins);
|
||||
};
|
||||
|
||||
// 删除允许来源列表中的某一项
|
||||
const handleDeleteOrigin = (index: number) => {
|
||||
const newOrigins = [...corsConfig.allowOrigins];
|
||||
newOrigins.splice(index, 1);
|
||||
handleCorsConfigChange("allowOrigins", newOrigins);
|
||||
};
|
||||
|
||||
// 保存配置请求
|
||||
const { loading, run: saveConfig } = useRequest(
|
||||
async () => {
|
||||
// 保存时使用完整的源列表(包括开发URL)
|
||||
const fullOrigins = getFullOrigins(corsConfig.allowOrigins);
|
||||
|
||||
await patchClash({
|
||||
"external-controller-cors": {
|
||||
"allow-private-network": corsConfig.allowPrivateNetwork,
|
||||
"allow-origins": fullOrigins.filter(
|
||||
(origin: string) => origin.trim() !== "",
|
||||
),
|
||||
},
|
||||
});
|
||||
await mutateClash();
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
showNotice("success", t("Configuration saved successfully"));
|
||||
},
|
||||
onError: () => {
|
||||
showNotice("error", t("Failed to save configuration"));
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
const cors = clash?.["external-controller-cors"];
|
||||
const origins = cors?.["allow-origins"] ?? ["*"];
|
||||
setCorsConfig({
|
||||
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
|
||||
allowOrigins: filterDevOrigins(origins),
|
||||
});
|
||||
setOpen(true);
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
await saveConfig();
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("External Cors Configuration")}
|
||||
contentSx={{ width: 500 }}
|
||||
okBtn={loading ? t("Saving...") : t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<List sx={{ width: "90%", padding: 2 }}>
|
||||
<ListItem sx={{ padding: "8px 0" }}>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
<span style={{ fontWeight: "normal" }}>
|
||||
{t("Allow private network access")}
|
||||
</span>
|
||||
<ToggleButton>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={corsConfig.allowPrivateNetwork}
|
||||
onChange={(e) =>
|
||||
handleCorsConfigChange(
|
||||
"allowPrivateNetwork",
|
||||
e.target.checked,
|
||||
)
|
||||
}
|
||||
id="private-network-toggle"
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
</ListItem>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<ListItem sx={{ padding: "8px 0" }}>
|
||||
<div style={{ width: "100%" }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
|
||||
{t("Allowed Origins")}
|
||||
</div>
|
||||
{corsConfig.allowOrigins.map((origin, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
sx={{ fontSize: 14, marginRight: 2 }}
|
||||
value={origin}
|
||||
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
|
||||
placeholder={t("Please enter a valid url")}
|
||||
inputProps={{ style: { fontSize: 14 } }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => handleDeleteOrigin(index)}
|
||||
disabled={corsConfig.allowOrigins.length <= 1}
|
||||
sx={deleteButtonStyle}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleAddOrigin}
|
||||
sx={addButtonStyle}
|
||||
>
|
||||
{t("Add")}
|
||||
</Button>
|
||||
|
||||
{isDevMode && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: 8,
|
||||
backgroundColor: "#f5f5f5",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ color: "#666", fontSize: 12, fontStyle: "italic" }}
|
||||
>
|
||||
{t(
|
||||
"Development mode: Automatically includes Tauri and localhost origins",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,12 +1,62 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { styled, Typography, Switch } from "@mui/material";
|
||||
import { styled, Typography } from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
import { HotkeyInput } from "./hotkey-input";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
// 修复后的自定义开关组件
|
||||
const ToggleButton = styled("label")`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #e0e0e0;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196f3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
`;
|
||||
|
||||
const ItemWrapper = styled("div")`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -97,11 +147,15 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
>
|
||||
<ItemWrapper style={{ marginBottom: 16 }}>
|
||||
<Typography>{t("Enable Global Hotkey")}</Typography>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={enableGlobalHotkey}
|
||||
onChange={(e) => setEnableHotkey(e.target.checked)}
|
||||
/>
|
||||
<ToggleButton>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableGlobalHotkey}
|
||||
onChange={(e) => setEnableHotkey(e.target.checked)}
|
||||
id="global-hotkey-toggle"
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
</ToggleButton>
|
||||
</ItemWrapper>
|
||||
|
||||
{HOTKEY_FUNC.map((func) => (
|
||||
|
||||
@@ -209,7 +209,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
)}
|
||||
{OS === "macos" && (
|
||||
{/* {OS === "macos" && (
|
||||
<Item>
|
||||
<ListItemText primary={t("Enable Tray Speed")} />
|
||||
<GuardState
|
||||
@@ -223,7 +223,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</Item>
|
||||
)}
|
||||
)} */}
|
||||
{OS === "macos" && (
|
||||
<Item>
|
||||
<ListItemText primary={t("Enable Tray Icon")} />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user