Compare commits

..

26 Commits

47 changed files with 1607 additions and 625 deletions

View File

@@ -286,8 +286,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -308,7 +307,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
# - name: Release ${{ env.TAG_CHANNEL }} Version
# run: pnpm release-version ${{ env.TAG_NAME }}
@@ -363,7 +362,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
save-if: false
- name: Install Node
uses: actions/setup-node@v4
@@ -378,7 +377,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
# - name: Release ${{ env.TAG_CHANNEL }} Version
# run: pnpm release-version ${{ env.TAG_NAME }}
@@ -491,8 +490,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install Node
uses: actions/setup-node@v4
@@ -507,7 +505,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
# - name: Release ${{ env.TAG_CHANNEL }} Version
# run: pnpm release-version ${{ env.TAG_NAME }}

View File

@@ -183,7 +183,7 @@ jobs:
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -191,20 +191,21 @@ jobs:
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
@@ -260,21 +261,23 @@ jobs:
with:
workspaces: src-tauri
cache-all-crates: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
save-if: ${{ github.ref == 'refs/heads/dev' }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
@@ -388,22 +391,23 @@ jobs:
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
cache: "pnpm"
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}

View File

@@ -27,12 +27,11 @@ jobs:
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
# - name: Rust Cache
# uses: Swatinem/rust-cache@v2
# with:
# workspaces: src-tauri
# cache-all-crates: true
# cache-on-failure: true
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
save-if: false
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -53,7 +52,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Build Web Assets
run: pnpm run web:build

View File

@@ -50,14 +50,13 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Cargo Check (deny warnings)
working-directory: src-tauri

View File

@@ -42,8 +42,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install Node
uses: actions/setup-node@v4
@@ -58,7 +57,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0

View File

@@ -30,7 +30,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- run: pnpm i --frozen-lockfile
- run: pnpm format:check

View File

@@ -73,8 +73,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -95,7 +94,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
@@ -144,7 +143,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
save-if: false
- name: Install Node
uses: actions/setup-node@v4
@@ -159,7 +158,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: "Setup for linux"
run: |-
@@ -263,8 +262,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install Node
uses: actions/setup-node@v4
@@ -279,7 +277,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm prepare ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Download WebView2 Runtime
run: |

View File

@@ -8,4 +8,20 @@ if git diff --cached --name-only | grep -q '^src-tauri/'; then
fi
fi
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
fi
else
echo "[pre-push] Not pushing to target repo. Skipping format check."
fi
exit 0

View File

@@ -52,9 +52,9 @@ You have two options for downloading the clash binary:
- Automatically download it via the provided script:
```shell
pnpm run prepare
pnpm run prebuild
# Use '--force' to force update to the latest version
# pnpm run prepare --force
# pnpm run prebuild --force
```
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).

View File

@@ -1,3 +1,25 @@
## v2.3.1
### 🐞 修复问题
- 增加配置文件校验,修复从古老版本升级上来的"No such file or directory (os error 2)"错误
- 修复扩展脚本转义错误
- 修复 macOS Intel X86 架构构建错误导致无法运行
- 修复 Linux 下界面边框白边问题
- 修复 托盘 无响应问题
- 修复 托盘 无法从轻量模式退出并恢复窗口
- 修复 快速切换订阅可能导致的卡死问题
### ✨ 新增功能
- 新增 window-state 窗口状态管理和恢复
### 🚀 优化改进
- 优化 托盘 统一响应
- 优化 静默启动+自启动轻量模式 运行方式
- 升级依赖
## v2.3.0
**发行代号:御**
@@ -5,20 +27,17 @@
尽管 `external-controller` 密钥现已自动补全默认值且不允许为空,**仍建议手动修改密钥以提高安全性**。
---
### ⚠️ 已知问题
- 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
- macOS
- MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
- 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
- 彩色 Tray 图标颜色偏淡;
- 已确认窗口状态管理器存在上游缺陷,已暂时移除窗口大小与位置记忆功能。
---
### 🐞 修复问题
- 修复首页“代理模式”快速切换导致的卡死问题
@@ -42,8 +61,6 @@
- 修复 JS 脚本转义特殊字符报错
- 修复 macOS 静默启动时异常启动 Dock 栏图标
---
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.10**
@@ -72,8 +89,6 @@
- 新增 Zashboard 一键跳转入口
- 使用系统默认窗口管理器
---
### 🚀 优化改进
- **系统相关:**
@@ -112,13 +127,9 @@
- 网络延迟测试替换为 HTTPS 协议:`https://cp.cloudflare.com/generate_204`
- 优化 IP 信息获取流程,添加去重机制与轮询检测算法
---
- 同步修复翻译错误与不一致项,优化整体语言体验
- 加强语言切换后的页面稳定性,避免加载异常
---
### 🗑️ 移除内容
- 窗口状态管理器(上游存在缺陷)

View File

@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.3.0",
"version": "2.3.1",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
@@ -11,7 +11,7 @@
"web:dev": "vite",
"web:build": "tsc --noEmit && vite build",
"web:serve": "vite preview",
"prepare": "node scripts/prepare.mjs",
"prebuild": "node scripts/prebuild.mjs",
"updater": "node scripts/updater.mjs",
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
"portable": "node scripts/portable.mjs",
@@ -34,27 +34,27 @@
"@mui/icons-material": "^7.1.1",
"@mui/lab": "7.0.0-beta.13",
"@mui/material": "^7.1.1",
"@mui/x-data-grid": "^8.5.1",
"@mui/x-data-grid": "^8.5.2",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@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.2",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "2.2.1",
"@tauri-apps/plugin-updater": "2.7.1",
"@tauri-apps/plugin-window-state": "^2.2.2",
"@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",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.8.5",
"axios": "^1.9.0",
"chart.js": "^4.4.9",
"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.2",
"glob": "^11.0.3",
"i18next": "^25.2.1",
"js-base64": "^3.7.7",
"js-yaml": "^4.1.0",
@@ -67,12 +67,12 @@
"react-chartjs-2": "^5.3.0",
"react-dom": "19.1.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.57.0",
"react-i18next": "15.5.2",
"react-hook-form": "^7.58.1",
"react-i18next": "15.5.3",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.58.0",
"react-router-dom": "7.6.2",
"react-virtuoso": "^4.12.8",
"react-virtuoso": "^4.13.0",
"sockette": "^2.0.6",
"swr": "^2.3.3",
"tar": "^7.4.3",
@@ -99,7 +99,7 @@
"prettier": "^3.5.3",
"pretty-quick": "^4.2.2",
"sass": "^1.89.2",
"terser": "^5.42.0",
"terser": "^5.43.0",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-monaco-editor": "^1.1.0",

228
pnpm-lock.yaml generated
View File

@@ -36,14 +36,14 @@ importers:
specifier: ^7.1.1
version: 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@mui/x-data-grid':
specifier: ^8.5.1
version: 8.5.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^8.5.2
version: 8.5.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tauri-apps/api':
specifier: 2.5.0
version: 2.5.0
'@tauri-apps/plugin-clipboard-manager':
specifier: ^2.2.2
version: 2.2.2
specifier: ^2.2.3
version: 2.2.3
'@tauri-apps/plugin-dialog':
specifier: ^2.2.2
version: 2.2.2
@@ -54,20 +54,20 @@ importers:
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-notification':
specifier: ^2.2.2
version: 2.2.2
specifier: ^2.2.3
version: 2.2.3
'@tauri-apps/plugin-process':
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-shell':
specifier: 2.2.1
version: 2.2.1
'@tauri-apps/plugin-updater':
specifier: 2.7.1
version: 2.7.1
'@tauri-apps/plugin-window-state':
specifier: ^2.2.2
version: 2.2.2
'@tauri-apps/plugin-shell':
specifier: 2.2.2
version: 2.2.2
'@tauri-apps/plugin-updater':
specifier: 2.8.1
version: 2.8.1
'@tauri-apps/plugin-window-state':
specifier: ^2.2.3
version: 2.2.3
'@types/d3-shape':
specifier: ^3.1.7
version: 3.1.7
@@ -78,11 +78,11 @@ importers:
specifier: ^3.8.5
version: 3.8.5(react@19.1.0)
axios:
specifier: ^1.9.0
version: 1.9.0
specifier: ^1.10.0
version: 1.10.0
chart.js:
specifier: ^4.4.9
version: 4.4.9
specifier: ^4.5.0
version: 4.5.0
cli-color:
specifier: ^2.0.4
version: 2.0.4
@@ -96,8 +96,8 @@ importers:
specifier: ^0.2.49
version: 0.2.49(react@19.1.0)
glob:
specifier: ^11.0.2
version: 11.0.2
specifier: ^11.0.3
version: 11.0.3
i18next:
specifier: ^25.2.1
version: 25.2.1(typescript@5.8.3)
@@ -127,7 +127,7 @@ importers:
version: 19.1.0
react-chartjs-2:
specifier: ^5.3.0
version: 5.3.0(chart.js@4.4.9)(react@19.1.0)
version: 5.3.0(chart.js@4.5.0)(react@19.1.0)
react-dom:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
@@ -135,11 +135,11 @@ importers:
specifier: 6.0.0
version: 6.0.0(react@19.1.0)
react-hook-form:
specifier: ^7.57.0
version: 7.57.0(react@19.1.0)
specifier: ^7.58.1
version: 7.58.1(react@19.1.0)
react-i18next:
specifier: 15.5.2
version: 15.5.2(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
specifier: 15.5.3
version: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/react@19.1.8)(react@19.1.0)
@@ -150,8 +150,8 @@ importers:
specifier: 7.6.2
version: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-virtuoso:
specifier: ^4.12.8
version: 4.12.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^4.13.0
version: 4.13.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
sockette:
specifier: ^2.0.6
version: 2.0.6
@@ -191,10 +191,10 @@ importers:
version: 19.1.6(@types/react@19.1.8)
'@vitejs/plugin-legacy':
specifier: ^6.1.1
version: 6.1.1(terser@5.42.0)(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1))
version: 6.1.1(terser@5.43.0)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))
'@vitejs/plugin-react':
specifier: 4.5.2
version: 4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1))
version: 4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))
adm-zip:
specifier: ^0.5.16
version: 0.5.16
@@ -226,20 +226,20 @@ importers:
specifier: ^1.89.2
version: 1.89.2
terser:
specifier: ^5.42.0
version: 5.42.0
specifier: ^5.43.0
version: 5.43.0
typescript:
specifier: ^5.8.3
version: 5.8.3
vite:
specifier: ^6.3.5
version: 6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1)
version: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)
vite-plugin-monaco-editor:
specifier: ^1.1.0
version: 1.1.0(monaco-editor@0.52.2)
vite-plugin-svgr:
specifier: ^4.3.0
version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1))
version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))
packages:
@@ -980,6 +980,14 @@ packages:
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
'@isaacs/brace-expansion@5.0.0':
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
engines: {node: 20 || >=22}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -1127,8 +1135,8 @@ packages:
'@types/react':
optional: true
'@mui/x-data-grid@8.5.1':
resolution: {integrity: sha512-Ukodx8cOc/GR4+2zr4DRNBJJOlyeJNaxK4hggWv7VchDL7Jf70dLZO7oLXPhEFG05Yna81RatL/UFsRYIdWI1Q==}
'@mui/x-data-grid@8.5.2':
resolution: {integrity: sha512-4KzawLZqRKp3KcGKsTDVz7zkEjACllQD5Zb8ds1QKlA6C3/oIoSU7PsemFLj+RL3rT5aORsLMBl97/egQ5tUhA==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.9.0
@@ -1143,8 +1151,8 @@ packages:
'@emotion/styled':
optional: true
'@mui/x-internals@8.5.1':
resolution: {integrity: sha512-7rAWK7SB6FxEIXKgsHsJjIzeeKOLxFJ16gePgZVWlvyew+xDb4P0fgjwW3ThcJjgvkUm0UhGGfLh/JP8l514IA==}
'@mui/x-internals@8.5.2':
resolution: {integrity: sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0
@@ -1545,8 +1553,8 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-clipboard-manager@2.2.2':
resolution: {integrity: sha512-bZvDLMqfcNmsw7Ag8I49jlaCjdpDvvlJHnpp6P+Gg/3xtpSERdwlDxm7cKGbs2mj46dsw4AuG3RoAgcpwgioUA==}
'@tauri-apps/plugin-clipboard-manager@2.2.3':
resolution: {integrity: sha512-myZTLyBpJ9gnDsywtdgRpAYLxEtSVaJa11s1xoiB6w8cjFtG2/znas4Cz3vqYigJkY0A57tyZUE6tjxavIAzgw==}
'@tauri-apps/plugin-dialog@2.2.2':
resolution: {integrity: sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A==}
@@ -1557,20 +1565,20 @@ packages:
'@tauri-apps/plugin-global-shortcut@2.2.1':
resolution: {integrity: sha512-b64/TI1t5LIi2JY4OWlYjZpPRq60T5GVVL/no27sUuxaNUZY8dVtwsMtDUgxUpln2yR+P2PJsYlqY5V8sLSxEw==}
'@tauri-apps/plugin-notification@2.2.2':
resolution: {integrity: sha512-d71rJdtkFUTcG4dqydnv6d7ZwlNZVcdjrVOPwc9GsF6y9DgVN1WCZ9T/vbfD2qrJslf7ai+rnNJc62TLLC2IdA==}
'@tauri-apps/plugin-notification@2.2.3':
resolution: {integrity: sha512-IlMdSVFsrKg0eIHBloFFosnWbbz6JdwBywfZrYZnE1+acgXvNS3T1YB5w9R6UXw+KKQ94ODBu7JF7a1YUiAK6A==}
'@tauri-apps/plugin-process@2.2.1':
resolution: {integrity: sha512-cF/k8J+YjjuowhNG1AboHNTlrGiOwgX5j6NzsX6WFf9FMzyZUchkCgZMxCdSE5NIgFX0vvOgLQhODFJgbMenLg==}
'@tauri-apps/plugin-process@2.2.2':
resolution: {integrity: sha512-1HuR+uGcokQxlgbS0DheFyMpJSuVGuy3Yh3Eq5o3Jm/sEW+44JaVVgYWM0efpDPA8oT5wpabTFEOZHvKfp8dCg==}
'@tauri-apps/plugin-shell@2.2.1':
resolution: {integrity: sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA==}
'@tauri-apps/plugin-shell@2.2.2':
resolution: {integrity: sha512-fg9XKWfzRQsN8p+Zrk82WeHvXFvGVnG0/mTlujQdLWNnO5cM6WD9qCrHbFytScVS+WhmRAkuypQPcxeKKl3VBg==}
'@tauri-apps/plugin-updater@2.7.1':
resolution: {integrity: sha512-1OPqEY/z7NDVSeTEMIhD2ss/vXWdpfZ5Th2Mk0KtPR/RA6FKuOTDGZQhxoyYBk0pcZJ+nNZUbl/IujDCLBApjA==}
'@tauri-apps/plugin-updater@2.8.1':
resolution: {integrity: sha512-VVQ3wCfM+zok/e0QNBT0oBaFu3gBbzzMsRHzS2yxl7VOCh9dWuZo4yyl29OfaExxSvcixWJ9BZ6pWmKdUt8fCg==}
'@tauri-apps/plugin-window-state@2.2.2':
resolution: {integrity: sha512-7pFwmMtGhhhE/WgmM7PUrj0BSSWVAQMfDdYbRalphIqqF1tWBvxtlxclx8bTutpXHLJTQoCpIeWtBEIXsoAlGw==}
'@tauri-apps/plugin-window-state@2.2.3':
resolution: {integrity: sha512-Iqqzugs6lxpa9JPOe4O33lkCUyMGvh9dqnXof1tK4dP2wU7jKa7W3MLwVyo6c3oVl3dUCm73wkB3RJ0exR0SPg==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -1705,8 +1713,8 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.9.0:
resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
axios@1.10.0:
resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==}
babel-plugin-macros@3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
@@ -1730,15 +1738,9 @@ packages:
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
@@ -1788,8 +1790,8 @@ packages:
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
chart.js@4.4.9:
resolution: {integrity: sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==}
chart.js@4.5.0:
resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==}
engines: {pnpm: '>=8'}
chokidar@4.0.3:
@@ -2086,8 +2088,8 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
glob@11.0.2:
resolution: {integrity: sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==}
glob@11.0.3:
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
engines: {node: 20 || >=22}
hasBin: true
@@ -2205,8 +2207,8 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jackspeak@4.1.0:
resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==}
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
js-base64@3.7.7:
@@ -2394,8 +2396,8 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@10.0.1:
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
minimatch@10.0.3:
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
engines: {node: 20 || >=22}
minipass@7.1.2:
@@ -2570,14 +2572,14 @@ packages:
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-hook-form@7.57.0:
resolution: {integrity: sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==}
react-hook-form@7.58.1:
resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@15.5.2:
resolution: {integrity: sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==}
react-i18next@15.5.3:
resolution: {integrity: sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
@@ -2638,8 +2640,8 @@ packages:
react: '>=16.6.0'
react-dom: '>=16.6.0'
react-virtuoso@4.12.8:
resolution: {integrity: sha512-NMMKfDBr/+xZZqCQF3tN1SZsh6FwOJkYgThlfnsPLkaEhdyQo0EuWUzu3ix6qjnI7rYwJhMwRGoJBi+aiDfGsA==}
react-virtuoso@4.13.0:
resolution: {integrity: sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==}
peerDependencies:
react: '>=16 || >=17 || >= 18 || >= 19'
react-dom: '>=16 || >=17 || >= 18 || >=19'
@@ -2813,8 +2815,8 @@ packages:
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
engines: {node: '>=18'}
terser@5.42.0:
resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==}
terser@5.43.0:
resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==}
engines: {node: '>=10'}
hasBin: true
@@ -3896,6 +3898,12 @@ snapshots:
'@fastify/busboy@2.1.1': {}
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
dependencies:
'@isaacs/balanced-match': 4.0.1
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -4038,18 +4046,17 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.8
'@mui/x-data-grid@8.5.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@mui/x-data-grid@8.5.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@babel/runtime': 7.27.6
'@mui/material': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)
'@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0)
'@mui/x-internals': 8.5.1(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)
'@mui/x-internals': 8.5.2(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)
clsx: 2.1.1
prop-types: 15.8.1
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
reselect: 5.1.1
use-sync-external-store: 1.5.0(react@19.1.0)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0)
@@ -4057,12 +4064,13 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
'@mui/x-internals@8.5.1(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)':
'@mui/x-internals@8.5.2(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)':
dependencies:
'@babel/runtime': 7.27.6
'@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)
'@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
reselect: 5.1.1
transitivePeerDependencies:
- '@types/react'
@@ -4382,7 +4390,7 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.5.0
'@tauri-apps/cli-win32-x64-msvc': 2.5.0
'@tauri-apps/plugin-clipboard-manager@2.2.2':
'@tauri-apps/plugin-clipboard-manager@2.2.3':
dependencies:
'@tauri-apps/api': 2.5.0
@@ -4398,23 +4406,23 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-notification@2.2.2':
'@tauri-apps/plugin-notification@2.2.3':
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-process@2.2.1':
'@tauri-apps/plugin-process@2.2.2':
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-shell@2.2.1':
'@tauri-apps/plugin-shell@2.2.2':
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-updater@2.7.1':
'@tauri-apps/plugin-updater@2.8.1':
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-window-state@2.2.2':
'@tauri-apps/plugin-window-state@2.2.3':
dependencies:
'@tauri-apps/api': 2.5.0
@@ -4499,7 +4507,7 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-legacy@6.1.1(terser@5.42.0)(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1))':
'@vitejs/plugin-legacy@6.1.1(terser@5.43.0)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))':
dependencies:
'@babel/core': 7.27.4
'@babel/preset-env': 7.27.2(@babel/core@7.27.4)
@@ -4509,12 +4517,12 @@ snapshots:
magic-string: 0.30.17
regenerator-runtime: 0.14.1
systemjs: 6.15.1
terser: 5.42.0
vite: 6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1)
terser: 5.43.0
vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-react@4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1))':
'@vitejs/plugin-react@4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))':
dependencies:
'@babel/core': 7.27.4
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4)
@@ -4522,7 +4530,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.11
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1)
vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)
transitivePeerDependencies:
- supports-color
@@ -4559,7 +4567,7 @@ snapshots:
asynckit@0.4.0: {}
axios@1.9.0:
axios@1.10.0:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.2
@@ -4599,14 +4607,8 @@ snapshots:
bail@2.0.2: {}
balanced-match@1.0.2: {}
before-after-hook@2.2.3: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
braces@3.0.3:
dependencies:
fill-range: 7.1.1
@@ -4647,7 +4649,7 @@ snapshots:
character-reference-invalid@2.0.1: {}
chart.js@4.4.9:
chart.js@4.5.0:
dependencies:
'@kurkle/color': 0.3.4
@@ -4954,11 +4956,11 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
glob@11.0.2:
glob@11.0.3:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.1.0
minimatch: 10.0.1
jackspeak: 4.1.1
minimatch: 10.0.3
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.0
@@ -5075,7 +5077,7 @@ snapshots:
isexe@2.0.0: {}
jackspeak@4.1.0:
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
@@ -5382,9 +5384,9 @@ snapshots:
dependencies:
mime-db: 1.52.0
minimatch@10.0.1:
minimatch@10.0.3:
dependencies:
brace-expansion: 2.0.1
'@isaacs/brace-expansion': 5.0.0
minipass@7.1.2: {}
@@ -5539,9 +5541,9 @@ snapshots:
proxy-from-env@1.1.0: {}
react-chartjs-2@5.3.0(chart.js@4.4.9)(react@19.1.0):
react-chartjs-2@5.3.0(chart.js@4.5.0)(react@19.1.0):
dependencies:
chart.js: 4.4.9
chart.js: 4.5.0
react: 19.1.0
react-dom@19.1.0(react@19.1.0):
@@ -5556,11 +5558,11 @@ snapshots:
react-fast-compare@3.2.2: {}
react-hook-form@7.57.0(react@19.1.0):
react-hook-form@7.58.1(react@19.1.0):
dependencies:
react: 19.1.0
react-i18next@15.5.2(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3):
react-i18next@15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3):
dependencies:
'@babel/runtime': 7.27.6
html-parse-stringify: 3.0.1
@@ -5623,7 +5625,7 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-virtuoso@4.12.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
react-virtuoso@4.13.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
@@ -5816,7 +5818,7 @@ snapshots:
mkdirp: 3.0.1
yallist: 5.0.0
terser@5.42.0:
terser@5.43.0:
dependencies:
'@jridgewell/source-map': 0.3.6
acorn: 8.14.1
@@ -5928,18 +5930,18 @@ snapshots:
dependencies:
monaco-editor: 0.52.2
vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1)):
vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)):
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.40.2)
'@svgr/core': 8.1.0(typescript@5.8.3)
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))
vite: 6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1)
vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)
transitivePeerDependencies:
- rollup
- supports-color
- typescript
vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1):
vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1):
dependencies:
esbuild: 0.25.4
fdir: 6.4.4(picomatch@4.0.2)
@@ -5950,7 +5952,7 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
sass: 1.89.2
terser: 5.42.0
terser: 5.43.0
yaml: 2.7.1
void-elements@3.1.0: {}

143
src-tauri/Cargo.lock generated
View File

@@ -1042,7 +1042,7 @@ dependencies = [
[[package]]
name = "clash-verge"
version = "2.3.0"
version = "2.3.1"
dependencies = [
"ab_glyph",
"aes-gcm",
@@ -1100,13 +1100,13 @@ dependencies = [
"tauri-plugin-window-state",
"tempfile",
"tokio",
"tokio-tungstenite 0.26.2",
"tungstenite 0.26.2",
"tokio-tungstenite 0.27.0",
"tungstenite 0.27.0",
"users",
"warp",
"winapi",
"winreg 0.55.0",
"zip 4.0.0",
"zip",
]
[[package]]
@@ -3587,9 +3587,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.172"
version = "0.2.173"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
[[package]]
name = "libfuzzer-sys"
@@ -6977,8 +6977,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-autostart"
version = "2.3.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#9bc4b2230ebb32bd30a4c0c2a21077829a729193"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9641831518c56775a364a8750e0eed8852adee87e0f11006d043b9ebba0bf5"
dependencies = [
"auto-launch",
"serde",
@@ -6990,9 +6991,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.2.2"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab4cb42fdf745229b768802e9180920a4be63122cf87ed1c879103f7609d98e"
checksum = "11fa4f17a6d380490597f7632aca40b65d379cb374cb92bd9d80f333309b7fd7"
dependencies = [
"arboard",
"log",
@@ -7107,9 +7108,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-process"
version = "2.2.1"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57da5888533e802b6206b9685091f8714aa1f5266dc80051a82388449558b773"
checksum = "4d870adae9408be585abd56eade2b5def2660339512b7c8de5ddf21238b67a34"
dependencies = [
"tauri",
"tauri-plugin",
@@ -7117,9 +7118,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-shell"
version = "2.2.1"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d5eb3368b959937ad2aeaf6ef9a8f5d11e01ffe03629d3530707bbcb27ff5d"
checksum = "d34e525a448b80ad5d906fcbd93838ac3ba37985b29ac699a045b5da9b0a1a22"
dependencies = [
"encoding_rs",
"log",
@@ -7138,9 +7139,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.7.1"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f05c38afd77a4b8fd98e8fb6f1cdbb5fbb8a46ba181eb2758b05321e3c6209"
checksum = "b068673e9037376ca9906f99b00ae5f9e6eb62f456f900b4435c38d57cfa73e4"
dependencies = [
"base64 0.22.1",
"dirs 6.0.0",
@@ -7164,15 +7165,15 @@ dependencies = [
"time",
"tokio",
"url",
"windows-sys 0.59.0",
"zip 2.6.1",
"windows-sys 0.60.2",
"zip",
]
[[package]]
name = "tauri-plugin-window-state"
version = "2.2.2"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27a3fe49de72adbe0d84aee33c89a0b059722cd0b42aaeab29eaaee7f7535cd"
checksum = "136e5ce5e61edc8eeeaca70080811bbdcdd890cac9c4070cb4db9cc3de1da449"
dependencies = [
"bitflags 2.9.0",
"log",
@@ -7595,14 +7596,14 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite 0.26.2",
"tungstenite 0.27.0",
]
[[package]]
@@ -7953,9 +7954,9 @@ dependencies = [
[[package]]
name = "tungstenite"
version = "0.26.2"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
dependencies = [
"bytes",
"data-encoding",
@@ -8870,6 +8871,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.2",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -8909,13 +8919,29 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-version"
version = "0.1.4"
@@ -8943,6 +8969,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -8961,6 +8993,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -8979,12 +9017,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -9003,6 +9053,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -9021,6 +9077,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -9039,6 +9101,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -9057,6 +9125,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.5.40"
@@ -9452,22 +9526,9 @@ dependencies = [
[[package]]
name = "zip"
version = "2.6.1"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"indexmap 2.8.0",
"memchr",
]
[[package]]
name = "zip"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd"
checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0"
dependencies = [
"aes",
"arbitrary",

View File

@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.3.0"
version = "2.3.1"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -55,27 +55,27 @@ tauri = { version = "2.5.1", features = [
"image-png",
] }
network-interface = { version = "2.0.1", features = ["serde"] }
tauri-plugin-shell = "2.2.1"
tauri-plugin-shell = "2.2.2"
tauri-plugin-dialog = "2.2.2"
tauri-plugin-fs = "2.3.0"
tauri-plugin-process = "2.2.1"
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-process = "2.2.2"
tauri-plugin-clipboard-manager = "2.2.3"
tauri-plugin-deep-link = "2.3.0"
tauri-plugin-devtools = "2.0.0"
tauri-plugin-window-state = "2.2.2"
zip = "4.0.0"
tauri-plugin-window-state = "2.2.3"
zip = "4.1.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.26.2"
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.26.2"
libc = "0.2.172"
tungstenite = "0.27.0"
libc = "0.2.173"
gethostname = "1.0.2"
hmac = "0.12.1"
sha2 = "0.10.9"
@@ -99,9 +99,9 @@ winapi = { version = "0.3.9", features = [
users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-autostart = "2.4.0"
tauri-plugin-global-shortcut = "2.2.1"
tauri-plugin-updater = "2.7.1"
tauri-plugin-updater = "2.8.1"
[features]
default = ["custom-protocol"]

View File

@@ -916,9 +916,9 @@ FunctionEnd
!macroend
Section Uninstall
;删除 .window-state.json 文件
;删除 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"
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
@@ -1015,9 +1015,9 @@ Section Uninstall
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
${EndIf}
;删除 .window-state.json 文件
;删除 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"
${GetOptions} $CMDLINE "/P" $R0
IfErrors +2 0

View File

@@ -12,45 +12,89 @@ use tokio::sync::Mutex;
// 添加全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
/// 获取配置文件列表
/// 获取配置文件避免锁竞争
#[tauri::command]
pub async fn get_profiles() -> CmdResult<IProfiles> {
let profiles_result = tokio::time::timeout(
Duration::from_secs(3), // 3秒超时
tokio::task::spawn_blocking(move || Config::profiles().data().clone()),
// 策略1: 尝试快速获取latest数据
let latest_result = tokio::time::timeout(
Duration::from_millis(500),
tokio::task::spawn_blocking(move || {
let profiles = Config::profiles();
let latest = profiles.latest();
IProfiles {
current: latest.current.clone(),
items: latest.items.clone(),
}
}),
)
.await;
match profiles_result {
Ok(Ok(profiles)) => Ok(*profiles),
match latest_result {
Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => {
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err);
Ok(IProfiles {
current: None,
items: Some(vec![]),
})
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
}
Err(_) => {
// 超时情况
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
}
}
// 策略2: 如果快速获取失败尝试获取data()
let data_result = tokio::time::timeout(
Duration::from_secs(2),
tokio::task::spawn_blocking(move || {
let profiles = Config::profiles();
let data = profiles.data();
IProfiles {
current: data.current.clone(),
items: data.items.clone(),
}
}),
)
.await;
match data_result {
Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => {
logging!(
error,
Type::Cmd,
true,
"获取配置列表超时(3秒),可能存在锁竞争"
"获取draft配置任务失败: {}",
join_err
);
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await {
Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用latest()成功获取配置");
Ok(*profiles)
}
Err(_) => {
logging!(error, Type::Cmd, true, "fallback获取配置也失败返回空配置");
Ok(IProfiles {
current: None,
items: Some(vec![]),
})
}
}
}
Err(_) => {
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
}
}
// 策略3: fallback尝试重新创建配置
logging!(
warn,
Type::Cmd,
true,
"所有获取配置策略都失败尝试fallback"
);
match tokio::task::spawn_blocking(IProfiles::new).await {
Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用fallback配置成功");
Ok(profiles)
}
Err(err) => {
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
// 返回空配置避免崩溃
Ok(IProfiles {
current: None,
items: Some(vec![]),
})
}
}
}
@@ -246,6 +290,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
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);
}
});
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Tray::global().update_tooltip() {
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);

View File

@@ -43,6 +43,28 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
Ok(*proxies)
}
/// 强制刷新代理缓存用于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)
}
#[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let app_handle = handle::Handle::global().app_handle().unwrap();

View File

@@ -1,5 +1,11 @@
use super::CmdResult;
use crate::{config::*, core::*, utils::dirs, wrap_err};
use crate::{
config::*,
core::*,
logging,
utils::{dirs, logging::Type},
wrap_err,
};
use std::fs;
/// 保存profiles的配置
@@ -26,29 +32,54 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
let file_path_str = file_path.to_string_lossy().to_string();
println!(
logging!(
info,
Type::Config,
true,
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
file_path_str, is_merge_file
file_path_str,
is_merge_file
);
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
if is_merge_file {
println!("[cmd配置save] 检测到merge文件只进行语法验证");
logging!(
info,
Type::Config,
true,
"[cmd配置save] 检测到merge文件只进行语法验证"
);
match CoreManager::global()
.validate_config_file(&file_path_str, Some(true))
.await
{
Ok((true, _)) => {
println!("[cmd配置save] merge文件语法验证通过");
logging!(
info,
Type::Config,
true,
"[cmd配置save] merge文件语法验证通过"
);
// 成功后尝试更新整体配置
if let Err(e) = CoreManager::global().update_config().await {
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
}
return Ok(());
}
Ok((false, error_msg)) => {
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
logging!(
warn,
Type::Config,
true,
"[cmd配置save] merge文件语法验证失败: {}",
error_msg
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
// 发送合并文件专用错误通知
@@ -57,7 +88,13 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
return Ok(());
}
Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e);
logging!(
error,
Type::Config,
true,
"[cmd配置save] 验证过程发生错误: {}",
e
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
return Err(e.to_string());
@@ -71,11 +108,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
.await
{
Ok((true, _)) => {
println!("[cmd配置save] 验证成功");
logging!(info, Type::Config, true, "[cmd配置save] 验证成功");
Ok(())
}
Ok((false, error_msg)) => {
println!("[cmd配置save] 验证失败: {}", error_msg);
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 验证失败: {}",
error_msg
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
@@ -90,24 +133,30 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|| (!file_path_str.ends_with(".js") && !is_script_error)
{
// 普通YAML错误使用YAML通知处理
println!("[cmd配置save] YAML配置文件验证失败发送通知");
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败发送通知");
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error {
// 脚本错误使用专门的通知处理
println!("[cmd配置save] 脚本文件验证失败,发送通知");
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
handle::Handle::notice_message("config_validate::error", &error_msg);
}
Ok(())
}
Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e);
logging!(
error,
Type::Config,
true,
"[cmd配置save] 验证过程发生错误: {}",
e
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
Err(e.to_string())

View File

@@ -1,5 +1,5 @@
use super::CmdResult;
use crate::core::*;
use crate::{core::*, logging, utils::logging::Type};
/// 发送脚本验证通知消息
#[tauri::command]
@@ -28,7 +28,14 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
"config_validate::script_error"
};
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
logging!(
warn,
Type::Config,
true,
"{} 验证失败: {}",
file_type,
error_msg
);
handle::Handle::notice_message(status, error_msg);
}
}
@@ -36,7 +43,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
/// 验证指定脚本文件
#[tauri::command]
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
log::info!(target: "app", "验证脚本文件: {}", file_path);
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
match CoreManager::global()
.validate_config_file(&file_path, None)
@@ -48,7 +55,13 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
}
Err(e) => {
let error_msg = e.to_string();
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
logging!(
error,
Type::Config,
true,
"验证脚本文件过程发生错误: {}",
error_msg
);
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
Ok(false)
}
@@ -60,7 +73,14 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
if !result.0 {
let error_msg = &result.1;
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
logging!(
info,
Type::Config,
true,
"[通知] 处理{}验证错误: {}",
file_type,
error_msg
);
// 检查是否为merge文件
let is_merge_file = file_type.contains("合并");
@@ -97,8 +117,22 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
}
};
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
logging!(
warn,
Type::Config,
true,
"{} 验证失败: {}",
file_type,
error_msg
);
logging!(
info,
Type::Config,
true,
"[通知] 发送通知: status={}, msg={}",
status,
error_msg
);
handle::Handle::notice_message(status, error_msg);
}
}

View File

@@ -1,6 +1,7 @@
use crate::{
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
utils::{dirs, help, i18n},
logging,
utils::{dirs, help, i18n, logging::Type},
};
use anyhow::Result;
use log::LevelFilter;
@@ -232,6 +233,93 @@ pub struct IVergeTheme {
}
impl IVerge {
/// 有效的clash核心名称
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
/// 验证并修正配置文件中的clash_core值
pub fn validate_and_fix_config() -> Result<()> {
let config_path = dirs::verge_path()?;
let mut config = match help::read_yaml::<IVerge>(&config_path) {
Ok(config) => config,
Err(_) => Self::template(),
};
let mut needs_fix = false;
if let Some(ref core) = config.clash_core {
let core_str = core.trim();
if core_str.is_empty() || !Self::VALID_CLASH_CORES.contains(&core_str) {
logging!(
warn,
Type::Config,
true,
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
core
);
config.clash_core = Some("verge-mihomo".to_string());
needs_fix = true;
}
} else {
logging!(
info,
Type::Config,
true,
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
);
config.clash_core = Some("verge-mihomo".to_string());
needs_fix = true;
}
// 修正后保存配置
if needs_fix {
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
logging!(
info,
Type::Config,
true,
"配置文件修正完成,需要重新加载配置"
);
Self::reload_config_after_fix(config)?;
} else {
logging!(
info,
Type::Config,
true,
"clash_core配置验证通过: {:?}",
config.clash_core
);
}
Ok(())
}
/// 配置修正后重新加载配置
fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
use crate::config::Config;
let config_draft = Config::verge();
*config_draft.draft() = Box::new(updated_config.clone());
config_draft.apply();
logging!(
info,
Type::Config,
true,
"内存配置已强制更新新的clash_core: {:?}",
updated_config.clash_core
);
Ok(())
}
pub fn get_valid_clash_core(&self) -> String {
self.clash_core
.clone()
.unwrap_or_else(|| "verge-mihomo".to_string())
}
fn get_system_language() -> String {
let sys_lang = sys_locale::get_locale()
.unwrap_or_else(|| String::from("en"))
@@ -503,6 +591,8 @@ pub struct IVergeResponse {
impl From<IVerge> for IVergeResponse {
fn from(verge: IVerge) -> Self {
// 先获取验证后的clash_core值避免后续借用冲突
let valid_clash_core = verge.get_valid_clash_core();
Self {
app_log_level: verge.app_log_level,
language: verge.language,
@@ -534,7 +624,7 @@ impl From<IVerge> for IVergeResponse {
proxy_host: verge.proxy_host,
theme_setting: verge.theme_setting,
web_ui_list: verge.web_ui_list,
clash_core: verge.clash_core,
clash_core: Some(valid_clash_core),
hotkeys: verge.hotkeys,
auto_close_connection: verge.auto_close_connection,
auto_check_update: verge.auto_check_update,

View File

@@ -54,7 +54,7 @@ impl fmt::Display for RunningMode {
}
}
const CLASH_CORES: [&str; 2] = ["verge-mihomo", "verge-mihomo-alpha"];
use crate::config::IVerge;
impl CoreManager {
/// 检查文件是否为脚本文件
@@ -249,8 +249,7 @@ impl CoreManager {
config_path
);
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
let clash_core = Config::verge().latest().get_valid_clash_core();
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().unwrap();
@@ -442,11 +441,7 @@ impl CoreManager {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
let clash_core = Config::verge()
.latest()
.clash_core
.clone()
.unwrap_or("verge-mihomo".to_string());
let clash_core = Config::verge().latest().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
@@ -804,7 +799,7 @@ impl CoreManager {
return Err(error_message.to_string());
}
let core: &str = &clash_core.clone().unwrap();
if !CLASH_CORES.contains(&core) {
if !IVerge::VALID_CLASH_CORES.contains(&core) {
let error_message = format!("Clash core invalid name: {}", core);
logging!(error, Type::Core, true, "{}", error_message);
return Err(error_message);

View File

@@ -151,6 +151,10 @@ impl NotificationSystem {
match window.emit(event_name_str, payload) {
Ok(_) => {
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
// 记录成功发送的事件
if log::log_enabled!(log::Level::Debug) {
log::debug!("Successfully emitted event: {}", event_name_str);
}
}
Err(e) => {
log::warn!("Failed to emit event {}: {}", event_name_str, e);
@@ -224,12 +228,27 @@ impl NotificationSystem {
}
fn shutdown(&mut self) {
log::info!("NotificationSystem shutdown initiated");
self.is_running = false;
self.sender = None;
if let Some(handle) = self.worker_handle.take() {
let _ = handle.join();
// 先关闭发送端,让接收端知道不会再有新消息
if let Some(sender) = self.sender.take() {
drop(sender);
}
// 设置超时避免无限等待
if let Some(handle) = self.worker_handle.take() {
match handle.join() {
Ok(_) => {
log::info!("NotificationSystem worker thread joined successfully");
}
Err(e) => {
log::error!("NotificationSystem worker thread join failed: {:?}", e);
}
}
}
log::info!("NotificationSystem shutdown completed");
}
}

View File

@@ -272,10 +272,11 @@ impl Hotkey {
if is_enable_global_hotkey {
f();
} else if let Some(window) = app_handle.get_webview_window("main") {
} else {
use crate::utils::window_manager::WindowManager;
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
let is_visible = window.is_visible().unwrap_or(false);
let is_focused = window.is_focused().unwrap_or(false);
let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible {
f();
@@ -330,9 +331,9 @@ impl Hotkey {
let func = iter.next();
let key = iter.next();
if func.is_some() && key.is_some() {
let func = func.unwrap().trim();
let key = key.unwrap().trim();
if let (Some(func), Some(key)) = (func, key) {
let func = func.trim();
let key = key.trim();
map.insert(key, func);
}
});

View File

@@ -742,8 +742,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().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
let clash_core = Config::verge().latest().get_valid_clash_core();
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}");

View File

@@ -6,11 +6,7 @@ use crate::{
cmd,
config::Config,
feat, logging,
module::{
lightweight::{entry_lightweight_mode, is_in_lightweight_mode},
mihomo::Rate,
},
resolve,
module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
Type,
};
@@ -205,33 +201,38 @@ impl Tray {
Ok(())
}
/// 更新托盘菜单(带频率限制)
/// 更新托盘菜单
pub fn update_menu(&self) -> Result<()> {
// 检查是否正在更新或距离上次更新太近
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(500);
// 调整最小更新间隔,确保状态及时刷新
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
// 检查是否已有更新任务在执行
// 检查是否正在更新
if self.menu_updating.load(Ordering::Acquire) {
log::debug!(target: "app", "托盘菜单正在更新中,跳过本次更新");
return Ok(());
}
// 检查更新频率
{
let last_update = self.last_menu_update.lock();
if let Some(last_time) = *last_update {
if last_time.elapsed() < MIN_UPDATE_INTERVAL {
log::debug!(target: "app", "托盘菜单更新频率过高,跳过本次更新");
return Ok(());
// 检查更新频率,但允许重要事件跳过频率限制
let should_force_update = match std::thread::current().name() {
Some("main") => true,
_ => {
let last_update = self.last_menu_update.lock();
if let Some(last_time) = *last_update {
last_time.elapsed() >= MIN_UPDATE_INTERVAL
} else {
true
}
}
};
if !should_force_update {
return Ok(());
}
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
return Ok(()); // 早期返回避免panic
return Ok(());
}
};
@@ -248,6 +249,7 @@ impl Tray {
result
}
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
let verge = Config::verge().latest().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
@@ -395,6 +397,17 @@ impl Tray {
Ok(())
}
/// 更新托盘显示状态的函数
pub fn update_tray_display(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let _tray = app_handle.tray_by_id("main").unwrap();
// 更新菜单
self.update_menu()?;
Ok(())
}
/// 更新托盘提示
pub fn update_tooltip(&self) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() {
@@ -457,6 +470,8 @@ impl Tray {
self.update_menu()?;
self.update_icon(None)?;
self.update_tooltip()?;
// 更新轻量模式显示状态
self.update_tray_display()?;
Ok(())
}
@@ -653,10 +668,14 @@ impl Tray {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None),
"main_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Tray点击事件: 显示主窗口");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
crate::module::lightweight::exit_lightweight_mode();
}
let _ = resolve::create_window(true);
let result = WindowManager::show_main_window();
log::info!(target: "app", "窗口显示结果: {:?}", result);
}
_ => {}
}
@@ -666,6 +685,17 @@ impl Tray {
log::info!(target: "app", "系统托盘创建成功");
Ok(())
}
// 托盘统一的状态更新函数
pub fn update_all_states(&self) -> Result<()> {
// 确保所有状态更新完成
self.update_menu()?;
self.update_icon(None)?;
self.update_tooltip()?;
self.update_tray_display()?;
Ok(())
}
}
fn create_tray_menu(
@@ -917,15 +947,23 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
feat::change_clash_mode(mode.into());
}
"open_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "托盘菜单点击: 打开窗口");
// 如果在轻量模式中,先退出轻量模式
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出");
crate::module::lightweight::exit_lightweight_mode();
}
// 然后创建窗口
let _ = resolve::create_window(true);
// 使用统一的窗口管理器显示窗口
let result = WindowManager::show_main_window();
log::info!(target: "app", "窗口显示结果: {:?}", result);
}
"system_proxy" => {
feat::toggle_system_proxy();
}
"tun_mode" => {
feat::toggle_tun_mode(None);
}
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None),
"copy_env" => feat::copy_clash_env(),
"open_app_dir" => {
let _ = cmd::open_app_dir();
@@ -938,7 +976,22 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
"restart_clash" => feat::restart_clash_core(),
"restart_app" => feat::restart_app(),
"entry_lightweight_mode" => entry_lightweight_mode(),
"entry_lightweight_mode" => {
// 处理轻量模式的切换
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
if was_lightweight {
crate::module::lightweight::exit_lightweight_mode();
} else {
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);
}
}
"quit" => {
feat::quit();
}
@@ -948,4 +1001,9 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
_ => {}
}
// 统一调用状态更新
if let Err(e) = Tray::global().update_all_states() {
log::warn!(target: "app", "更新托盘状态失败: {}", e);
}
}

View File

@@ -22,7 +22,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
let verge = Config::verge();
let verge = verge.latest();
(
verge.clash_core.clone(),
Some(verge.get_valid_clash_core()),
verge.enable_tun_mode.unwrap_or(false),
verge.enable_builtin_enhanced.unwrap_or(true),
verge.verge_socks_enabled.unwrap_or(false),

View File

@@ -45,8 +45,8 @@ pub fn use_script(
let config = use_lowercase(config.clone());
let config_str = serde_json::to_string(&config)?;
// 处理 name 参数中的特殊字符
let safe_name = escape_js_string(&name);
// 处理 name 参数中的特殊字符
let safe_name = escape_js_string_for_single_quote(&name);
let code = format!(
r#"try{{
@@ -64,18 +64,8 @@ pub fn use_script(
let result = result.to_string(&mut context).unwrap();
let result = result.to_std_string().unwrap();
// 处理 JS 执行结果中的特殊字符
let unescaped_result = unescape_js_string(&result);
if unescaped_result.starts_with("__error_flag__") {
anyhow::bail!(unescaped_result[15..].to_owned());
}
if unescaped_result == "\"\"" {
anyhow::bail!("main function should return object");
}
// 安全地解析 JSON 结果
let res: Result<Mapping, Error> = parse_json_safely(&unescaped_result);
// 直接解析JSON结果,不做其他解析
let res: Result<Mapping, Error> = parse_json_safely(&result);
let mut out = outputs.lock().unwrap();
match res {
@@ -90,72 +80,25 @@ pub fn use_script(
}
}
// 解析 JSON 字符串,处理可能的转义字符
fn parse_json_safely(json_str: &str) -> Result<Mapping, Error> {
// 移除可能的引号包裹
let json_str = if json_str.starts_with('"') && json_str.ends_with('"') {
&json_str[1..json_str.len() - 1]
let json_str = strip_outer_quotes(json_str);
Ok(serde_json::from_str::<Mapping>(json_str)?)
}
// 移除字符串外层的引号
fn strip_outer_quotes(s: &str) -> &str {
let s = s.trim();
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
&s[1..s.len() - 1]
} else {
json_str
};
// 处理可能的 JSON 字符串中的转义字符
let json_str = json_str.replace("\\\"", "\"");
Ok(serde_json::from_str::<Mapping>(&json_str)?)
s
}
}
// 转义 JS 字符串中的特殊字符
fn escape_js_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\'' => result.push_str("\\'"),
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
'\0' => result.push_str("\\0"),
_ => result.push(c),
}
}
result
}
// 反转义 JS 字符串中的特殊字符
fn unescape_js_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('0') => result.push('\0'),
Some('\\') => result.push('\\'),
Some('\'') => result.push('\''),
Some('"') => result.push('"'),
Some('u') => {
// 处理转义序列
let hex = chars.by_ref().take(4).collect::<String>();
if let Ok(codepoint) = u32::from_str_radix(&hex, 16) {
if let Some(ch) = char::from_u32(codepoint) {
result.push(ch);
}
}
}
Some(other) => result.push(other),
None => break,
}
} else {
result.push(c);
}
}
result
// 转义单引号和反斜杠用于单引号包裹的JavaScript字符串
fn escape_js_string_for_single_quote(s: &str) -> String {
s.replace('\\', "\\\\").replace('\'', "\\'")
}
#[test]
@@ -197,10 +140,9 @@ fn test_script() {
#[test]
fn test_escape_unescape() {
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
let escaped = escape_js_string(test_string);
let unescaped = unescape_js_string(&escaped);
assert_eq!(test_string, unescaped);
let escaped = escape_js_string_for_single_quote(test_string);
println!("Original: {}", test_string);
println!("Escaped: {}", escaped);
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
let parsed = parse_json_safely(json_str).unwrap();

View File

@@ -84,7 +84,7 @@ pub fn change_clash_mode(mode: String) {
after_change_clash_mode();
}
}
Err(err) => println!("{err}"),
Err(err) => log::error!(target: "app", "{err}"),
}
});
}

View File

@@ -2,7 +2,9 @@ use crate::{
cmd,
config::{Config, PrfItem, PrfOption},
core::{handle, CoreManager, *},
logging,
process::AsyncHandler,
utils::logging::Type,
};
use anyhow::{bail, Result};
@@ -29,7 +31,7 @@ pub async fn update_profile(
option: Option<PrfOption>,
auto_refresh: Option<bool>,
) -> Result<()> {
println!("[订阅更新] 开始更新订阅 {}", uid);
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true保持兼容性
let url_opt = {
@@ -39,13 +41,13 @@ pub async fn update_profile(
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote {
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
log::info!(target: "app", "[订阅更新] {} 不是远程订阅,跳过更新", uid);
None // 非远程订阅直接更新
} else if item.url.is_none() {
println!("[订阅更新] {} 缺少URL无法更新", uid);
log::warn!(target: "app", "[订阅更新] {} 缺少URL无法更新", uid);
bail!("failed to get the profile item url");
} else {
println!(
log::info!(target: "app",
"[订阅更新] {} 是远程订阅URL: {}",
uid,
item.url.clone().unwrap()
@@ -56,24 +58,24 @@ pub async fn update_profile(
let should_update = match url_opt {
Some((url, opt)) => {
println!("[订阅更新] 开始下载新的订阅内容");
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
// 尝试使用正常设置更新
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => {
println!("[订阅更新] 更新订阅配置成功");
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?;
let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
is_current && auto_refresh
}
Err(err) => {
// 首次更新失败尝试使用Clash代理
println!("[订阅更新] 正常更新失败: {}尝试使用Clash代理更新", err);
log::warn!(target: "app", "[订阅更新] 正常更新失败: {}尝试使用Clash代理更新", err);
// 发送通知
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
@@ -90,7 +92,7 @@ pub async fn update_profile(
// 使用Clash代理重试
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
Ok(mut item) => {
println!("[订阅更新] 使用Clash代理更新成功");
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
// 恢复原始代理设置到item
if let Some(option) = item.option.as_mut() {
@@ -110,11 +112,11 @@ pub async fn update_profile(
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
is_current && auto_refresh
}
Err(retry_err) => {
println!("[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{}", retry_err),
@@ -129,14 +131,14 @@ pub async fn update_profile(
};
if should_update {
println!("[订阅更新] 更新内核配置");
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
match CoreManager::global().update_config().await {
Ok(_) => {
println!("[订阅更新] 更新成功");
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
handle::Handle::refresh_clash();
}
Err(err) => {
println!("[订阅更新] 更新失败: {}", err);
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("update_failed", format!("{err}"));
log::error!(target: "app", "{err}");
}

View File

@@ -3,69 +3,37 @@ use crate::AppHandleManager;
use crate::{
config::Config,
core::{handle, sysopt, CoreManager},
logging,
module::mihomo::MihomoManager,
utils::resolve,
utils::logging::Type,
};
/// Open or close the dashboard window
#[allow(dead_code)]
pub fn open_or_close_dashboard() {
println!("Attempting to open/close dashboard");
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Attempting to open/close dashboard");
// 检查是否在轻量模式下
if crate::module::lightweight::is_in_lightweight_mode() {
println!("Currently in lightweight mode, exiting lightweight mode");
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode();
println!("Creating new window after exiting lightweight mode");
log::info!(target: "app", "Creating new window after exiting lightweight mode");
resolve::create_window(true);
let result = WindowManager::show_main_window();
log::info!(target: "app", "Window operation result: {:?}", result);
return;
}
if let Some(window) = handle::Handle::global().get_window() {
println!("Found existing window");
log::info!(target: "app", "Found existing window");
// 如果窗口存在,则切换其显示状态
match window.is_visible() {
Ok(visible) => {
println!("Window visibility status: {}", visible);
log::info!(target: "app", "Window visibility status: {}", visible);
if visible {
println!("Attempting to hide window");
log::info!(target: "app", "Attempting to hide window");
let _ = window.hide();
} else {
println!("Attempting to show and focus window");
log::info!(target: "app", "Attempting to show and focus window");
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
}
}
Err(e) => {
println!("Failed to get window visibility: {:?}", e);
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
}
}
} else {
println!("No existing window found, creating new window");
log::info!(target: "app", "No existing window found, creating new window");
resolve::create_window(true);
}
// 使用统一的窗口管理器切换窗口状态
let result = WindowManager::toggle_main_window();
log::info!(target: "app", "Window toggle result: {:?}", result);
}
/// 异步优化的应用退出函数
pub fn quit() {
use crate::process::AsyncHandler;
log::debug!(target: "app", "启动退出流程");
logging!(debug, Type::System, true, "启动退出流程");
// 获取应用句柄并设置退出标志
let app_handle = handle::Handle::global().app_handle().unwrap();
@@ -79,10 +47,16 @@ pub fn quit() {
// 使用异步任务处理资源清理,避免阻塞
AsyncHandler::spawn(move || async move {
log::info!(target: "app", "开始异步清理资源");
logging!(info, Type::System, true, "开始异步清理资源");
let cleanup_result = clean_async().await;
log::info!(target: "app", "资源清理完成,退出代码: {}", if cleanup_result { 0 } else { 1 });
logging!(
info,
Type::System,
true,
"资源清理完成,退出代码: {}",
if cleanup_result { 0 } else { 1 }
);
app_handle.exit(if cleanup_result { 0 } else { 1 });
});
}
@@ -90,7 +64,7 @@ pub fn quit() {
async fn clean_async() -> bool {
use tokio::time::{timeout, Duration};
log::info!(target: "app", "开始执行异步清理操作...");
logging!(info, Type::System, true, "开始执行异步清理操作...");
// 1. 处理TUN模式
let tun_task = async {
@@ -156,7 +130,12 @@ async fn clean_async() -> bool {
// 4. DNS恢复仅macOS
#[cfg(target_os = "macos")]
let dns_task = async {
match timeout(Duration::from_millis(1000), resolve::restore_public_dns()).await {
match timeout(
Duration::from_millis(1000),
crate::utils::resolve::restore_public_dns(),
)
.await
{
Ok(_) => {
log::info!(target: "app", "DNS设置已恢复");
true
@@ -178,10 +157,16 @@ async fn clean_async() -> bool {
let all_success = tun_success && proxy_success && core_success && dns_success;
log::info!(
target: "app",
logging!(
info,
Type::System,
true,
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
tun_success, proxy_success, core_success, dns_success, all_success
tun_success,
proxy_success,
core_success,
dns_success,
all_success
);
all_success
@@ -193,7 +178,7 @@ pub fn clean() -> bool {
let (tx, rx) = std::sync::mpsc::channel();
AsyncHandler::spawn(move || async move {
log::info!(target: "app", "开始执行清理操作...");
logging!(info, Type::System, true, "开始执行清理操作...");
// 使用已有的异步清理函数
let cleanup_result = clean_async().await;
@@ -204,11 +189,16 @@ pub fn clean() -> bool {
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
Ok(result) => {
log::info!(target: "app", "清理操作完成,结果: {}", result);
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
result
}
Err(_) => {
log::warn!(target: "app", "清理操作超时,返回成功状态避免阻塞");
logging!(
warn,
Type::System,
true,
"清理操作超时,返回成功状态避免阻塞"
);
true
}
}

View File

@@ -162,6 +162,14 @@ pub fn run() {
});
});
// 窗口管理
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
.build();
let _ = app.handle().plugin(window_state_plugin);
// 异步处理
let app_handle = app.handle().clone();
AsyncHandler::spawn(move || async move {
@@ -255,6 +263,7 @@ pub fn run() {
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::get_proxies,
cmd::force_refresh_proxies,
cmd::get_providers_proxies,
cmd::save_dns_config,
cmd::apply_dns_config,
@@ -368,7 +377,7 @@ pub fn run() {
if core::handle::Handle::global().is_exiting() {
return;
}
println!("closing window...");
log::info!(target: "app", "closing window...");
api.prevent_close();
if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide();

View File

@@ -1,6 +1,6 @@
use crate::{
config::Config,
core::{handle, timer::Timer},
core::{handle, timer::Timer, tray::Tray},
log_err, logging,
state::lightweight::LightWeightState,
utils::logging::Type,
@@ -30,39 +30,44 @@ where
pub fn run_once_auto_lightweight() {
LightWeightState::default().run_once_time(|| {
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(false);
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true);
let enable_auto = Config::verge()
.data()
.enable_auto_light_weight_mode
.unwrap_or(false);
.unwrap_or(true);
if enable_auto && is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"Add timer listener when creating window in silent start mode"
"正常创建窗口和添加定时器监听器"
);
set_lightweight_mode(true);
enable_auto_light_weight_mode();
set_lightweight_mode(false);
disable_auto_light_weight_mode();
// 触发托盘更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {}", e);
}
}
});
}
pub fn auto_lightweight_mode_init() {
if let Some(app_handle) = handle::Handle::global().app_handle() {
// 通过 app_handle.state 保证同步
let _ = app_handle.state::<Mutex<LightWeightState>>();
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
if enable_auto && !is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"Add timer listener when creating window normally"
);
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);
}
}
}
}
@@ -77,6 +82,11 @@ 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);
}
}
pub fn enable_auto_light_weight_mode() {
@@ -93,10 +103,18 @@ pub fn disable_auto_light_weight_mode() {
}
pub fn entry_lightweight_mode() {
use crate::utils::window_manager::WindowManager;
let result = WindowManager::hide_main_window();
logging!(
info,
Type::Lightweight,
true,
"轻量模式隐藏窗口结果: {:?}",
result
);
if let Some(window) = handle::Handle::global().get_window() {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
}
if let Some(webview) = window.get_webview_window("main") {
let _ = webview.destroy();
}
@@ -106,6 +124,9 @@ pub fn entry_lightweight_mode() {
}
set_lightweight_mode(true);
let _ = cancel_light_weight_timer();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
}
// 添加从轻量模式恢复的函数
@@ -125,6 +146,9 @@ pub fn exit_lightweight_mode() {
// 重置UI就绪状态
crate::utils::resolve::reset_ui_ready();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
}
#[cfg(target_os = "macos")]

View File

@@ -286,6 +286,9 @@ pub fn init_config() -> Result<()> {
<Result<()>>::Ok(())
}));
// 验证并修正verge.yaml中的clash_core配置
crate::log_err!(IVerge::validate_and_fix_config());
crate::log_err!(dirs::profiles_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;

View File

@@ -8,3 +8,4 @@ pub mod network;
pub mod resolve;
pub mod server;
pub mod tmpl;
pub mod window_manager;

View File

@@ -293,12 +293,7 @@ pub fn create_window(is_show: bool) -> bool {
);
if !is_show {
logging!(
info,
Type::Window,
true,
"Not to create window when starting in silent mode"
);
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
handle::Handle::notify_startup_completed();
return false;
}
@@ -307,8 +302,17 @@ pub fn create_window(is_show: bool) -> bool {
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
if is_show {
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
}
return true;
}
@@ -355,7 +359,7 @@ pub fn create_window(is_show: bool) -> bool {
console.log('[Tauri] 加载指示器已存在');
return;
}
console.log('[Tauri] 创建加载指示器');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'initial-loading-overlay';
@@ -363,15 +367,15 @@ pub fn create_window(is_show: bool) -> bool {
<div style="
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 9999;
display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transition: opacity 0.3s ease;
">
<div style="margin-bottom: 20px;">
<div style="
width: 40px; height: 40px; border: 3px solid #e3e3e3;
border-top: 3px solid #3498db; border-radius: 50%;
width: 40px; height: 40px; border: 3px solid #e3e3e3;
border-top: 3px solid #3498db; border-radius: 50%;
animation: spin 1s linear infinite;
"></div>
</div>
@@ -406,7 +410,7 @@ pub fn create_window(is_show: bool) -> bool {
} else {
createLoadingOverlay();
}
console.log('[Tauri] 窗口初始化脚本执行完成');
"#,
)

View File

@@ -0,0 +1,272 @@
use crate::{core::handle, logging, utils::logging::Type};
use tauri::{Manager, WebviewWindow, Wry};
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
/// 窗口操作结果
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowOperationResult {
/// 窗口已显示并获得焦点
Shown,
/// 窗口已隐藏
Hidden,
/// 创建了新窗口
Created,
/// 操作失败
Failed,
/// 无需操作
NoAction,
}
/// 窗口状态
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowState {
/// 窗口可见且有焦点
VisibleFocused,
/// 窗口可见但无焦点
VisibleUnfocused,
/// 窗口最小化
Minimized,
/// 窗口隐藏
Hidden,
/// 窗口不存在
NotExist,
}
/// 统一的窗口管理器
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) {
WindowState::VisibleFocused
} else {
WindowState::VisibleUnfocused
}
} else {
WindowState::Hidden
}
} else {
WindowState::NotExist
}
}
/// 获取主窗口实例
pub fn get_main_window() -> Option<WebviewWindow<Wry>> {
handle::Handle::global()
.app_handle()
.and_then(|app| app.get_webview_window("main"))
}
/// 智能显示主窗口
pub fn show_main_window() -> WindowOperationResult {
logging!(info, Type::Window, true, "开始智能显示主窗口");
logging!(
debug,
Type::Window,
true,
"{}",
Self::get_window_status_info()
);
let current_state = Self::get_main_window_state();
match current_state {
WindowState::NotExist => {
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
if Self::create_new_window() {
WindowOperationResult::Created
} else {
WindowOperationResult::Failed
}
}
WindowState::VisibleFocused => {
logging!(info, Type::Window, true, "窗口已经可见且有焦点,无需操作");
WindowOperationResult::NoAction
}
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
if let Some(window) = Self::get_main_window() {
Self::activate_window(&window)
} else {
WindowOperationResult::Failed
}
}
}
}
/// 切换主窗口显示状态(显示/隐藏)
pub fn toggle_main_window() -> WindowOperationResult {
logging!(info, Type::Window, true, "开始切换主窗口显示状态");
let current_state = Self::get_main_window_state();
logging!(
info,
Type::Window,
true,
"当前窗口状态: {:?}",
current_state
);
match current_state {
WindowState::NotExist => {
// 窗口不存在,创建新窗口
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
} else {
WindowOperationResult::Failed
}
} else {
WindowOperationResult::Failed
}
}
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
// 窗口存在但不可见或无焦点,激活它
if let Some(window) = Self::get_main_window() {
Self::activate_window(&window)
} else {
WindowOperationResult::Failed
}
}
}
}
/// 激活窗口(取消最小化、显示、设置焦点)
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult {
logging!(info, Type::Window, true, "开始激活窗口");
let mut operations_successful = true;
// 1. 如果窗口最小化,先取消最小化
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
if let Err(e) = window.unminimize() {
logging!(warn, Type::Window, true, "取消最小化失败: {}", e);
operations_successful = false;
}
}
// 2. 显示窗口
if let Err(e) = window.show() {
logging!(warn, Type::Window, true, "显示窗口失败: {}", e);
operations_successful = false;
}
// 3. 设置焦点
if let Err(e) = window.set_focus() {
logging!(warn, Type::Window, true, "设置窗口焦点失败: {}", e);
operations_successful = false;
}
// 4. 平台特定的激活策略
#[cfg(target_os = "macos")]
{
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
AppHandleManager::global().set_activation_policy_regular();
}
#[cfg(target_os = "windows")]
{
// Windows 尝试额外的激活方法
if let Err(e) = window.set_always_on_top(true) {
logging!(
debug,
Type::Window,
true,
"设置置顶失败(非关键错误): {}",
e
);
}
// 立即取消置顶
if let Err(e) = window.set_always_on_top(false) {
logging!(
debug,
Type::Window,
true,
"取消置顶失败(非关键错误): {}",
e
);
}
}
if operations_successful {
logging!(info, Type::Window, true, "窗口激活成功");
WindowOperationResult::Shown
} else {
logging!(warn, Type::Window, true, "窗口激活部分失败");
WindowOperationResult::Failed
}
}
/// 隐藏主窗口
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
}
} else {
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
WindowOperationResult::NoAction
}
}
/// 检查窗口是否可见
pub fn is_main_window_visible() -> bool {
Self::get_main_window()
.map(|window| window.is_visible().unwrap_or(false))
.unwrap_or(false)
}
/// 检查窗口是否有焦点
pub fn is_main_window_focused() -> bool {
Self::get_main_window()
.map(|window| window.is_focused().unwrap_or(false))
.unwrap_or(false)
}
/// 检查窗口是否最小化
pub fn is_main_window_minimized() -> bool {
Self::get_main_window()
.map(|window| window.is_minimized().unwrap_or(false))
.unwrap_or(false)
}
/// 创建新窗口现有的实现
fn create_new_window() -> bool {
use crate::utils::resolve;
resolve::create_window(true)
}
/// 获取详细的窗口状态信息
pub fn get_window_status_info() -> String {
let state = Self::get_main_window_state();
let is_visible = Self::is_main_window_visible();
let is_focused = Self::is_main_window_focused();
let is_minimized = Self::is_main_window_minimized();
format!(
"窗口状态: {:?} | 可见: {} | 有焦点: {} | 最小化: {}",
state, is_visible, is_focused, is_minimized
)
}
}

View File

@@ -1,5 +1,5 @@
{
"version": "2.3.0",
"version": "2.3.1",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,

View File

@@ -52,19 +52,16 @@ export const useCustomTheme = () => {
let isMounted = true;
const timerId = setTimeout(() => {
if (!isMounted) return;
appWindow
.theme()
.then((systemTheme) => {
if (isMounted && systemTheme) {
setMode(systemTheme);
}
})
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
}, 0);
appWindow
.theme()
.then((systemTheme) => {
if (isMounted && systemTheme) {
setMode(systemTheme);
}
})
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
if (isMounted) {
@@ -74,7 +71,6 @@ export const useCustomTheme = () => {
return () => {
isMounted = false;
clearTimeout(timerId);
unlistenPromise
.then((unlistenFn) => {
if (typeof unlistenFn === "function") {
@@ -131,6 +127,7 @@ export const useCustomTheme = () => {
},
background: {
paper: dt.background_color,
default: dt.background_color,
},
},
shadows: Array(25).fill("none") as Shadows,
@@ -157,6 +154,10 @@ export const useCustomTheme = () => {
warning: { main: dt.warning_color },
success: { main: dt.success_color },
text: { primary: dt.primary_text, secondary: dt.secondary_text },
background: {
paper: dt.background_color,
default: dt.background_color,
},
},
typography: { fontFamily: dt.font_family },
});
@@ -164,9 +165,10 @@ export const useCustomTheme = () => {
const rootEle = document.documentElement;
if (rootEle) {
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const backgroundColor =
mode === "light" ? "#ECECEC" : dt.background_color;
const selectColor = mode === "light" ? "#f5f5f5" : "#3E3E3E";
const scrollColor = mode === "light" ? "#90939980" : "#555555";
const dividerColor =
mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";
@@ -182,16 +184,68 @@ export const useCustomTheme = () => {
"--background-color-alpha",
alpha(muiTheme.palette.primary.main, 0.1),
);
// 添加CSS变量
rootEle.style.setProperty(
"--window-border-color",
mode === "light" ? "#cccccc" : "#1E1E1E",
);
rootEle.style.setProperty(
"--scrollbar-bg",
mode === "light" ? "#f1f1f1" : "#2E303D",
);
rootEle.style.setProperty(
"--scrollbar-thumb",
mode === "light" ? "#c1c1c1" : "#555555",
);
}
// inject css
let styleElement = document.querySelector("style#verge-theme");
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = "verge-theme";
document.head.appendChild(styleElement!);
}
if (styleElement) {
styleElement.innerHTML = setting.css_injection || "";
// 添加全局样式,确保所有元素都使用暗色主题
const globalStyles = `
/* 修复滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: var(--scrollbar-bg);
}
::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: ${mode === "light" ? "#a1a1a1" : "#666666"};
}
/* 确保所有元素都使用正确的背景色 */
body, html {
background-color: var(--background-color) !important;
}
/* 修复可能的白色边框 */
.MuiPaper-root {
border-color: var(--window-border-color) !important;
}
/* 确保模态框和对话框也使用暗色主题 */
.MuiDialog-paper {
background-color: ${mode === "light" ? "#ffffff" : "#2E303D"} !important;
}
/* 移除可能的白色点或线条 */
* {
outline: none !important;
box-shadow: none !important;
}
`;
styleElement.innerHTML = (setting.css_injection || "") + globalStyles;
}
const { palette } = muiTheme;

View File

@@ -1,6 +1,5 @@
import { BaseDialog, DialogRef } from "@/components/base";
import { useClashInfo } from "@/hooks/use-clash";
import { patchClashConfig } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { ContentCopy } from "@mui/icons-material";
import {
@@ -42,19 +41,19 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置
const onSave = useLockFn(async () => {
if (!controller.trim()) {
showNotice("error", t("Controller address cannot be empty"), 3000);
showNotice("error", t("Controller address cannot be empty"));
return;
}
if (!secret.trim()) {
showNotice("error", t("Secret cannot be empty"), 3000);
showNotice("error", t("Secret cannot be empty"));
return;
}
try {
setIsSaving(true);
await patchInfo({ "external-controller": controller, secret });
showNotice("success", t("Configuration saved successfully"), 2000);
showNotice("success", t("Configuration saved successfully"));
setOpen(false);
} catch (err: any) {
showNotice(
@@ -73,9 +72,9 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
try {
await navigator.clipboard.writeText(text);
setCopySuccess(type);
setTimeout(() => setCopySuccess(null), 2000);
setTimeout(() => setCopySuccess(null));
} catch (err) {
showNotice("error", t("Failed to copy"), 2000);
showNotice("error", t("Failed to copy"));
}
},
);

View File

@@ -10,11 +10,32 @@ export const useProfiles = () => {
const { data: profiles, mutate: mutateProfiles } = useSWR(
"getProfiles",
getProfiles,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 2000,
errorRetryCount: 2,
errorRetryInterval: 1000,
},
);
const patchProfiles = async (value: Partial<IProfilesConfig>) => {
await patchProfilesConfig(value);
mutateProfiles();
// 立即更新本地状态
if (value.current && profiles) {
const optimisticUpdate = {
...profiles,
current: value.current,
};
mutateProfiles(optimisticUpdate, false); // 不重新验证
}
try {
await patchProfilesConfig(value);
mutateProfiles();
} catch (error) {
mutateProfiles();
throw error;
}
};
const patchCurrent = async (value: Partial<IProfileItem>) => {
@@ -26,40 +47,90 @@ export const useProfiles = () => {
// 根据selected的节点选择
const activateSelected = async () => {
const proxiesData = await getProxies();
const profileData = await getProfiles();
try {
console.log("[ActivateSelected] 开始处理代理选择");
if (!profileData || !proxiesData) return;
const [proxiesData, profileData] = await Promise.all([
getProxies(),
getProfiles(),
]);
const current = profileData.items?.find(
(e) => e && e.uid === profileData.current,
);
if (!current) return;
// init selected array
const { selected = [] } = current;
const selectedMap = Object.fromEntries(
selected.map((each) => [each.name!, each.now!]),
);
let hasChange = false;
const newSelected: typeof selected = [];
const { global, groups } = proxiesData;
[global, ...groups].forEach(({ type, name, now }) => {
if (!now || type !== "Selector") return;
if (selectedMap[name] != null && selectedMap[name] !== now) {
hasChange = true;
updateProxy(name, selectedMap[name]);
if (!profileData || !proxiesData) {
console.log("[ActivateSelected] 代理或配置数据不可用,跳过处理");
return;
}
newSelected.push({ name, now: selectedMap[name] });
});
if (hasChange) {
patchProfile(profileData.current!, { selected: newSelected });
mutate("getProxies", getProxies());
const current = profileData.items?.find(
(e) => e && e.uid === profileData.current,
);
if (!current) {
console.log("[ActivateSelected] 未找到当前profile配置");
return;
}
// 检查是否有saved的代理选择
const { selected = [] } = current;
if (selected.length === 0) {
console.log("[ActivateSelected] 当前profile无保存的代理选择跳过");
return;
}
console.log(
`[ActivateSelected] 当前profile有 ${selected.length} 个代理选择配置`,
);
const selectedMap = Object.fromEntries(
selected.map((each) => [each.name!, each.now!]),
);
let hasChange = false;
const newSelected: typeof selected = [];
const { global, groups } = proxiesData;
// 处理所有代理组
[global, ...groups].forEach(({ type, name, now }) => {
if (!now || type !== "Selector") {
if (selectedMap[name] != null) {
newSelected.push({ name, now: now || selectedMap[name] });
}
return;
}
const targetProxy = selectedMap[name];
if (targetProxy != null && targetProxy !== now) {
console.log(
`[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${targetProxy}`,
);
hasChange = true;
updateProxy(name, targetProxy);
}
newSelected.push({ name, now: targetProxy || now });
});
if (!hasChange) {
console.log("[ActivateSelected] 所有代理选择已经是目标状态,无需更新");
return;
}
console.log(`[ActivateSelected] 完成代理切换,保存新的选择配置`);
try {
await patchProfile(profileData.current!, { selected: newSelected });
console.log("[ActivateSelected] 代理选择配置保存成功");
setTimeout(() => {
mutate("getProxies", getProxies());
}, 100);
} catch (error: any) {
console.error(
"[ActivateSelected] 保存代理选择配置失败:",
error.message,
);
}
} catch (error: any) {
console.error("[ActivateSelected] 处理代理选择失败:", error.message);
}
};

View File

@@ -419,6 +419,7 @@
"Clash Core Restarted": "Clash Core Restarted",
"GeoData Updated": "GeoData Updated",
"Currently on the Latest Version": "Currently on the Latest Version",
"Already Using Latest Core": "Already Using Latest Core",
"Import Subscription Successful": "Import subscription successful",
"WebDAV Server URL": "WebDAV Server URL",
"Username": "Username",

View File

@@ -419,6 +419,7 @@
"Clash Core Restarted": "已重启 Clash 内核",
"GeoData Updated": "已更新 GeoData",
"Currently on the Latest Version": "当前已是最新版本",
"Already Using Latest Core": "已是最新内核版本",
"Import Subscription Successful": "导入订阅成功",
"WebDAV Server URL": "WebDAV 服务器地址 http(s)://",
"Username": "用户名",
@@ -602,8 +603,8 @@
"Proxy Mode": "代理模式",
"Group": "代理组",
"Proxy": "节点",
"IP Information Card": "IP信息卡",
"IP Information": "IP信息",
"IP Information Card": "IP 信息卡",
"IP Information": "IP 信息",
"Failed to get IP info": "获取IP信息失败",
"ISP": "服务商",
"ASN": "自治域",

View File

@@ -169,7 +169,13 @@ const Layout = () => {
const handleNotice = useCallback(
(payload: [string, string]) => {
const [status, msg] = payload;
handleNoticeMessage(status, msg, t, navigate);
setTimeout(() => {
try {
handleNoticeMessage(status, msg, t, navigate);
} catch (error) {
console.error("[Layout] 处理通知消息失败:", error);
}
}, 0);
},
[t, navigate],
);
@@ -220,12 +226,35 @@ const Layout = () => {
const cleanupWindow = setupWindowListeners();
return () => {
listeners.forEach((listener) => {
if (typeof listener.then === "function") {
listener.then((unlisten) => unlisten());
}
});
cleanupWindow.then((cleanup) => cleanup());
setTimeout(() => {
listeners.forEach((listener) => {
if (typeof listener.then === "function") {
listener
.then((unlisten) => {
try {
unlisten();
} catch (error) {
console.error("[Layout] 清理事件监听器失败:", error);
}
})
.catch((error) => {
console.error("[Layout] 获取unlisten函数失败:", error);
});
}
});
cleanupWindow
.then((cleanup) => {
try {
cleanup();
} catch (error) {
console.error("[Layout] 清理窗口监听器失败:", error);
}
})
.catch((error) => {
console.error("[Layout] 获取cleanup函数失败:", error);
});
}, 0);
};
}, [handleNotice]);
@@ -471,6 +500,10 @@ const Layout = () => {
square
elevation={0}
className={`${OS} layout`}
style={{
borderTopLeftRadius: "0px",
borderTopRightRadius: "0px",
}}
onContextMenu={(e) => {
if (
OS === "windows" &&
@@ -488,8 +521,8 @@ const Layout = () => {
? {
borderRadius: "8px",
border: "1px solid var(--divider-color)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
width: "100vw",
height: "100vh",
}
: {},
]}

View File

@@ -190,27 +190,53 @@ const ProfilePage = () => {
}
};
const activateProfile = async (profile: string, notifySuccess: boolean) => {
// 避免大多数情况下loading态闪烁
const reset = setTimeout(() => {
setActivatings((prev) => [...prev, profile]);
}, 100);
try {
const success = await patchProfiles({ current: profile });
await mutateLogs();
closeAllConnections();
await activateSelected();
if (notifySuccess && success) {
showNotice("success", t("Profile Switched"), 1000);
const activateProfile = useLockFn(
async (profile: string, notifySuccess: boolean) => {
if (profiles.current === profile && !notifySuccess) {
console.log(
`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`,
);
return;
}
} catch (err: any) {
showNotice("error", err?.message || err.toString(), 4000);
} finally {
clearTimeout(reset);
setActivatings([]);
}
};
// 避免大多数情况下loading态闪烁
const reset = setTimeout(() => {
setActivatings((prev) => [...prev, profile]);
}, 100);
try {
console.log(`[Profile] 开始切换到: ${profile}`);
const success = await patchProfiles({ current: profile });
await mutateLogs();
closeAllConnections();
if (notifySuccess && success) {
showNotice("success", t("Profile Switched"), 1000);
}
// 立即清除loading状态
clearTimeout(reset);
setActivatings([]);
console.log(`[Profile] 切换到 ${profile} 完成,开始后台处理`);
setTimeout(async () => {
try {
await activateSelected();
console.log(`[Profile] 后台处理完成`);
} catch (err: any) {
console.warn("Failed to activate selected proxies:", err);
}
}, 50);
} catch (err: any) {
console.error(`[Profile] 切换失败:`, err);
showNotice("error", err?.message || err.toString(), 4000);
clearTimeout(reset);
setActivatings([]);
}
},
);
const onSelect = useLockFn(async (current: string, force: boolean) => {
if (!force && current === profiles.current) return;
await activateProfile(current, true);
@@ -300,31 +326,45 @@ const ProfilePage = () => {
// 监听后端配置变更
useEffect(() => {
let unlistenPromise: Promise<() => void> | undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
let lastProfileId: string | null = null;
let lastUpdateTime = 0;
const debounceDelay = 200;
const setupListener = async () => {
unlistenPromise = listen<string>("profile-changed", (event) => {
console.log("Profile changed event received:", event.payload);
if (timeoutId) {
clearTimeout(timeoutId);
const newProfileId = event.payload;
const now = Date.now();
console.log(`[Profile] 收到配置变更事件: ${newProfileId}`);
if (
lastProfileId === newProfileId &&
now - lastUpdateTime < debounceDelay
) {
console.log(`[Profile] 重复事件被防抖,跳过`);
return;
}
timeoutId = setTimeout(() => {
mutateProfiles();
timeoutId = undefined;
}, 300);
lastProfileId = newProfileId;
lastUpdateTime = now;
console.log(`[Profile] 执行配置数据刷新`);
// 使用异步调度避免阻塞事件处理
setTimeout(() => {
mutateProfiles().catch((error) => {
console.error("[Profile] 配置数据刷新失败:", error);
});
}, 0);
});
};
setupListener();
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
unlistenPromise?.then((unlisten) => unlisten());
unlistenPromise?.then((unlisten) => unlisten()).catch(console.error);
};
}, [mutateProfiles, t]);
}, [mutateProfiles]);
return (
<BasePage

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useMemo } from "react";
import { createContext, useContext, useMemo, useEffect } from "react";
import useSWR from "swr";
import useSWRSubscription from "swr/subscription";
import {
@@ -8,10 +8,16 @@ import {
getProxyProviders,
getRuleProviders,
} from "@/services/api";
import { getSystemProxy, getRunningMode, getAppUptime } from "@/services/cmds";
import {
getSystemProxy,
getRunningMode,
getAppUptime,
forceRefreshProxies,
} from "@/services/cmds";
import { useClashInfo } from "@/hooks/use-clash";
import { createAuthSockette } from "@/utils/websocket";
import { useVisibility } from "@/hooks/use-visibility";
import { listen } from "@tauri-apps/api/event";
// 定义AppDataContext类型 - 使用宽松类型
interface AppDataContextType {
@@ -64,6 +70,126 @@ export const AppDataProvider = ({
},
);
// 监听profile和clash配置变更事件
useEffect(() => {
let profileUnlisten: Promise<() => void> | undefined;
let lastProfileId: string | null = null;
let lastUpdateTime = 0;
const refreshThrottle = 500;
const setupEventListeners = async () => {
try {
// 监听profile切换事件
profileUnlisten = listen<string>("profile-changed", (event) => {
const newProfileId = event.payload;
const now = Date.now();
console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`);
if (
lastProfileId === newProfileId &&
now - lastUpdateTime < refreshThrottle
) {
console.log("[AppDataProvider] 重复事件被防抖,跳过");
return;
}
lastProfileId = newProfileId;
lastUpdateTime = now;
setTimeout(async () => {
try {
console.log("[AppDataProvider] 强制刷新代理缓存");
const refreshPromise = Promise.race([
forceRefreshProxies(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("forceRefreshProxies timeout")),
8000,
),
),
]);
await refreshPromise;
console.log("[AppDataProvider] 刷新前端代理数据");
await refreshProxy();
console.log("[AppDataProvider] Profile切换的代理数据刷新完成");
} catch (error) {
console.error("[AppDataProvider] 强制刷新代理缓存失败:", error);
refreshProxy().catch((e) =>
console.warn("[AppDataProvider] 普通刷新也失败:", e),
);
}
}, 0);
});
// 监听Clash配置刷新事件(enhance操作等)
const handleRefreshClash = () => {
const now = Date.now();
console.log("[AppDataProvider] Clash配置刷新事件");
if (now - lastUpdateTime > refreshThrottle) {
lastUpdateTime = now;
setTimeout(async () => {
try {
console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存");
// 添加超时保护
const refreshPromise = Promise.race([
forceRefreshProxies(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("forceRefreshProxies timeout")),
8000,
),
),
]);
await refreshPromise;
await refreshProxy();
} catch (error) {
console.error(
"[AppDataProvider] Clash刷新时强制刷新代理缓存失败:",
error,
);
refreshProxy().catch((e) =>
console.warn("[AppDataProvider] Clash刷新普通刷新也失败:", e),
);
}
}, 0);
}
};
window.addEventListener(
"verge://refresh-clash-config",
handleRefreshClash,
);
return () => {
window.removeEventListener(
"verge://refresh-clash-config",
handleRefreshClash,
);
};
} catch (error) {
console.error("[AppDataProvider] 事件监听器设置失败:", error);
return () => {};
}
};
const cleanupPromise = setupEventListeners();
return () => {
profileUnlisten?.then((unlisten) => unlisten()).catch(console.error);
cleanupPromise.then((cleanup) => cleanup());
};
}, [refreshProxy]);
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
"getClashConfig",
getClashConfig,

View File

@@ -220,6 +220,12 @@ export async function cmdGetProxyDelay(
}
}
/// 用于profile切换等场景
export async function forceRefreshProxies() {
console.log("[API] 强制刷新代理缓存");
return invoke<any>("force_refresh_proxies");
}
export async function cmdTestDelay(url: string) {
return invoke<number>("test_delay", { url });
}