Compare commits
85 Commits
updater-al
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e855e4755 | ||
|
|
a8b75aeabd | ||
|
|
854d42180a | ||
|
|
e94724595c | ||
|
|
6f1d9ba1b4 | ||
|
|
c090ae3b11 | ||
|
|
3303e95713 | ||
|
|
5cdc5075f8 | ||
|
|
eb1e4fe0c3 | ||
|
|
b1e3283a24 | ||
|
|
ce3b0bb479 | ||
|
|
25b295f2a8 | ||
|
|
18b7366258 | ||
|
|
565771a3ea | ||
|
|
f9376f6903 | ||
|
|
8e0f5b6abd | ||
|
|
41f32231f0 | ||
|
|
e1968891ac | ||
|
|
f04e707b10 | ||
|
|
0bb795b0e1 | ||
|
|
1c5e43690e | ||
|
|
f604416532 | ||
|
|
87ee07d481 | ||
|
|
7dec9cbe9b | ||
|
|
1274ba2324 | ||
|
|
d6014865d6 | ||
|
|
48a5ff6948 | ||
|
|
dd3950e46d | ||
|
|
1708246866 | ||
|
|
b0734f5935 | ||
|
|
11768862d3 | ||
|
|
ef409216d8 | ||
|
|
f739afea3d | ||
|
|
d5266fa003 | ||
|
|
149bdd5175 | ||
|
|
ec99e24ca1 | ||
|
|
7cc893383e | ||
|
|
3902480d39 | ||
|
|
686490ded1 | ||
|
|
4435a5aee4 | ||
|
|
d9e3a47894 | ||
|
|
c96be18187 | ||
|
|
47416dd3f8 | ||
|
|
4486f734bb | ||
|
|
bea6a2c8f7 | ||
|
|
82af9ed78e | ||
|
|
2b9e38d259 | ||
|
|
7c4222aed2 | ||
|
|
a574ced428 | ||
|
|
cf437e6d94 | ||
|
|
e1bb8aa125 | ||
|
|
c11bdd81e9 | ||
|
|
f1192c95a8 | ||
|
|
ae187cc21a | ||
|
|
a7875718f7 | ||
|
|
5db1f7cda7 | ||
|
|
cb98b17052 | ||
|
|
7aee1c6d6e | ||
|
|
f22199b7d9 | ||
|
|
0a8d6e5147 | ||
|
|
6d519dac1e | ||
|
|
d5a174c71b | ||
|
|
628de70e89 | ||
|
|
fee08f3826 | ||
|
|
bdfc383a18 | ||
|
|
f6b5524e0e | ||
|
|
e7461fccab | ||
|
|
a92872c831 | ||
|
|
094feb74ec | ||
|
|
9b1c660306 | ||
|
|
4b860ba897 | ||
|
|
3d8b2cf35f | ||
|
|
41fc13cfe2 | ||
|
|
5fde5dcc7c | ||
|
|
034885d810 | ||
|
|
3f7a7b8cd2 | ||
|
|
98dc50a9ed | ||
|
|
5dd820d12e | ||
|
|
bc30db2875 | ||
|
|
abe914d446 | ||
|
|
cc65ce6812 | ||
|
|
1a6454ee79 | ||
|
|
b72f397369 | ||
|
|
e698fe8d18 | ||
|
|
c8cad1c295 |
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: clash-verge-rev
|
||||||
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -61,5 +61,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to debug, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
|
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to debug, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
|
||||||
|
value: |
|
||||||
|
<details><summary>日志内容 / Log Content</summary>
|
||||||
|
```log
|
||||||
|
<!-- 在此处粘贴完整日志 / Paste the full log here -->
|
||||||
|
|
||||||
|
```
|
||||||
|
</details>
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
77
.github/workflows/autobuild.yml
vendored
@@ -2,9 +2,9 @@ name: Auto Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
# schedule:
|
||||||
# UTC+8 0,6,12,18
|
# # UTC+8 0,6,12,18
|
||||||
- cron: "0 16,22,4,10 * * *"
|
# - cron: "0 16,22,4,10 * * *"
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
TAG_NAME: autobuild
|
TAG_NAME: autobuild
|
||||||
@@ -77,6 +77,15 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Update git tag
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git tag -f ${{ env.TAG_NAME }}
|
||||||
|
git push --force origin ${{ env.TAG_NAME }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Fetch UPDATE logs
|
- name: Fetch UPDATE logs
|
||||||
id: fetch_update_logs
|
id: fetch_update_logs
|
||||||
run: |
|
run: |
|
||||||
@@ -111,31 +120,25 @@ jobs:
|
|||||||
cat > release.txt << EOF
|
cat > release.txt << EOF
|
||||||
$UPDATE_LOGS
|
$UPDATE_LOGS
|
||||||
|
|
||||||
## 我应该下载哪个版本?
|
## Which version should I download?
|
||||||
|
|
||||||
### MacOS
|
### MacOS
|
||||||
- MacOS intel芯片: x64.dmg
|
- MacOS Intel Chip: x64.dmg
|
||||||
- MacOS apple M芯片: aarch64.dmg
|
- MacOS Apple M Chip: aarch64.dmg
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
- Linux 64位: amd64.deb/amd64.rpm
|
- Linux 64-bit: amd64.deb/amd64.rpm
|
||||||
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
- Linux arm64: arm64.deb/aarch64.rpm
|
||||||
- Linux armv7架构: armhf.deb/armhfp.rpm
|
- Linux armv7: armhf.deb/armhfp.rpm
|
||||||
|
|
||||||
### Windows (不再支持Win7)
|
### Windows (Win7 is no longer supported)
|
||||||
#### 正常版本(推荐)
|
#### Normal version (recommended)
|
||||||
- 64位: x64-setup.exe
|
- 64-bit: x64-setup.exe
|
||||||
- arm64架构: arm64-setup.exe
|
- arm64: arm64-setup.exe
|
||||||
#### 便携版问题很多不再提供
|
#### Portable version is no longer available with many problems
|
||||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
#### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
|
||||||
- 64位: x64_fixed_webview2-setup.exe
|
- 64-bit: x64_fixed_webview2-setup.exe
|
||||||
- arm64架构: arm64_fixed_webview2-setup.exe
|
- arm64: arm64_fixed_webview2-setup.exe
|
||||||
|
|
||||||
### FAQ
|
|
||||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
|
||||||
|
|
||||||
### 稳定机场VPN推荐
|
|
||||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
@@ -150,6 +153,28 @@ jobs:
|
|||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|
||||||
|
clean_old_assets:
|
||||||
|
name: Clean Old Release Assets
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: update_tag
|
||||||
|
if: ${{ needs.update_tag.result == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Remove old assets from release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAG_NAME: ${{ env.TAG_NAME }}
|
||||||
|
run: |
|
||||||
|
VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
|
||||||
|
for asset in $assets; do
|
||||||
|
if [[ "$asset" != *"$VERSION"* ]]; then
|
||||||
|
echo "Deleting old asset: $asset"
|
||||||
|
gh release delete-asset "$TAG_NAME" "$asset" -y
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
autobuild-x86-windows-macos-linux:
|
autobuild-x86-windows-macos-linux:
|
||||||
name: Autobuild x86 Windows, MacOS and Linux
|
name: Autobuild x86 Windows, MacOS and Linux
|
||||||
needs: update_tag
|
needs: update_tag
|
||||||
@@ -217,12 +242,6 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
with:
|
with:
|
||||||
tagName: ${{ env.TAG_NAME }}
|
tagName: ${{ env.TAG_NAME }}
|
||||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
|
|||||||
4
.github/workflows/fmt.yml
vendored
@@ -26,11 +26,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- run: npm i -g --force corepack
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
- run: pnpm i --frozen-lockfile
|
- run: corepack enable
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm format:check
|
- run: pnpm format:check
|
||||||
|
|
||||||
# taplo:
|
# taplo:
|
||||||
|
|||||||
35
.github/workflows/release.yml
vendored
@@ -103,15 +103,9 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
with:
|
with:
|
||||||
tagName: v__VERSION__
|
tagName: v__VERSION__
|
||||||
releaseName: "Clash Verge Rev v__VERSION__"
|
releaseName: "Clash Verge Rev Lite v__VERSION__"
|
||||||
releaseBody: "More new features are now supported."
|
releaseBody: "More new features are now supported."
|
||||||
tauriScript: pnpm
|
tauriScript: pnpm
|
||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
@@ -231,7 +225,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: v${{env.VERSION}}
|
tag_name: v${{env.VERSION}}
|
||||||
name: "Clash Verge Rev v${{env.VERSION}}"
|
name: "Clash Verge Rev Lite v${{env.VERSION}}"
|
||||||
body: "More new features are now supported."
|
body: "More new features are now supported."
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
@@ -322,7 +316,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: v${{steps.build.outputs.appVersion}}
|
tag_name: v${{steps.build.outputs.appVersion}}
|
||||||
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
|
name: "Clash Verge Rev Lite v${{steps.build.outputs.appVersion}}"
|
||||||
body: "More new features are now supported."
|
body: "More new features are now supported."
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
@@ -382,26 +376,3 @@ jobs:
|
|||||||
run: pnpm updater-fixed-webview2
|
run: pnpm updater-fixed-webview2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
submit-to-winget:
|
|
||||||
name: Submit to Winget
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [release-update]
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Get Version
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install jq
|
|
||||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
|
||||||
- name: Submit to Winget
|
|
||||||
uses: vedantmgoyal9/winget-releaser@main
|
|
||||||
with:
|
|
||||||
identifier: ClashVergeRev.ClashVergeRev
|
|
||||||
version: ${{env.VERSION}}
|
|
||||||
release-tag: v${{env.VERSION}}
|
|
||||||
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
|
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# $1: remote name (e.g., origin)
|
||||||
|
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
|
||||||
|
|
||||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||||
cargo clippy --manifest-path ./src-tauri/Cargo.toml
|
cargo clippy --manifest-path ./src-tauri/Cargo.toml
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@@ -8,11 +11,9 @@ if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
remote_name="$1"
|
# 只在 push 到 origin 并且 origin 指向目标仓库时执行格式检查
|
||||||
remote_url=$(git remote get-url "$remote_name")
|
if [ "$1" = "origin" ] && echo "$2" | grep -Eq 'github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$'; then
|
||||||
|
echo "[pre-push] Detected push to origin (clash-verge-rev/clash-verge-rev)"
|
||||||
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..."
|
echo "[pre-push] Running pnpm format:check..."
|
||||||
|
|
||||||
pnpm format:check
|
pnpm format:check
|
||||||
|
|||||||
18
.prettierrc
@@ -1,6 +1,16 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"printWidth": 80,
|
||||||
"semi": false,
|
"tabWidth": 2,
|
||||||
"trailingComma": "none",
|
"useTabs": false,
|
||||||
"experimentalOperatorPosition": "start"
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"embeddedLanguageFormatting": "auto"
|
||||||
}
|
}
|
||||||
|
|||||||
74
README.md
@@ -1,7 +1,7 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="./src-tauri/icons/icon.png" alt="Clash" width="128" />
|
<img src="./src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||||
<br>
|
<br>
|
||||||
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
Fork of <a href="https://github.com/clash-verge-rev/clash-verge-rev">Clash Verge Rev</a>
|
||||||
<br>
|
<br>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -11,74 +11,26 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
|||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||
| Dark | Light |
|
| Dark | Light |
|
||||||
| -------------------------------- | --------------------------------- |
|
| ----------------------------------- | ------------------------------------ |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
Go to the [Release page](https://github.com/coolcoala/clash-verge-rev-lite/releases) to download the corresponding installation package<br>
|
||||||
Go to the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
|
||||||
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||||
|
|
||||||
#### 我应当怎样选择发行版
|
### Telegram channel: ---
|
||||||
|
|
||||||
| 版本 | 特征 | 链接 |
|
|
||||||
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
|
|
||||||
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
|
||||||
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
|
||||||
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
|
||||||
|
|
||||||
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### TG 频道: [@clash_verge_rev](https://t.me/clash_verge_re)
|
|
||||||
|
|
||||||
## Promotion
|
|
||||||
|
|
||||||
#### [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
|
||||||
|
|
||||||
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
|
||||||
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
|
||||||
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
|
||||||
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
|
||||||
- 海外团队,无跑路风险,高达 50% 返佣
|
|
||||||
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
|
|
||||||
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
|
|
||||||
- 解锁流媒体及 ChatGPT
|
|
||||||
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
|
||||||
|
|
||||||
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
|
|
||||||
|
|
||||||
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
|
|
||||||
|
|
||||||
🧩 YXVM 独立服务器优势:
|
|
||||||
|
|
||||||
- 🌎 优质网络,回程优化,下载快到飞起
|
|
||||||
- 🔧 物理机独享资源,非VPS可比,性能拉满
|
|
||||||
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
|
|
||||||
- 💡 支持即开即用,多机房选择,CN2 / IEPL 可选
|
|
||||||
- 📦 本项目使用配置已在售,欢迎同款入手!
|
|
||||||
- 🎯 想要同款构建体验?[立即下单 YXVM 独立服务器!](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
- Based on the powerful Rust and Tauri 2 frameworks.
|
||||||
- 内置[Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo)内核,并支持切换 `Alpha` 版本内核。
|
- Built-in [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) kernel with support for switching between `Alpha` versions of the kernel.
|
||||||
- 简洁美观的用户界面,支持自定义主题颜色、代理组/托盘图标以及 `CSS Injection`。
|
- Simple and beautiful user interface with support for custom theme colors, agent group/tray icons, and `CSS Injection`.
|
||||||
- 配置文件管理和增强(Merge 和 Script),配置文件语法提示。
|
- Configuration file management, configuration file syntax hints.
|
||||||
- 系统代理和守卫、`TUN(虚拟网卡)` 模式。
|
- System Agent and Guard, `TUN (Virtual NIC)` mode.
|
||||||
- 可视化节点和规则编辑
|
- Visual node and rule editing
|
||||||
- WebDav 配置备份和同步
|
- WebDav configuration backup and synchronization
|
||||||
|
|
||||||
### FAQ
|
|
||||||
|
|
||||||
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
|
|
||||||
|
|
||||||
### Donation
|
|
||||||
|
|
||||||
[捐助Clash Verge Rev的开发](https://github.com/sponsors/clash-verge-rev)
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
1756
UPDATELOG.md
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@root/src/components",
|
||||||
|
"utils": "@root/lib/utils",
|
||||||
|
"ui": "@root/src/components/ui",
|
||||||
|
"lib": "@root/lib",
|
||||||
|
"hooks": "@root/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 712 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 671 KiB |
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
68
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "2.3.1",
|
"version": "0.2.0",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||||
@@ -30,53 +30,78 @@
|
|||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"@mui/icons-material": "^7.1.1",
|
"@mui/icons-material": "^7.1.1",
|
||||||
"@mui/lab": "7.0.0-beta.13",
|
"@mui/lab": "7.0.0-beta.13",
|
||||||
"@mui/material": "^7.1.1",
|
"@mui/material": "^7.1.1",
|
||||||
"@mui/x-data-grid": "^8.5.2",
|
"@mui/x-data-grid": "^8.5.1",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.15",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@tauri-apps/api": "2.5.0",
|
"@tauri-apps/api": "2.5.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.3",
|
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
|
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
|
||||||
"@tauri-apps/plugin-notification": "^2.2.3",
|
"@tauri-apps/plugin-notification": "^2.2.2",
|
||||||
"@tauri-apps/plugin-process": "^2.2.2",
|
"@tauri-apps/plugin-process": "^2.2.1",
|
||||||
"@tauri-apps/plugin-shell": "2.2.2",
|
"@tauri-apps/plugin-shell": "2.2.1",
|
||||||
"@tauri-apps/plugin-updater": "2.8.1",
|
"@tauri-apps/plugin-updater": "2.7.1",
|
||||||
"@tauri-apps/plugin-window-state": "^2.2.3",
|
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||||
"@types/d3-shape": "^3.1.7",
|
"@types/d3-shape": "^3.1.7",
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"ahooks": "^3.8.5",
|
"ahooks": "^3.8.5",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.9.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.4.9",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"cli-color": "^2.0.4",
|
"cli-color": "^2.0.4",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"d3-shape": "^3.2.0",
|
"d3-shape": "^3.2.0",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"foxact": "^0.2.49",
|
"foxact": "^0.2.45",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.2",
|
||||||
"i18next": "^25.2.1",
|
"i18next": "^25.2.1",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.7",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"lucide-react": "^0.514.0",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"monaco-yaml": "^5.4.0",
|
"monaco-yaml": "^5.4.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"peggy": "^5.0.3",
|
"peggy": "^5.0.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-error-boundary": "6.0.0",
|
"react-error-boundary": "6.0.0",
|
||||||
"react-hook-form": "^7.58.1",
|
"react-hook-form": "^7.57.0",
|
||||||
"react-i18next": "15.5.3",
|
"react-i18next": "15.5.2",
|
||||||
"react-markdown": "10.1.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-monaco-editor": "0.58.0",
|
"react-monaco-editor": "0.58.0",
|
||||||
"react-router-dom": "7.6.2",
|
"react-router-dom": "7.6.2",
|
||||||
"react-virtuoso": "^4.13.0",
|
"react-virtuoso": "^4.12.8",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
|
"sonner": "^2.0.5",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
|
"zod": "^3.25.67",
|
||||||
"zustand": "^5.0.5"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -85,21 +110,26 @@
|
|||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/react": "19.1.8",
|
"@types/node": "^24.0.0",
|
||||||
|
"@types/react": "19.1.6",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"@vitejs/plugin-legacy": "^6.1.1",
|
"@vitejs/plugin-legacy": "^6.1.1",
|
||||||
"@vitejs/plugin-react": "4.5.2",
|
"@vitejs/plugin-react": "4.5.1",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
"commander": "^14.0.0",
|
"commander": "^14.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"meta-json-schema": "^1.19.10",
|
"meta-json-schema": "^1.19.10",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"postcss": "^8.5.4",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"pretty-quick": "^4.2.2",
|
"pretty-quick": "^4.2.2",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.89.1",
|
||||||
"terser": "^5.43.0",
|
"tailwindcss": "^4.1.11",
|
||||||
|
"terser": "^5.41.0",
|
||||||
|
"tw-animate-css": "^1.3.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
|
|||||||
2067
pnpm-lock.yaml
generated
1990
src-tauri/Cargo.lock
generated
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.3.1"
|
version = "0.2.0"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
repository = "https://github.com/coolcoala/clash-verge-rev-lite.git"
|
||||||
default-run = "clash-verge"
|
default-run = "clash-verge"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
@@ -13,9 +13,12 @@ build = "build.rs"
|
|||||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.2.0", features = [] }
|
tauri-build = { version = "2.3.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
url = "2.5.4"
|
||||||
|
os_info = "3.0"
|
||||||
|
machine-uid = "0.2"
|
||||||
warp = "0.3.7"
|
warp = "0.3.7"
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
@@ -47,7 +50,7 @@ regex = "1.11.1"
|
|||||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||||
image = "0.25.6"
|
image = "0.25.6"
|
||||||
imageproc = "0.25.0"
|
imageproc = "0.25.0"
|
||||||
tauri = { version = "2.5.1", features = [
|
tauri = { version = "2.6.2", features = [
|
||||||
"protocol-asset",
|
"protocol-asset",
|
||||||
"devtools",
|
"devtools",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
@@ -55,15 +58,15 @@ tauri = { version = "2.5.1", features = [
|
|||||||
"image-png",
|
"image-png",
|
||||||
] }
|
] }
|
||||||
network-interface = { version = "2.0.1", features = ["serde"] }
|
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||||
tauri-plugin-shell = "2.2.2"
|
tauri-plugin-shell = "2.3.0"
|
||||||
tauri-plugin-dialog = "2.2.2"
|
tauri-plugin-dialog = "2.3.0"
|
||||||
tauri-plugin-fs = "2.3.0"
|
tauri-plugin-fs = "2.4.0"
|
||||||
tauri-plugin-process = "2.2.2"
|
tauri-plugin-process = "2.3.0"
|
||||||
tauri-plugin-clipboard-manager = "2.2.3"
|
tauri-plugin-clipboard-manager = "2.3.0"
|
||||||
tauri-plugin-deep-link = "2.3.0"
|
tauri-plugin-deep-link = "2.4.0"
|
||||||
tauri-plugin-devtools = "2.0.0"
|
tauri-plugin-devtools = "2.0.0"
|
||||||
tauri-plugin-window-state = "2.2.3"
|
tauri-plugin-window-state = "2.3.0"
|
||||||
zip = "4.1.0"
|
zip = "4.2.0"
|
||||||
reqwest_dav = "0.2.1"
|
reqwest_dav = "0.2.1"
|
||||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
@@ -75,11 +78,13 @@ async-trait = "0.1.88"
|
|||||||
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
||||||
ab_glyph = "0.2.29"
|
ab_glyph = "0.2.29"
|
||||||
tungstenite = "0.27.0"
|
tungstenite = "0.27.0"
|
||||||
libc = "0.2.173"
|
libc = "0.2.174"
|
||||||
gethostname = "1.0.2"
|
gethostname = "1.0.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
scopeguard = "1.2.0"
|
||||||
|
tauri-plugin-notification = "2.3.0"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
@@ -93,15 +98,19 @@ winapi = { version = "0.3.9", features = [
|
|||||||
"errhandlingapi",
|
"errhandlingapi",
|
||||||
"minwindef",
|
"minwindef",
|
||||||
"winerror",
|
"winerror",
|
||||||
|
"tlhelp32",
|
||||||
|
"processthreadsapi",
|
||||||
|
"winhttp",
|
||||||
|
"winreg",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
users = "0.11.0"
|
users = "0.11.0"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-autostart = "2.4.0"
|
tauri-plugin-autostart = "2.5.0"
|
||||||
tauri-plugin-global-shortcut = "2.2.1"
|
tauri-plugin-global-shortcut = "2.3.0"
|
||||||
tauri-plugin-updater = "2.8.1"
|
tauri-plugin-updater = "2.9.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["custom-protocol"]
|
default = ["custom-protocol"]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"autostart:allow-enable",
|
"autostart:allow-enable",
|
||||||
"autostart:allow-disable",
|
"autostart:allow-disable",
|
||||||
"autostart:allow-is-enabled",
|
"autostart:allow-is-enabled",
|
||||||
"core:window:allow-set-theme"
|
"core:window:allow-set-theme",
|
||||||
|
"notification:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
236
src-tauri/deny.toml
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# This template contains all of the possible sections and their default values
|
||||||
|
|
||||||
|
# Note that all fields that take a lint level have these possible values:
|
||||||
|
# * deny - An error will be produced and the check will fail
|
||||||
|
# * warn - A warning will be produced, but the check will not fail
|
||||||
|
# * allow - No warning or error will be produced, though in some cases a note
|
||||||
|
# will be
|
||||||
|
|
||||||
|
# The values provided in this template are the default values that will be used
|
||||||
|
# when any section or field is not specified in your own configuration
|
||||||
|
|
||||||
|
# Root options
|
||||||
|
|
||||||
|
# The graph table configures how the dependency graph is constructed and thus
|
||||||
|
# which crates the checks are performed against
|
||||||
|
[graph]
|
||||||
|
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||||
|
# only the specified targets will be checked when running `cargo deny check`.
|
||||||
|
# This means, if a particular package is only ever used as a target specific
|
||||||
|
# dependency, such as, for example, the `nix` crate only being used via the
|
||||||
|
# `target_family = "unix"` configuration, that only having windows targets in
|
||||||
|
# this list would mean the nix crate, as well as any of its exclusive
|
||||||
|
# dependencies not shared by any other crates, would be ignored, as the target
|
||||||
|
# list here is effectively saying which targets you are building for.
|
||||||
|
targets = [
|
||||||
|
# The triple can be any string, but only the target triples built in to
|
||||||
|
# rustc (as of 1.40) can be checked against actual config expressions
|
||||||
|
#"x86_64-unknown-linux-musl",
|
||||||
|
# You can also specify which target_features you promise are enabled for a
|
||||||
|
# particular target. target_features are currently not validated against
|
||||||
|
# the actual valid features supported by the target architecture.
|
||||||
|
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||||
|
]
|
||||||
|
# When creating the dependency graph used as the source of truth when checks are
|
||||||
|
# executed, this field can be used to prune crates from the graph, removing them
|
||||||
|
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||||
|
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||||
|
# they are connected to another crate in the graph that hasn't been pruned,
|
||||||
|
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||||
|
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||||
|
#exclude = []
|
||||||
|
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||||
|
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||||
|
# is recommended to pass `--all-features` on the cmd line instead
|
||||||
|
all-features = false
|
||||||
|
# If true, metadata will be collected with `--no-default-features`. The same
|
||||||
|
# caveat with `all-features` applies
|
||||||
|
no-default-features = false
|
||||||
|
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||||
|
# is specified on the cmd line they will take precedence over this option.
|
||||||
|
#features = []
|
||||||
|
|
||||||
|
# The output table provides options for how/if diagnostics are outputted
|
||||||
|
[output]
|
||||||
|
# When outputting inclusion graphs in diagnostics that include features, this
|
||||||
|
# option can be used to specify the depth at which feature edges will be added.
|
||||||
|
# This option is included since the graphs can be quite large and the addition
|
||||||
|
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||||
|
# This option can be overridden via `--feature-depth` on the cmd line
|
||||||
|
feature-depth = 1
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check advisories`
|
||||||
|
# More documentation for the advisories section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||||
|
[advisories]
|
||||||
|
# The path where the advisory databases are cloned/fetched into
|
||||||
|
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||||
|
# The url(s) of the advisory databases to use
|
||||||
|
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||||
|
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||||
|
# output a note when they are encountered.
|
||||||
|
ignore = [
|
||||||
|
#"RUSTSEC-0000-0000",
|
||||||
|
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||||
|
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||||
|
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||||
|
"RUSTSEC-2024-0415",
|
||||||
|
]
|
||||||
|
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||||
|
# If this is false, then it uses a built-in git library.
|
||||||
|
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||||
|
# See Git Authentication for more information about setting up git authentication.
|
||||||
|
#git-fetch-with-cli = true
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check licenses`
|
||||||
|
# More documentation for the licenses section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||||
|
[licenses]
|
||||||
|
# List of explicitly allowed licenses
|
||||||
|
# See https://spdx.org/licenses/ for list of possible licenses
|
||||||
|
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||||
|
allow = [
|
||||||
|
#"MIT",
|
||||||
|
#"Apache-2.0",
|
||||||
|
#"Apache-2.0 WITH LLVM-exception",
|
||||||
|
]
|
||||||
|
# The confidence threshold for detecting a license from license text.
|
||||||
|
# The higher the value, the more closely the license text must be to the
|
||||||
|
# canonical license text of a valid SPDX license file.
|
||||||
|
# [possible values: any between 0.0 and 1.0].
|
||||||
|
confidence-threshold = 0.85
|
||||||
|
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||||
|
# aren't accepted for every possible crate as with the normal allow list
|
||||||
|
exceptions = [
|
||||||
|
# Each entry is the crate and version constraint, and its specific allow
|
||||||
|
# list
|
||||||
|
#{ allow = ["Zlib"], crate = "adler32" },
|
||||||
|
]
|
||||||
|
|
||||||
|
# Some crates don't have (easily) machine readable licensing information,
|
||||||
|
# adding a clarification entry for it allows you to manually specify the
|
||||||
|
# licensing information
|
||||||
|
#[[licenses.clarify]]
|
||||||
|
# The package spec the clarification applies to
|
||||||
|
#crate = "ring"
|
||||||
|
# The SPDX expression for the license requirements of the crate
|
||||||
|
#expression = "MIT AND ISC AND OpenSSL"
|
||||||
|
# One or more files in the crate's source used as the "source of truth" for
|
||||||
|
# the license expression. If the contents match, the clarification will be used
|
||||||
|
# when running the license check, otherwise the clarification will be ignored
|
||||||
|
# and the crate will be checked normally, which may produce warnings or errors
|
||||||
|
# depending on the rest of your configuration
|
||||||
|
#license-files = [
|
||||||
|
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||||
|
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||||
|
#]
|
||||||
|
|
||||||
|
[licenses.private]
|
||||||
|
# If true, ignores workspace crates that aren't published, or are only
|
||||||
|
# published to private registries.
|
||||||
|
# To see how to mark a crate as unpublished (to the official registry),
|
||||||
|
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||||
|
ignore = false
|
||||||
|
# One or more private registries that you might publish crates to, if a crate
|
||||||
|
# is only published to private registries, and ignore is true, the crate will
|
||||||
|
# not have its license(s) checked
|
||||||
|
registries = [
|
||||||
|
#"https://sekretz.com/registry
|
||||||
|
]
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check bans`.
|
||||||
|
# More documentation about the 'bans' section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||||
|
[bans]
|
||||||
|
# Lint level for when multiple versions of the same crate are detected
|
||||||
|
multiple-versions = "warn"
|
||||||
|
# Lint level for when a crate version requirement is `*`
|
||||||
|
wildcards = "allow"
|
||||||
|
# The graph highlighting used when creating dotgraphs for crates
|
||||||
|
# with multiple versions
|
||||||
|
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||||
|
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||||
|
# * all - Both lowest-version and simplest-path are used
|
||||||
|
highlight = "all"
|
||||||
|
# The default lint level for `default` features for crates that are members of
|
||||||
|
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||||
|
# `default` on a crate-by-crate basis if desired.
|
||||||
|
workspace-default-features = "allow"
|
||||||
|
# The default lint level for `default` features for external crates that are not
|
||||||
|
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||||
|
# on a crate-by-crate basis if desired.
|
||||||
|
external-default-features = "allow"
|
||||||
|
# List of crates that are allowed. Use with care!
|
||||||
|
allow = [
|
||||||
|
#"ansi_term@0.11.0",
|
||||||
|
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||||
|
]
|
||||||
|
# List of crates to deny
|
||||||
|
deny = [
|
||||||
|
#"ansi_term@0.11.0",
|
||||||
|
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||||
|
# Wrapper crates can optionally be specified to allow the crate when it
|
||||||
|
# is a direct dependency of the otherwise banned crate
|
||||||
|
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||||
|
]
|
||||||
|
|
||||||
|
# List of features to allow/deny
|
||||||
|
# Each entry the name of a crate and a version range. If version is
|
||||||
|
# not specified, all versions will be matched.
|
||||||
|
#[[bans.features]]
|
||||||
|
#crate = "reqwest"
|
||||||
|
# Features to not allow
|
||||||
|
#deny = ["json"]
|
||||||
|
# Features to allow
|
||||||
|
#allow = [
|
||||||
|
# "rustls",
|
||||||
|
# "__rustls",
|
||||||
|
# "__tls",
|
||||||
|
# "hyper-rustls",
|
||||||
|
# "rustls",
|
||||||
|
# "rustls-pemfile",
|
||||||
|
# "rustls-tls-webpki-roots",
|
||||||
|
# "tokio-rustls",
|
||||||
|
# "webpki-roots",
|
||||||
|
#]
|
||||||
|
# If true, the allowed features must exactly match the enabled feature set. If
|
||||||
|
# this is set there is no point setting `deny`
|
||||||
|
#exact = true
|
||||||
|
|
||||||
|
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||||
|
skip = [
|
||||||
|
#"ansi_term@0.11.0",
|
||||||
|
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||||
|
]
|
||||||
|
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||||
|
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||||
|
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||||
|
# by default infinite.
|
||||||
|
skip-tree = [
|
||||||
|
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||||
|
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||||
|
]
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check sources`.
|
||||||
|
# More documentation about the 'sources' section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||||
|
[sources]
|
||||||
|
# Lint level for what to happen when a crate from a crate registry that is not
|
||||||
|
# in the allow list is encountered
|
||||||
|
unknown-registry = "warn"
|
||||||
|
# Lint level for what to happen when a crate from a git repository that is not
|
||||||
|
# in the allow list is encountered
|
||||||
|
unknown-git = "warn"
|
||||||
|
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||||
|
# if not specified. If it is specified but empty, no registries are allowed.
|
||||||
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||||
|
# List of URLs for allowed Git repositories
|
||||||
|
allow-git = []
|
||||||
|
|
||||||
|
[sources.allow-org]
|
||||||
|
# github.com organizations to allow git sources for
|
||||||
|
github = []
|
||||||
|
# gitlab.com organizations to allow git sources for
|
||||||
|
gitlab = []
|
||||||
|
# bitbucket.org organizations to allow git sources for
|
||||||
|
bitbucket = []
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/tray-icon-sys-mono-new.ico
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
src-tauri/icons/tray-icon-tun-mono-new.ico
Normal file
|
After Width: | Height: | Size: 107 KiB |
@@ -147,7 +147,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
|||||||
Ok(icon_path.to_string_lossy().to_string())
|
Ok(icon_path.to_string_lossy().to_string())
|
||||||
} else {
|
} else {
|
||||||
let _ = std::fs::remove_file(&temp_path);
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
Err(format!("下载的内容不是有效图片: {}", url))
|
Err(format!("下载的内容不是有效图片: {url}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
|
|||||||
/// UI加载阶段
|
/// UI加载阶段
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||||
log::info!(target: "app", "UI加载阶段更新: {}", stage);
|
log::info!(target: "app", "UI加载阶段更新: {stage}");
|
||||||
|
|
||||||
use crate::utils::resolve::UiReadyStage;
|
use crate::utils::resolve::UiReadyStage;
|
||||||
|
|
||||||
@@ -228,8 +228,8 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
|||||||
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
||||||
"Ready" => UiReadyStage::Ready,
|
"Ready" => UiReadyStage::Ready,
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!(target: "app", "未知的UI加载阶段: {}", stage);
|
log::warn!(target: "app", "未知的UI加载阶段: {stage}");
|
||||||
return Err(format!("未知的UI加载阶段: {}", stage));
|
return Err(format!("未知的UI加载阶段: {stage}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error_msg = format!("Core changed but failed to restart: {}", err);
|
let error_msg = format!("Core changed but failed to restart: {err}");
|
||||||
log::error!(target: "app", "{}", error_msg);
|
log::error!(target: "app", "{error_msg}");
|
||||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||||
Ok(Some(error_msg))
|
Ok(Some(error_msg))
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
|||||||
// 保存DNS配置到文件
|
// 保存DNS配置到文件
|
||||||
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
||||||
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
|
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
|
||||||
log::info!(target: "app", "DNS config saved to {:?}", dns_path);
|
log::info!(target: "app", "DNS config saved to {dns_path:?}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
let dns_path = match dirs::app_home_dir() {
|
let dns_path = match dirs::app_home_dir() {
|
||||||
Ok(path) => path.join("dns_config.yaml"),
|
Ok(path) => path.join("dns_config.yaml"),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to get home dir: {}", e);
|
log::error!(target: "app", "Failed to get home dir: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -150,7 +150,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to read DNS config: {}", e);
|
log::error!(target: "app", "Failed to read DNS config: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -163,7 +163,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
patch
|
patch
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to parse DNS config: {}", e);
|
log::error!(target: "app", "Failed to parse DNS config: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -178,13 +178,13 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
|
|
||||||
// 首先重新生成配置
|
// 首先重新生成配置
|
||||||
if let Err(err) = Config::generate().await {
|
if let Err(err) = Config::generate().await {
|
||||||
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
|
log::error!(target: "app", "Failed to regenerate config with DNS: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 然后应用新配置
|
// 然后应用新配置
|
||||||
if let Err(err) = CoreManager::global().update_config().await {
|
if let Err(err) = CoreManager::global().update_config().await {
|
||||||
log::error!(target: "app", "Failed to apply config with DNS: {}", err);
|
log::error!(target: "app", "Failed to apply config with DNS: {err}");
|
||||||
} else {
|
} else {
|
||||||
log::info!(target: "app", "DNS config successfully applied");
|
log::info!(target: "app", "DNS config successfully applied");
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
@@ -196,7 +196,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
|
|
||||||
// 重新生成配置
|
// 重新生成配置
|
||||||
if let Err(err) = Config::generate().await {
|
if let Err(err) = Config::generate().await {
|
||||||
log::error!(target: "app", "Failed to regenerate config: {}", err);
|
log::error!(target: "app", "Failed to regenerate config: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
|
log::error!(target: "app", "Failed to apply regenerated config: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ fn country_code_to_emoji(country_code: &str) -> String {
|
|||||||
let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
|
let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
|
||||||
|
|
||||||
char::from_u32(c1)
|
char::from_u32(c1)
|
||||||
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{}{}", c1, c2)))
|
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}")))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
|||||||
|
|
||||||
map.get("loc").map(|loc| {
|
map.get("loc").map(|loc| {
|
||||||
let emoji = country_code_to_emoji(loc);
|
let emoji = country_code_to_emoji(loc);
|
||||||
format!("{}{}", emoji, loc)
|
format!("{emoji}{loc}")
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -255,7 +255,7 @@ async fn check_gemini(client: &Client) -> UnlockItem {
|
|||||||
caps.get(1).map(|m| {
|
caps.get(1).map(|m| {
|
||||||
let country_code = m.as_str();
|
let country_code = m.as_str();
|
||||||
let emoji = country_code_to_emoji(country_code);
|
let emoji = country_code_to_emoji(country_code);
|
||||||
format!("{}{}", emoji, country_code)
|
format!("{emoji}{country_code}")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
|||||||
caps.get(1).map(|m| {
|
caps.get(1).map(|m| {
|
||||||
let country_code = m.as_str().trim();
|
let country_code = m.as_str().trim();
|
||||||
let emoji = country_code_to_emoji(country_code);
|
let emoji = country_code_to_emoji(country_code);
|
||||||
format!("{}{}", emoji, country_code)
|
format!("{emoji}{country_code}")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -384,10 +384,8 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 第二步:使用设备ID检查访问权限 (使用相同的Cookie)
|
// 第二步:使用设备ID检查访问权限 (使用相同的Cookie)
|
||||||
let url = format!(
|
let url =
|
||||||
"https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={}",
|
format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
|
||||||
device_id
|
|
||||||
);
|
|
||||||
|
|
||||||
let token_result = match client_with_cookies.get(&url).send().await {
|
let token_result = match client_with_cookies.get(&url).send().await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
@@ -431,7 +429,7 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
|||||||
.map(|m| {
|
.map(|m| {
|
||||||
let country_code = m.as_str();
|
let country_code = m.as_str();
|
||||||
let emoji = country_code_to_emoji(country_code);
|
let emoji = country_code_to_emoji(country_code);
|
||||||
format!("{}{}", emoji, country_code)
|
format!("{emoji}{country_code}")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -470,7 +468,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
|
|
||||||
// 检查连接失败情况
|
// 检查连接失败情况
|
||||||
if let Err(e) = &result1 {
|
if let Err(e) = &result1 {
|
||||||
eprintln!("Netflix请求错误: {}", e);
|
eprintln!("Netflix请求错误: {e}");
|
||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
status: "Failed".to_string(),
|
status: "Failed".to_string(),
|
||||||
@@ -487,7 +485,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Err(e) = &result2 {
|
if let Err(e) = &result2 {
|
||||||
eprintln!("Netflix请求错误: {}", e);
|
eprintln!("Netflix请求错误: {e}");
|
||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
status: "Failed".to_string(),
|
status: "Failed".to_string(),
|
||||||
@@ -541,7 +539,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
status: "Yes".to_string(),
|
status: "Yes".to_string(),
|
||||||
region: Some(format!("{}{}", emoji, region_code)),
|
region: Some(format!("{emoji}{region_code}")),
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -557,7 +555,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("获取Netflix区域信息失败: {}", e);
|
eprintln!("获取Netflix区域信息失败: {e}");
|
||||||
UnlockItem {
|
UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
status: "Yes (但无法获取区域)".to_string(),
|
status: "Yes (但无法获取区域)".to_string(),
|
||||||
@@ -570,7 +568,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
// 其他未知错误状态
|
// 其他未知错误状态
|
||||||
UnlockItem {
|
UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
status: format!("Failed (状态码: {}_{}", status1, status2),
|
status: format!("Failed (状态码: {status1}_{status2}"),
|
||||||
region: None,
|
region: None,
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
}
|
}
|
||||||
@@ -614,7 +612,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
|||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
status: "Yes".to_string(),
|
status: "Yes".to_string(),
|
||||||
region: Some(format!("{}{}", emoji, country)),
|
region: Some(format!("{emoji}{country}")),
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -631,7 +629,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("解析Fast.com API响应失败: {}", e);
|
eprintln!("解析Fast.com API响应失败: {e}");
|
||||||
UnlockItem {
|
UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
status: "Failed (解析错误)".to_string(),
|
status: "Failed (解析错误)".to_string(),
|
||||||
@@ -642,7 +640,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Fast.com API请求失败: {}", e);
|
eprintln!("Fast.com API请求失败: {e}");
|
||||||
UnlockItem {
|
UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
status: "Failed (CDN API)".to_string(),
|
status: "Failed (CDN API)".to_string(),
|
||||||
@@ -884,7 +882,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Disney+".to_string(),
|
name: "Disney+".to_string(),
|
||||||
status: "Yes".to_string(),
|
status: "Yes".to_string(),
|
||||||
region: Some(format!("{}{} (from main page)", emoji, region)),
|
region: Some(format!("{emoji}{region} (from main page)")),
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -947,7 +945,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Disney+".to_string(),
|
name: "Disney+".to_string(),
|
||||||
status: "Yes".to_string(),
|
status: "Yes".to_string(),
|
||||||
region: Some(format!("{}{} (from main page)", emoji, region)),
|
region: Some(format!("{emoji}{region} (from main page)")),
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -968,7 +966,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Disney+".to_string(),
|
name: "Disney+".to_string(),
|
||||||
status: "Yes".to_string(),
|
status: "Yes".to_string(),
|
||||||
region: Some(format!("{}{}", emoji, region)),
|
region: Some(format!("{emoji}{region}")),
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -990,7 +988,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
UnlockItem {
|
UnlockItem {
|
||||||
name: "Disney+".to_string(),
|
name: "Disney+".to_string(),
|
||||||
status: "Soon".to_string(),
|
status: "Soon".to_string(),
|
||||||
region: Some(format!("{}{}(即将上线)", emoji, region)),
|
region: Some(format!("{emoji}{region}(即将上线)")),
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -999,13 +997,13 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
UnlockItem {
|
UnlockItem {
|
||||||
name: "Disney+".to_string(),
|
name: "Disney+".to_string(),
|
||||||
status: "Yes".to_string(),
|
status: "Yes".to_string(),
|
||||||
region: Some(format!("{}{}", emoji, region)),
|
region: Some(format!("{emoji}{region}")),
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => UnlockItem {
|
None => UnlockItem {
|
||||||
name: "Disney+".to_string(),
|
name: "Disney+".to_string(),
|
||||||
status: format!("Failed (Error: Unknown region status for {})", region),
|
status: format!("Failed (Error: Unknown region status for {region})"),
|
||||||
region: None,
|
region: None,
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
},
|
},
|
||||||
@@ -1056,7 +1054,7 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
|
|||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Prime Video".to_string(),
|
name: "Prime Video".to_string(),
|
||||||
status: "Yes".to_string(),
|
status: "Yes".to_string(),
|
||||||
region: Some(format!("{}{}", emoji, region)),
|
region: Some(format!("{emoji}{region}")),
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1170,7 +1168,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
.connection_verbose(true) // 详细连接信息
|
.connection_verbose(true) // 详细连接信息
|
||||||
.build() {
|
.build() {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(e) => return Err(format!("创建HTTP客户端失败: {}", e)),
|
Err(e) => return Err(format!("创建HTTP客户端失败: {e}")),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建共享的结果集
|
// 创建共享的结果集
|
||||||
@@ -1284,7 +1282,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
// 等待所有任务完成
|
// 等待所有任务完成
|
||||||
while let Some(res) = tasks.join_next().await {
|
while let Some(res) = tasks.join_next().await {
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
eprintln!("任务执行失败: {}", e);
|
eprintln!("任务执行失败: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
|
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
|
||||||
use crate::wrap_err;
|
use crate::wrap_err;
|
||||||
use network_interface::NetworkInterface;
|
use network_interface::NetworkInterface;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use sysproxy::{Autoproxy, Sysproxy};
|
|
||||||
use tokio::task::spawn_blocking;
|
|
||||||
|
|
||||||
/// get the system proxy
|
/// get the system proxy
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||||
let current = spawn_blocking(Sysproxy::get_system_proxy)
|
log::debug!(target: "app", "异步获取系统代理配置");
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to spawn blocking task for sysproxy: {}", e))?
|
let current = AsyncProxyQuery::get_system_proxy().await;
|
||||||
.map_err(|e| format!("Failed to get system proxy: {}", e))?;
|
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
let mut map = Mapping::new();
|
||||||
map.insert("enable".into(), current.enable.into());
|
map.insert("enable".into(), current.enable.into());
|
||||||
@@ -21,21 +19,28 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
|||||||
);
|
);
|
||||||
map.insert("bypass".into(), current.bypass.into());
|
map.insert("bypass".into(), current.bypass.into());
|
||||||
|
|
||||||
|
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the system proxy
|
/// 获取自动代理配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||||
let current = spawn_blocking(Autoproxy::get_auto_proxy)
|
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to spawn blocking task for autoproxy: {}", e))?
|
let proxy_manager = EventDrivenProxyManager::global();
|
||||||
.map_err(|e| format!("Failed to get auto proxy: {}", e))?;
|
|
||||||
|
let current = proxy_manager.get_auto_proxy_cached();
|
||||||
|
// 异步请求更新,立即返回缓存数据
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = proxy_manager.get_auto_proxy_async().await;
|
||||||
|
});
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
let mut map = Mapping::new();
|
||||||
map.insert("enable".into(), current.enable.into());
|
map.insert("enable".into(), current.enable.into());
|
||||||
map.insert("url".into(), current.url.into());
|
map.insert("url".into(), current.url.clone().into());
|
||||||
|
|
||||||
|
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +54,7 @@ pub fn get_system_hostname() -> CmdResult<String> {
|
|||||||
Ok(name) => name,
|
Ok(name) => name,
|
||||||
Err(os_string) => {
|
Err(os_string) => {
|
||||||
// 对于包含非UTF-8的主机名,使用调试格式化
|
// 对于包含非UTF-8的主机名,使用调试格式化
|
||||||
let fallback = format!("{:?}", os_string);
|
let fallback = format!("{os_string:?}");
|
||||||
// 去掉可能存在的引号
|
// 去掉可能存在的引号
|
||||||
fallback.trim_matches('"').to_string()
|
fallback.trim_matches('"').to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,31 @@ use crate::{
|
|||||||
utils::{dirs, help, logging::Type},
|
utils::{dirs, help, logging::Type},
|
||||||
wrap_err,
|
wrap_err,
|
||||||
};
|
};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::{Mutex, RwLock};
|
||||||
|
|
||||||
// 添加全局互斥锁防止并发配置更新
|
// 全局互斥锁防止并发配置更新
|
||||||
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
||||||
|
|
||||||
|
// 全局请求序列号跟踪,用于避免队列化执行
|
||||||
|
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
static CURRENT_PROCESSING_PROFILE: RwLock<Option<String>> = RwLock::const_new(None);
|
||||||
|
|
||||||
|
/// 清理配置处理状态
|
||||||
|
async fn cleanup_processing_state(sequence: u64, reason: &str) {
|
||||||
|
*CURRENT_PROCESSING_PROFILE.write().await = None;
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"{},清理状态,序列号: {}",
|
||||||
|
reason,
|
||||||
|
sequence
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取配置文件避免锁竞争
|
/// 获取配置文件避免锁竞争
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
@@ -151,10 +170,60 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
|||||||
/// 修改profiles的配置
|
/// 修改profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||||
// 获取互斥锁,防止并发执行
|
// 为当前请求分配序列号
|
||||||
let _guard = PROFILE_UPDATE_MUTEX.lock().await;
|
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
let target_profile = profiles.current.clone();
|
||||||
|
|
||||||
logging!(info, Type::Cmd, true, "开始修改配置文件");
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
|
||||||
|
current_sequence,
|
||||||
|
target_profile
|
||||||
|
);
|
||||||
|
|
||||||
|
let mutex_result =
|
||||||
|
tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await;
|
||||||
|
|
||||||
|
let _guard = match mutex_result {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(_) => {
|
||||||
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
|
if current_sequence < latest_sequence {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"检测到更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||||
|
current_sequence,
|
||||||
|
latest_sequence
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"强制获取锁以处理最新请求: {}",
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
|
PROFILE_UPDATE_MUTEX.lock().await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
|
if current_sequence < latest_sequence {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||||
|
current_sequence,
|
||||||
|
latest_sequence
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
// 保存当前配置,以便在验证失败时恢复
|
// 保存当前配置,以便在验证失败时恢复
|
||||||
let current_profile = Config::profiles().latest().current.clone();
|
let current_profile = Config::profiles().latest().current.clone();
|
||||||
@@ -221,7 +290,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
||||||
}
|
}
|
||||||
Ok(Err(err)) => {
|
Ok(Err(err)) => {
|
||||||
let error_msg = format!(" {}", err);
|
let error_msg = format!(" {err}");
|
||||||
logging!(
|
logging!(
|
||||||
error,
|
error,
|
||||||
Type::Cmd,
|
Type::Cmd,
|
||||||
@@ -236,7 +305,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
Err(join_err) => {
|
Err(join_err) => {
|
||||||
let error_msg = format!("YAML解析任务失败: {}", join_err);
|
let error_msg = format!("YAML解析任务失败: {join_err}");
|
||||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||||
handle::Handle::notice_message(
|
handle::Handle::notice_message(
|
||||||
"config_validate::yaml_parse_error",
|
"config_validate::yaml_parse_error",
|
||||||
@@ -247,7 +316,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Err(err)) => {
|
Ok(Err(err)) => {
|
||||||
let error_msg = format!("无法读取目标配置文件: {}", err);
|
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||||
handle::Handle::notice_message(
|
handle::Handle::notice_message(
|
||||||
"config_validate::file_read_error",
|
"config_validate::file_read_error",
|
||||||
@@ -269,14 +338,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查请求有效性
|
||||||
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
|
if current_sequence < latest_sequence {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||||
|
current_sequence,
|
||||||
|
latest_sequence
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref profile) = target_profile {
|
||||||
|
*CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone());
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"设置当前处理profile: {}, 序列号: {}",
|
||||||
|
profile,
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 更新profiles配置
|
// 更新profiles配置
|
||||||
logging!(info, Type::Cmd, true, "正在更新配置草稿");
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"正在更新配置草稿,序列号: {}",
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
|
|
||||||
let current_value = profiles.current.clone();
|
let current_value = profiles.current.clone();
|
||||||
|
|
||||||
let _ = Config::profiles().draft().patch_config(profiles);
|
let _ = Config::profiles().draft().patch_config(profiles);
|
||||||
|
|
||||||
|
// 在调用内核前再次验证请求有效性
|
||||||
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
|
if current_sequence < latest_sequence {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||||
|
current_sequence,
|
||||||
|
latest_sequence
|
||||||
|
);
|
||||||
|
Config::profiles().discard();
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
// 为配置更新添加超时保护
|
// 为配置更新添加超时保护
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"开始内核配置更新,序列号: {}",
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
let update_result = tokio::time::timeout(
|
let update_result = tokio::time::timeout(
|
||||||
Duration::from_secs(30), // 30秒超时
|
Duration::from_secs(30), // 30秒超时
|
||||||
CoreManager::global().update_config(),
|
CoreManager::global().update_config(),
|
||||||
@@ -286,38 +409,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
// 更新配置并进行验证
|
// 更新配置并进行验证
|
||||||
match update_result {
|
match update_result {
|
||||||
Ok(Ok((true, _))) => {
|
Ok(Ok((true, _))) => {
|
||||||
logging!(info, Type::Cmd, true, "配置更新成功");
|
// 内核操作完成后再次检查请求有效性
|
||||||
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
|
if current_sequence < latest_sequence {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
|
||||||
|
current_sequence,
|
||||||
|
latest_sequence
|
||||||
|
);
|
||||||
|
Config::profiles().discard();
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"配置更新成功,序列号: {}",
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
|
|
||||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
if let Err(e) = super::proxy::force_refresh_proxies().await {
|
if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||||
log::warn!(target: "app", "强制刷新代理缓存失败: {}", e);
|
log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
if let Err(e) = Tray::global().update_tooltip() {
|
if let Err(e) = Tray::global().update_tooltip() {
|
||||||
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
|
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = Tray::global().update_menu() {
|
if let Err(e) = Tray::global().update_menu() {
|
||||||
log::warn!(target: "app", "异步更新托盘菜单失败: {}", e);
|
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存配置文件
|
// 保存配置文件
|
||||||
if let Err(e) = Config::profiles().data().save_file() {
|
if let Err(e) = Config::profiles().data().save_file() {
|
||||||
log::warn!(target: "app", "异步保存配置文件失败: {}", e);
|
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 立即通知前端配置变更
|
// 立即通知前端配置变更
|
||||||
if let Some(current) = ¤t_value {
|
if let Some(current) = ¤t_value {
|
||||||
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"向前端发送配置变更事件: {}, 序列号: {}",
|
||||||
|
current,
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
handle::Handle::notify_profile_changed(current.clone());
|
handle::Handle::notify_profile_changed(current.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup_processing_state(current_sequence, "配置切换完成").await;
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok(Ok((false, error_msg))) => {
|
Ok(Ok((false, error_msg))) => {
|
||||||
@@ -342,7 +495,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
|
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
if let Err(e) = Config::profiles().data().save_file() {
|
if let Err(e) = Config::profiles().data().save_file() {
|
||||||
log::warn!(target: "app", "异步保存恢复配置文件失败: {}", e);
|
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -351,18 +504,38 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
|
|
||||||
// 发送验证错误通知
|
// 发送验证错误通知
|
||||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
|
|
||||||
|
cleanup_processing_state(current_sequence, "配置验证失败").await;
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"更新过程发生错误: {}, 序列号: {}",
|
||||||
|
e,
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
Config::profiles().discard();
|
Config::profiles().discard();
|
||||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||||
|
|
||||||
|
cleanup_processing_state(current_sequence, "更新过程错误").await;
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// 超时处理
|
// 超时处理
|
||||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||||
logging!(error, Type::Cmd, true, "{}", timeout_msg);
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"{}, 序列号: {}",
|
||||||
|
timeout_msg,
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
Config::profiles().discard();
|
Config::profiles().discard();
|
||||||
|
|
||||||
if let Some(prev_profile) = current_profile {
|
if let Some(prev_profile) = current_profile {
|
||||||
@@ -370,8 +543,9 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
info,
|
info,
|
||||||
Type::Cmd,
|
Type::Cmd,
|
||||||
true,
|
true,
|
||||||
"超时后尝试恢复到之前的配置: {}",
|
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
|
||||||
prev_profile
|
prev_profile,
|
||||||
|
current_sequence
|
||||||
);
|
);
|
||||||
let restore_profiles = IProfiles {
|
let restore_profiles = IProfiles {
|
||||||
current: Some(prev_profile),
|
current: Some(prev_profile),
|
||||||
@@ -382,6 +556,9 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||||
|
|
||||||
|
cleanup_processing_state(current_sequence, "配置更新超时").await;
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||||
let sysinfo = PlatformSpecification::new_async().await;
|
let sysinfo = PlatformSpecification::new_async().await;
|
||||||
let info = format!("{:?}", sysinfo);
|
let info = format!("{sysinfo:?}");
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let cliboard = app_handle.clipboard();
|
let cliboard = app_handle.clipboard();
|
||||||
@@ -37,7 +37,7 @@ pub async fn export_diagnostic_info() -> CmdResult<()> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_system_info() -> CmdResult<String> {
|
pub async fn get_system_info() -> CmdResult<String> {
|
||||||
let sysinfo = PlatformSpecification::new_async().await;
|
let sysinfo = PlatformSpecification::new_async().await;
|
||||||
let info = format!("{:?}", sysinfo);
|
let info = format!("{sysinfo:?}");
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|||||||
// Encrypt data
|
// Encrypt data
|
||||||
let ciphertext = cipher
|
let ciphertext = cipher
|
||||||
.encrypt(nonce.as_slice().into(), data.as_bytes())
|
.encrypt(nonce.as_slice().into(), data.as_bytes())
|
||||||
.map_err(|e| format!("Encryption failed: {}", e))?;
|
.map_err(|e| format!("Encryption failed: {e}"))?;
|
||||||
|
|
||||||
// Concatenate nonce and ciphertext and encode them in base64
|
// Concatenate nonce and ciphertext and encode them in base64
|
||||||
let mut combined = nonce;
|
let mut combined = nonce;
|
||||||
@@ -46,7 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
|
|||||||
// Decrypt data
|
// Decrypt data
|
||||||
let plaintext = cipher
|
let plaintext = cipher
|
||||||
.decrypt(nonce.into(), ciphertext)
|
.decrypt(nonce.into(), ciphertext)
|
||||||
.map_err(|e| format!("Decryption failed: {}", e))?;
|
.map_err(|e| format!("Decryption failed: {e}"))?;
|
||||||
|
|
||||||
String::from_utf8(plaintext).map_err(|e| e.into())
|
String::from_utf8(plaintext).map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use reqwest::StatusCode;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::{fs, time::Duration};
|
use std::{fs, time::Duration};
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use super::Config;
|
use super::Config;
|
||||||
|
|
||||||
@@ -53,6 +55,14 @@ pub struct PrfItem {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub home: Option<String>,
|
pub home: Option<String>,
|
||||||
|
|
||||||
|
/// profile support url
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub support_url: Option<String>,
|
||||||
|
|
||||||
|
/// profile announce
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub announce: Option<String>,
|
||||||
|
|
||||||
/// the file data
|
/// the file data
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub file_data: Option<String>,
|
pub file_data: Option<String>,
|
||||||
@@ -113,6 +123,9 @@ pub struct PrfOption {
|
|||||||
pub proxies: Option<String>,
|
pub proxies: Option<String>,
|
||||||
|
|
||||||
pub groups: Option<String>,
|
pub groups: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub use_hwid: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrfOption {
|
impl PrfOption {
|
||||||
@@ -132,6 +145,7 @@ impl PrfOption {
|
|||||||
a.proxies = b.proxies.or(a.proxies);
|
a.proxies = b.proxies.or(a.proxies);
|
||||||
a.groups = b.groups.or(a.groups);
|
a.groups = b.groups.or(a.groups);
|
||||||
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
|
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
|
||||||
|
a.use_hwid = b.use_hwid.or(a.use_hwid);
|
||||||
Some(a)
|
Some(a)
|
||||||
}
|
}
|
||||||
t => t.0.or(t.1),
|
t => t.0.or(t.1),
|
||||||
@@ -230,6 +244,8 @@ impl PrfItem {
|
|||||||
..PrfOption::default()
|
..PrfOption::default()
|
||||||
}),
|
}),
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||||
})
|
})
|
||||||
@@ -251,6 +267,7 @@ impl PrfItem {
|
|||||||
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||||
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
|
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
|
||||||
|
let use_hwid = opt_ref.is_some_and(|o| o.use_hwid.unwrap_or(true));
|
||||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||||
let mut script = opt_ref.and_then(|o| o.script.clone());
|
let mut script = opt_ref.and_then(|o| o.script.clone());
|
||||||
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
||||||
@@ -274,6 +291,7 @@ impl PrfItem {
|
|||||||
Some(timeout),
|
Some(timeout),
|
||||||
user_agent.clone(),
|
user_agent.clone(),
|
||||||
accept_invalid_certs,
|
accept_invalid_certs,
|
||||||
|
use_hwid,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -291,6 +309,21 @@ impl PrfItem {
|
|||||||
|
|
||||||
let header = resp.headers();
|
let header = resp.headers();
|
||||||
|
|
||||||
|
let mut final_url = url.to_string();
|
||||||
|
|
||||||
|
if let Some(new_domain_value) = header.get("new-sub-domain") {
|
||||||
|
if let Ok(new_domain) = new_domain_value.to_str() {
|
||||||
|
if !new_domain.is_empty() {
|
||||||
|
if let Ok(mut parsed_url) = Url::parse(url) {
|
||||||
|
if parsed_url.set_host(Some(new_domain)).is_ok() {
|
||||||
|
final_url = parsed_url.to_string();
|
||||||
|
log::info!(target: "app", "URL host updated to -> {}", final_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parse the Subscription UserInfo
|
// parse the Subscription UserInfo
|
||||||
let extra = match header.get("Subscription-Userinfo") {
|
let extra = match header.get("Subscription-Userinfo") {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
@@ -348,9 +381,45 @@ impl PrfItem {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let support_url = match header.get("support-url") {
|
||||||
|
Some(value) => {
|
||||||
|
let str_value = value.to_str().unwrap_or("");
|
||||||
|
Some(str_value.to_string())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let announce = match header.get("announce") {
|
||||||
|
Some(value) => {
|
||||||
|
let str_value = value.to_str().unwrap_or("");
|
||||||
|
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
||||||
|
STANDARD.decode(b64_data)
|
||||||
|
.ok()
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
} else {
|
||||||
|
Some(str_value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let profile_title = match header.get("profile-title") {
|
||||||
|
Some(value) => {
|
||||||
|
let str_value = value.to_str().unwrap_or("");
|
||||||
|
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
||||||
|
STANDARD.decode(b64_data)
|
||||||
|
.ok()
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
} else {
|
||||||
|
Some(str_value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let uid = help::get_uid("R");
|
let uid = help::get_uid("R");
|
||||||
let file = format!("{uid}.yaml");
|
let file = format!("{uid}.yaml");
|
||||||
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
let name = name.or(profile_title).unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||||
let data = resp.text_with_charset("utf-8").await?;
|
let data = resp.text_with_charset("utf-8").await?;
|
||||||
|
|
||||||
// process the charset "UTF-8 with BOM"
|
// process the charset "UTF-8 with BOM"
|
||||||
@@ -398,7 +467,7 @@ impl PrfItem {
|
|||||||
name: Some(name),
|
name: Some(name),
|
||||||
desc,
|
desc,
|
||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: Some(url.into()),
|
url: Some(final_url),
|
||||||
selected: None,
|
selected: None,
|
||||||
extra,
|
extra,
|
||||||
option: Some(PrfOption {
|
option: Some(PrfOption {
|
||||||
@@ -411,6 +480,8 @@ impl PrfItem {
|
|||||||
..PrfOption::default()
|
..PrfOption::default()
|
||||||
}),
|
}),
|
||||||
home,
|
home,
|
||||||
|
support_url,
|
||||||
|
announce,
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(data.into()),
|
file_data: Some(data.into()),
|
||||||
})
|
})
|
||||||
@@ -438,6 +509,8 @@ impl PrfItem {
|
|||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(template),
|
file_data: Some(template),
|
||||||
})
|
})
|
||||||
@@ -460,6 +533,8 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
@@ -481,6 +556,8 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
@@ -502,6 +579,8 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
@@ -523,6 +602,8 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ impl IProfiles {
|
|||||||
let path = dirs::app_profiles_dir()?.join(&file);
|
let path = dirs::app_profiles_dir()?.join(&file);
|
||||||
|
|
||||||
fs::File::create(path)
|
fs::File::create(path)
|
||||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||||
.write(file_data.as_bytes())
|
.write(file_data.as_bytes())
|
||||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.current.is_none()
|
if self.current.is_none()
|
||||||
@@ -220,6 +220,10 @@ impl IProfiles {
|
|||||||
each.extra = item.extra;
|
each.extra = item.extra;
|
||||||
each.updated = item.updated;
|
each.updated = item.updated;
|
||||||
each.home = item.home;
|
each.home = item.home;
|
||||||
|
each.announce = item.announce;
|
||||||
|
each.support_url = item.support_url;
|
||||||
|
each.name = item.name;
|
||||||
|
each.url = item.url;
|
||||||
each.option = PrfOption::merge(each.option.clone(), item.option);
|
each.option = PrfOption::merge(each.option.clone(), item.option);
|
||||||
// save the file data
|
// save the file data
|
||||||
// move the field value after save
|
// move the field value after save
|
||||||
@@ -234,9 +238,9 @@ impl IProfiles {
|
|||||||
let path = dirs::app_profiles_dir()?.join(&file);
|
let path = dirs::app_profiles_dir()?.join(&file);
|
||||||
|
|
||||||
fs::File::create(path)
|
fs::File::create(path)
|
||||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||||
.write(file_data.as_bytes())
|
.write(file_data.as_bytes())
|
||||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -531,7 +535,7 @@ impl IProfiles {
|
|||||||
if Self::is_profile_file(file_name) {
|
if Self::is_profile_file(file_name) {
|
||||||
// 检查是否为全局扩展文件
|
// 检查是否为全局扩展文件
|
||||||
if protected_files.contains(file_name) {
|
if protected_files.contains(file_name) {
|
||||||
log::debug!(target: "app", "保护全局扩展配置文件: {}", file_name);
|
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,11 +544,11 @@ impl IProfiles {
|
|||||||
match std::fs::remove_file(&path) {
|
match std::fs::remove_file(&path) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
deleted_files.push(file_name.to_string());
|
deleted_files.push(file_name.to_string());
|
||||||
log::info!(target: "app", "已清理冗余文件: {}", file_name);
|
log::info!(target: "app", "已清理冗余文件: {file_name}");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
failed_deletions.push(format!("{}: {}", file_name, e));
|
failed_deletions.push(format!("{file_name}: {e}"));
|
||||||
log::warn!(target: "app", "清理文件失败: {} - {}", file_name, e);
|
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -681,7 +685,7 @@ impl IProfiles {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!(target: "app", "自动清理失败: {}", e);
|
log::warn!(target: "app", "自动清理失败: {e}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ pub struct IVerge {
|
|||||||
/// enable dns settings - this controls whether dns_config.yaml is applied
|
/// enable dns settings - this controls whether dns_config.yaml is applied
|
||||||
pub enable_dns_settings: Option<bool>,
|
pub enable_dns_settings: Option<bool>,
|
||||||
|
|
||||||
|
pub primary_action: Option<String>,
|
||||||
|
|
||||||
/// always use default bypass
|
/// always use default bypass
|
||||||
pub use_default_bypass: Option<bool>,
|
pub use_default_bypass: Option<bool>,
|
||||||
|
|
||||||
@@ -391,7 +393,7 @@ impl IVerge {
|
|||||||
auto_close_connection: Some(true),
|
auto_close_connection: Some(true),
|
||||||
auto_check_update: Some(true),
|
auto_check_update: Some(true),
|
||||||
enable_builtin_enhanced: Some(true),
|
enable_builtin_enhanced: Some(true),
|
||||||
auto_log_clean: Some(3),
|
auto_log_clean: Some(2),
|
||||||
webdav_url: None,
|
webdav_url: None,
|
||||||
webdav_username: None,
|
webdav_username: None,
|
||||||
webdav_password: None,
|
webdav_password: None,
|
||||||
@@ -401,6 +403,7 @@ impl IVerge {
|
|||||||
enable_auto_light_weight_mode: Some(false),
|
enable_auto_light_weight_mode: Some(false),
|
||||||
auto_light_weight_minutes: Some(10),
|
auto_light_weight_minutes: Some(10),
|
||||||
enable_dns_settings: Some(false),
|
enable_dns_settings: Some(false),
|
||||||
|
primary_action: Some("tun-mode".into()),
|
||||||
home_cards: None,
|
home_cards: None,
|
||||||
service_state: None,
|
service_state: None,
|
||||||
..Self::default()
|
..Self::default()
|
||||||
@@ -489,6 +492,7 @@ impl IVerge {
|
|||||||
patch!(enable_auto_light_weight_mode);
|
patch!(enable_auto_light_weight_mode);
|
||||||
patch!(auto_light_weight_minutes);
|
patch!(auto_light_weight_minutes);
|
||||||
patch!(enable_dns_settings);
|
patch!(enable_dns_settings);
|
||||||
|
patch!(primary_action);
|
||||||
patch!(home_cards);
|
patch!(home_cards);
|
||||||
patch!(service_state);
|
patch!(service_state);
|
||||||
}
|
}
|
||||||
@@ -584,6 +588,7 @@ pub struct IVergeResponse {
|
|||||||
pub enable_auto_light_weight_mode: Option<bool>,
|
pub enable_auto_light_weight_mode: Option<bool>,
|
||||||
pub auto_light_weight_minutes: Option<u64>,
|
pub auto_light_weight_minutes: Option<u64>,
|
||||||
pub enable_dns_settings: Option<bool>,
|
pub enable_dns_settings: Option<bool>,
|
||||||
|
pub primary_action: Option<String>,
|
||||||
pub home_cards: Option<serde_json::Value>,
|
pub home_cards: Option<serde_json::Value>,
|
||||||
pub enable_hover_jump_navigator: Option<bool>,
|
pub enable_hover_jump_navigator: Option<bool>,
|
||||||
pub service_state: Option<crate::core::service::ServiceState>,
|
pub service_state: Option<crate::core::service::ServiceState>,
|
||||||
@@ -656,6 +661,7 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||||
enable_dns_settings: verge.enable_dns_settings,
|
enable_dns_settings: verge.enable_dns_settings,
|
||||||
|
primary_action: verge.primary_action,
|
||||||
home_cards: verge.home_cards,
|
home_cards: verge.home_cards,
|
||||||
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
||||||
service_state: verge.service_state,
|
service_state: verge.service_state,
|
||||||
|
|||||||
531
src-tauri/src/core/async_proxy_query.rs
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use anyhow::anyhow;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct AsyncAutoproxy {
|
||||||
|
pub enable: bool,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AsyncSysproxy {
|
||||||
|
pub enable: bool,
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub bypass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AsyncSysproxy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enable: false,
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 7890,
|
||||||
|
bypass: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AsyncProxyQuery;
|
||||||
|
|
||||||
|
impl AsyncProxyQuery {
|
||||||
|
/// 异步获取自动代理配置(PAC)
|
||||||
|
pub async fn get_auto_proxy() -> AsyncAutoproxy {
|
||||||
|
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
|
||||||
|
Ok(Ok(proxy)) => {
|
||||||
|
log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url);
|
||||||
|
proxy
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::warn!(target: "app", "异步获取自动代理失败: {e}");
|
||||||
|
AsyncAutoproxy::default()
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!(target: "app", "异步获取自动代理超时");
|
||||||
|
AsyncAutoproxy::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 异步获取系统代理配置
|
||||||
|
pub async fn get_system_proxy() -> AsyncSysproxy {
|
||||||
|
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
|
||||||
|
Ok(Ok(proxy)) => {
|
||||||
|
log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
|
||||||
|
proxy
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::warn!(target: "app", "异步获取系统代理失败: {e}");
|
||||||
|
AsyncSysproxy::default()
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!(target: "app", "异步获取系统代理超时");
|
||||||
|
AsyncSysproxy::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||||
|
// Windows: 从注册表读取PAC配置
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<AsyncAutoproxy> {
|
||||||
|
Self::get_pac_config_from_registry()
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn get_pac_config_from_registry() -> Result<AsyncAutoproxy> {
|
||||||
|
use std::ptr;
|
||||||
|
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||||
|
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||||
|
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||||
|
.encode_utf16()
|
||||||
|
.collect::<Vec<u16>>();
|
||||||
|
|
||||||
|
let mut hkey: HKEY = ptr::null_mut();
|
||||||
|
let result =
|
||||||
|
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||||
|
|
||||||
|
if result != 0 {
|
||||||
|
log::debug!(target: "app", "无法打开注册表项");
|
||||||
|
return Ok(AsyncAutoproxy::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用)
|
||||||
|
let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::<Vec<u16>>();
|
||||||
|
let mut url_buffer = vec![0u16; 1024];
|
||||||
|
let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD;
|
||||||
|
let mut url_value_type: DWORD = 0;
|
||||||
|
|
||||||
|
let url_query_result = RegQueryValueExW(
|
||||||
|
hkey,
|
||||||
|
auto_config_url_name.as_ptr(),
|
||||||
|
ptr::null_mut(),
|
||||||
|
&mut url_value_type,
|
||||||
|
url_buffer.as_mut_ptr() as *mut u8,
|
||||||
|
&mut url_buffer_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut pac_url = String::new();
|
||||||
|
if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 {
|
||||||
|
let end_pos = url_buffer
|
||||||
|
.iter()
|
||||||
|
.position(|&x| x == 0)
|
||||||
|
.unwrap_or(url_buffer.len());
|
||||||
|
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
|
||||||
|
log::debug!(target: "app", "从注册表读取到PAC URL: {}", pac_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查自动检测设置是否启用
|
||||||
|
let auto_detect_name = "AutoDetect\0".encode_utf16().collect::<Vec<u16>>();
|
||||||
|
let mut auto_detect: DWORD = 0;
|
||||||
|
let mut detect_buffer_size: DWORD = 4;
|
||||||
|
let mut detect_value_type: DWORD = 0;
|
||||||
|
|
||||||
|
let detect_query_result = RegQueryValueExW(
|
||||||
|
hkey,
|
||||||
|
auto_detect_name.as_ptr(),
|
||||||
|
ptr::null_mut(),
|
||||||
|
&mut detect_value_type,
|
||||||
|
&mut auto_detect as *mut DWORD as *mut u8,
|
||||||
|
&mut detect_buffer_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
RegCloseKey(hkey);
|
||||||
|
|
||||||
|
// PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用
|
||||||
|
let pac_enabled = !pac_url.is_empty()
|
||||||
|
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
|
||||||
|
|
||||||
|
if pac_enabled {
|
||||||
|
log::debug!(target: "app", "PAC配置启用: URL={}, AutoDetect={}", pac_url, auto_detect);
|
||||||
|
|
||||||
|
if pac_url.is_empty() && auto_detect != 0 {
|
||||||
|
pac_url = "auto-detect".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AsyncAutoproxy {
|
||||||
|
enable: true,
|
||||||
|
url: pac_url,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log::debug!(target: "app", "PAC配置未启用");
|
||||||
|
Ok(AsyncAutoproxy::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||||
|
// macOS: 使用 scutil --proxy 命令
|
||||||
|
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Ok(AsyncAutoproxy::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
log::debug!(target: "app", "scutil output: {stdout}");
|
||||||
|
|
||||||
|
let mut pac_enabled = false;
|
||||||
|
let mut pac_url = String::new();
|
||||||
|
|
||||||
|
// 解析 scutil 输出
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.contains("ProxyAutoConfigEnable") && line.contains("1") {
|
||||||
|
pac_enabled = true;
|
||||||
|
} else if line.contains("ProxyAutoConfigURLString") {
|
||||||
|
// 正确解析包含冒号的URL
|
||||||
|
// 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac"
|
||||||
|
if let Some(colon_pos) = line.find(" : ") {
|
||||||
|
pac_url = line[colon_pos + 3..].trim().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
|
||||||
|
|
||||||
|
Ok(AsyncAutoproxy {
|
||||||
|
enable: pac_enabled && !pac_url.is_empty(),
|
||||||
|
url: pac_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||||
|
// Linux: 检查环境变量和GNOME设置
|
||||||
|
|
||||||
|
// 首先检查环境变量
|
||||||
|
if let Ok(auto_proxy) = std::env::var("auto_proxy") {
|
||||||
|
if !auto_proxy.is_empty() {
|
||||||
|
return Ok(AsyncAutoproxy {
|
||||||
|
enable: true,
|
||||||
|
url: auto_proxy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||||
|
let output = Command::new("gsettings")
|
||||||
|
.args(["get", "org.gnome.system.proxy", "mode"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(output) = output {
|
||||||
|
if output.status.success() {
|
||||||
|
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if mode.contains("auto") {
|
||||||
|
// 获取 PAC URL
|
||||||
|
let pac_output = Command::new("gsettings")
|
||||||
|
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(pac_output) = pac_output {
|
||||||
|
if pac_output.status.success() {
|
||||||
|
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
|
||||||
|
.trim()
|
||||||
|
.trim_matches('\'')
|
||||||
|
.trim_matches('"')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if !pac_url.is_empty() {
|
||||||
|
return Ok(AsyncAutoproxy {
|
||||||
|
enable: true,
|
||||||
|
url: pac_url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AsyncAutoproxy::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||||
|
// Windows: 使用注册表直接读取代理设置
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<AsyncSysproxy> {
|
||||||
|
Self::get_system_proxy_from_registry()
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn get_system_proxy_from_registry() -> Result<AsyncSysproxy> {
|
||||||
|
use std::ptr;
|
||||||
|
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||||
|
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||||
|
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||||
|
.encode_utf16()
|
||||||
|
.collect::<Vec<u16>>();
|
||||||
|
|
||||||
|
let mut hkey: HKEY = ptr::null_mut();
|
||||||
|
let result =
|
||||||
|
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||||
|
|
||||||
|
if result != 0 {
|
||||||
|
return Ok(AsyncSysproxy::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查代理是否启用
|
||||||
|
let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::<Vec<u16>>();
|
||||||
|
let mut proxy_enable: DWORD = 0;
|
||||||
|
let mut buffer_size: DWORD = 4;
|
||||||
|
let mut value_type: DWORD = 0;
|
||||||
|
|
||||||
|
let enable_result = RegQueryValueExW(
|
||||||
|
hkey,
|
||||||
|
proxy_enable_name.as_ptr(),
|
||||||
|
ptr::null_mut(),
|
||||||
|
&mut value_type,
|
||||||
|
&mut proxy_enable as *mut DWORD as *mut u8,
|
||||||
|
&mut buffer_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 {
|
||||||
|
RegCloseKey(hkey);
|
||||||
|
return Ok(AsyncSysproxy::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取代理服务器设置
|
||||||
|
let proxy_server_name = "ProxyServer\0".encode_utf16().collect::<Vec<u16>>();
|
||||||
|
let mut buffer = vec![0u16; 1024];
|
||||||
|
let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD;
|
||||||
|
let mut value_type: DWORD = 0;
|
||||||
|
|
||||||
|
let server_result = RegQueryValueExW(
|
||||||
|
hkey,
|
||||||
|
proxy_server_name.as_ptr(),
|
||||||
|
ptr::null_mut(),
|
||||||
|
&mut value_type,
|
||||||
|
buffer.as_mut_ptr() as *mut u8,
|
||||||
|
&mut buffer_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut proxy_server = String::new();
|
||||||
|
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
||||||
|
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
|
||||||
|
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取代理绕过列表
|
||||||
|
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
|
||||||
|
let mut bypass_buffer = vec![0u16; 1024];
|
||||||
|
let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD;
|
||||||
|
let mut bypass_value_type: DWORD = 0;
|
||||||
|
|
||||||
|
let override_result = RegQueryValueExW(
|
||||||
|
hkey,
|
||||||
|
proxy_override_name.as_ptr(),
|
||||||
|
ptr::null_mut(),
|
||||||
|
&mut bypass_value_type,
|
||||||
|
bypass_buffer.as_mut_ptr() as *mut u8,
|
||||||
|
&mut bypass_buffer_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut bypass_list = String::new();
|
||||||
|
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
|
||||||
|
let end_pos = bypass_buffer
|
||||||
|
.iter()
|
||||||
|
.position(|&x| x == 0)
|
||||||
|
.unwrap_or(bypass_buffer.len());
|
||||||
|
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RegCloseKey(hkey);
|
||||||
|
|
||||||
|
if !proxy_server.is_empty() {
|
||||||
|
// 解析服务器地址和端口
|
||||||
|
let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') {
|
||||||
|
let host = proxy_server[..colon_pos].to_string();
|
||||||
|
let port = proxy_server[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||||
|
(host, port)
|
||||||
|
} else {
|
||||||
|
(proxy_server, 8080)
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!(target: "app", "从注册表读取到代理设置: {}:{}, bypass: {}", host, port, bypass_list);
|
||||||
|
|
||||||
|
Ok(AsyncSysproxy {
|
||||||
|
enable: true,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
bypass: bypass_list,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(AsyncSysproxy::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||||
|
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Ok(AsyncSysproxy::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
log::debug!(target: "app", "scutil proxy output: {stdout}");
|
||||||
|
|
||||||
|
let mut http_enabled = false;
|
||||||
|
let mut http_host = String::new();
|
||||||
|
let mut http_port = 8080u16;
|
||||||
|
let mut exceptions = Vec::new();
|
||||||
|
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.contains("HTTPEnable") && line.contains("1") {
|
||||||
|
http_enabled = true;
|
||||||
|
} else if line.contains("HTTPProxy") && !line.contains("Port") {
|
||||||
|
if let Some(host_part) = line.split(':').nth(1) {
|
||||||
|
http_host = host_part.trim().to_string();
|
||||||
|
}
|
||||||
|
} else if line.contains("HTTPPort") {
|
||||||
|
if let Some(port_part) = line.split(':').nth(1) {
|
||||||
|
if let Ok(port) = port_part.trim().parse::<u16>() {
|
||||||
|
http_port = port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if line.contains("ExceptionsList") {
|
||||||
|
// 解析异常列表
|
||||||
|
if let Some(list_part) = line.split(':').nth(1) {
|
||||||
|
let list = list_part.trim();
|
||||||
|
if !list.is_empty() {
|
||||||
|
exceptions.push(list.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AsyncSysproxy {
|
||||||
|
enable: http_enabled && !http_host.is_empty(),
|
||||||
|
host: http_host,
|
||||||
|
port: http_port,
|
||||||
|
bypass: exceptions.join(","),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||||
|
// Linux: 检查环境变量和桌面环境设置
|
||||||
|
|
||||||
|
// 首先检查环境变量
|
||||||
|
if let Ok(http_proxy) = std::env::var("http_proxy") {
|
||||||
|
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) {
|
||||||
|
return Ok(proxy_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(https_proxy) = std::env::var("https_proxy") {
|
||||||
|
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) {
|
||||||
|
return Ok(proxy_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||||
|
let mode_output = Command::new("gsettings")
|
||||||
|
.args(["get", "org.gnome.system.proxy", "mode"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(mode_output) = mode_output {
|
||||||
|
if mode_output.status.success() {
|
||||||
|
let mode = String::from_utf8_lossy(&mode_output.stdout)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if mode.contains("manual") {
|
||||||
|
// 获取HTTP代理设置
|
||||||
|
let host_result = Command::new("gsettings")
|
||||||
|
.args(["get", "org.gnome.system.proxy.http", "host"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let port_result = Command::new("gsettings")
|
||||||
|
.args(["get", "org.gnome.system.proxy.http", "port"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) {
|
||||||
|
if host_output.status.success() && port_output.status.success() {
|
||||||
|
let host = String::from_utf8_lossy(&host_output.stdout)
|
||||||
|
.trim()
|
||||||
|
.trim_matches('\'')
|
||||||
|
.trim_matches('"')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let port = String::from_utf8_lossy(&port_output.stdout)
|
||||||
|
.trim()
|
||||||
|
.parse::<u16>()
|
||||||
|
.unwrap_or(8080);
|
||||||
|
|
||||||
|
if !host.is_empty() {
|
||||||
|
return Ok(AsyncSysproxy {
|
||||||
|
enable: true,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
bypass: String::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AsyncSysproxy::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn parse_proxy_url(proxy_url: &str) -> Result<AsyncSysproxy> {
|
||||||
|
// 解析形如 "http://proxy.example.com:8080" 的URL
|
||||||
|
let url = proxy_url.trim();
|
||||||
|
|
||||||
|
// 移除协议前缀
|
||||||
|
let url = if let Some(stripped) = url.strip_prefix("http://") {
|
||||||
|
stripped
|
||||||
|
} else if let Some(stripped) = url.strip_prefix("https://") {
|
||||||
|
stripped
|
||||||
|
} else {
|
||||||
|
url
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析主机和端口
|
||||||
|
let (host, port) = if let Some(colon_pos) = url.rfind(':') {
|
||||||
|
let host = url[..colon_pos].to_string();
|
||||||
|
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||||
|
(host, port)
|
||||||
|
} else {
|
||||||
|
(url.to_string(), 8080)
|
||||||
|
};
|
||||||
|
|
||||||
|
if host.is_empty() {
|
||||||
|
return Err(anyhow!("无效的代理URL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AsyncSysproxy {
|
||||||
|
enable: true,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
bypass: std::env::var("no_proxy").unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,10 +108,7 @@ impl WebDavClient {
|
|||||||
reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.timeout(Duration::from_secs(op.timeout()))
|
.timeout(Duration::from_secs(op.timeout()))
|
||||||
.user_agent(format!(
|
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
|
||||||
"clash-verge/{} ({} WebDAV-Client)",
|
|
||||||
APP_VERSION, OS
|
|
||||||
))
|
|
||||||
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
||||||
// 允许所有请求类型的重定向,包括PUT
|
// 允许所有请求类型的重定向,包括PUT
|
||||||
if attempt.previous().len() >= 5 {
|
if attempt.previous().len() >= 5 {
|
||||||
@@ -177,7 +174,7 @@ impl WebDavClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
log::warn!("Upload failed, retrying once: {}", e);
|
log::warn!("Upload failed, retrying once: {e}");
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
timeout(
|
timeout(
|
||||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||||
@@ -237,7 +234,7 @@ impl WebDavClient {
|
|||||||
|
|
||||||
pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||||
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
|
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
|
||||||
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
|
let zip_file_name = format!("{OS}-backup-{now}.zip");
|
||||||
let zip_path = temp_dir().join(&zip_file_name);
|
let zip_path = temp_dir().join(&zip_file_name);
|
||||||
|
|
||||||
let file = fs::File::create(&zip_path)?;
|
let file = fs::File::create(&zip_path)?;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#[cfg(target_os = "macos")]
|
|
||||||
use crate::core::tray::Tray;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::*,
|
config::*,
|
||||||
core::{
|
core::{
|
||||||
@@ -174,7 +172,7 @@ impl CoreManager {
|
|||||||
|
|
||||||
// 检查文件是否存在
|
// 检查文件是否存在
|
||||||
if !std::path::Path::new(config_path).exists() {
|
if !std::path::Path::new(config_path).exists() {
|
||||||
let error_msg = format!("File not found: {}", config_path);
|
let error_msg = format!("File not found: {config_path}");
|
||||||
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
|
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
|
||||||
return Ok((false, error_msg));
|
return Ok((false, error_msg));
|
||||||
}
|
}
|
||||||
@@ -286,7 +284,7 @@ impl CoreManager {
|
|||||||
} else if !stderr.is_empty() {
|
} else if !stderr.is_empty() {
|
||||||
stderr.to_string()
|
stderr.to_string()
|
||||||
} else if let Some(code) = output.status.code() {
|
} else if let Some(code) = output.status.code() {
|
||||||
format!("验证进程异常退出,退出码: {}", code)
|
format!("验证进程异常退出,退出码: {code}")
|
||||||
} else {
|
} else {
|
||||||
"验证进程被终止".to_string()
|
"验证进程被终止".to_string()
|
||||||
};
|
};
|
||||||
@@ -307,7 +305,7 @@ impl CoreManager {
|
|||||||
let content = match std::fs::read_to_string(config_path) {
|
let content = match std::fs::read_to_string(config_path) {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error_msg = format!("Failed to read file: {}", err);
|
let error_msg = format!("Failed to read file: {err}");
|
||||||
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
|
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
|
||||||
return Ok((false, error_msg));
|
return Ok((false, error_msg));
|
||||||
}
|
}
|
||||||
@@ -321,7 +319,7 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// 使用标准化的前缀,以便错误处理函数能正确识别
|
// 使用标准化的前缀,以便错误处理函数能正确识别
|
||||||
let error_msg = format!("YAML syntax error: {}", err);
|
let error_msg = format!("YAML syntax error: {err}");
|
||||||
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
|
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
|
||||||
Ok((false, error_msg))
|
Ok((false, error_msg))
|
||||||
}
|
}
|
||||||
@@ -333,7 +331,7 @@ impl CoreManager {
|
|||||||
let content = match std::fs::read_to_string(path) {
|
let content = match std::fs::read_to_string(path) {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error_msg = format!("Failed to read script file: {}", err);
|
let error_msg = format!("Failed to read script file: {err}");
|
||||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||||
return Ok((false, error_msg));
|
return Ok((false, error_msg));
|
||||||
@@ -366,7 +364,7 @@ impl CoreManager {
|
|||||||
Ok((true, String::new()))
|
Ok((true, String::new()))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error_msg = format!("Script syntax error: {}", err);
|
let error_msg = format!("Script syntax error: {err}");
|
||||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||||
Ok((false, error_msg))
|
Ok((false, error_msg))
|
||||||
@@ -435,6 +433,306 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CoreManager {
|
impl CoreManager {
|
||||||
|
/// 清理多余的 mihomo 进程
|
||||||
|
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
|
||||||
|
logging!(info, Type::Core, true, "开始清理多余的 mihomo 进程");
|
||||||
|
|
||||||
|
// 获取当前管理的进程 PID
|
||||||
|
let current_pid = {
|
||||||
|
let child_guard = self.child_sidecar.lock().await;
|
||||||
|
child_guard.as_ref().map(|child| child.pid())
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_processes = ["verge-mihomo", "verge-mihomo-alpha"];
|
||||||
|
|
||||||
|
// 并行查找所有目标进程
|
||||||
|
let mut process_futures = Vec::new();
|
||||||
|
for &target in &target_processes {
|
||||||
|
let process_name = if cfg!(windows) {
|
||||||
|
format!("{target}.exe")
|
||||||
|
} else {
|
||||||
|
target.to_string()
|
||||||
|
};
|
||||||
|
process_futures.push(self.find_processes_by_name(process_name, target));
|
||||||
|
}
|
||||||
|
|
||||||
|
let process_results = futures::future::join_all(process_futures).await;
|
||||||
|
|
||||||
|
// 收集所有需要终止的进程PID
|
||||||
|
let mut pids_to_kill = Vec::new();
|
||||||
|
for result in process_results {
|
||||||
|
match result {
|
||||||
|
Ok((pids, process_name)) => {
|
||||||
|
for pid in pids {
|
||||||
|
// 跳过当前管理的进程
|
||||||
|
if let Some(current) = current_pid {
|
||||||
|
if pid == current {
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Core,
|
||||||
|
true,
|
||||||
|
"跳过当前管理的进程: {} (PID: {})",
|
||||||
|
process_name,
|
||||||
|
pid
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pids_to_kill.push((pid, process_name.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(debug, Type::Core, true, "查找进程时发生错误: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pids_to_kill.is_empty() {
|
||||||
|
logging!(debug, Type::Core, true, "未发现多余的 mihomo 进程");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut kill_futures = Vec::new();
|
||||||
|
for (pid, process_name) in &pids_to_kill {
|
||||||
|
kill_futures.push(self.kill_process_with_verification(*pid, process_name.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let kill_results = futures::future::join_all(kill_futures).await;
|
||||||
|
|
||||||
|
let killed_count = kill_results.into_iter().filter(|&success| success).count();
|
||||||
|
|
||||||
|
if killed_count > 0 {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Core,
|
||||||
|
true,
|
||||||
|
"清理完成,共终止了 {} 个多余的 mihomo 进程",
|
||||||
|
killed_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据进程名查找进程PID列
|
||||||
|
async fn find_processes_by_name(
|
||||||
|
&self,
|
||||||
|
process_name: String,
|
||||||
|
_target: &str,
|
||||||
|
) -> Result<(Vec<u32>, String)> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use std::mem;
|
||||||
|
use winapi::um::handleapi::CloseHandle;
|
||||||
|
use winapi::um::tlhelp32::{
|
||||||
|
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
||||||
|
TH32CS_SNAPPROCESS,
|
||||||
|
};
|
||||||
|
use winapi::um::winnt::HANDLE;
|
||||||
|
|
||||||
|
let process_name_clone = process_name.clone();
|
||||||
|
let pids = tokio::task::spawn_blocking(move || -> Result<Vec<u32>> {
|
||||||
|
let mut pids = Vec::new();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// 创建进程快照
|
||||||
|
let snapshot: HANDLE = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
|
if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
|
||||||
|
return Err(anyhow::anyhow!("Failed to create process snapshot"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pe32: PROCESSENTRY32W = mem::zeroed();
|
||||||
|
pe32.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
|
||||||
|
|
||||||
|
// 获取第一个进程
|
||||||
|
if Process32FirstW(snapshot, &mut pe32) != 0 {
|
||||||
|
loop {
|
||||||
|
// 将宽字符转换为String
|
||||||
|
let end_pos = pe32
|
||||||
|
.szExeFile
|
||||||
|
.iter()
|
||||||
|
.position(|&x| x == 0)
|
||||||
|
.unwrap_or(pe32.szExeFile.len());
|
||||||
|
let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]);
|
||||||
|
|
||||||
|
// 检查进程名是否匹配
|
||||||
|
if exe_file.eq_ignore_ascii_case(&process_name_clone) {
|
||||||
|
pids.push(pe32.th32ProcessID);
|
||||||
|
}
|
||||||
|
if Process32NextW(snapshot, &mut pe32) == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭句柄
|
||||||
|
CloseHandle(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(pids)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
Ok((pids, process_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let output = if cfg!(target_os = "macos") {
|
||||||
|
tokio::process::Command::new("pgrep")
|
||||||
|
.arg(&process_name)
|
||||||
|
.output()
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
// Linux
|
||||||
|
tokio::process::Command::new("pidof")
|
||||||
|
.arg(&process_name)
|
||||||
|
.output()
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Ok((Vec::new(), process_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let mut pids = Vec::new();
|
||||||
|
|
||||||
|
// Unix系统直接解析PID列表
|
||||||
|
for pid_str in stdout.split_whitespace() {
|
||||||
|
if let Ok(pid) = pid_str.parse::<u32>() {
|
||||||
|
pids.push(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((pids, process_name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 终止进程并验证结果 - 使用Windows API直接终止,更优雅高效
|
||||||
|
async fn kill_process_with_verification(&self, pid: u32, process_name: String) -> bool {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Core,
|
||||||
|
true,
|
||||||
|
"尝试终止进程: {} (PID: {})",
|
||||||
|
process_name,
|
||||||
|
pid
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
let success = {
|
||||||
|
use winapi::um::handleapi::CloseHandle;
|
||||||
|
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
|
||||||
|
use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE};
|
||||||
|
|
||||||
|
tokio::task::spawn_blocking(move || -> bool {
|
||||||
|
unsafe {
|
||||||
|
let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
|
||||||
|
if process_handle.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let result = TerminateProcess(process_handle, 1);
|
||||||
|
CloseHandle(process_handle);
|
||||||
|
|
||||||
|
result != 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let success = {
|
||||||
|
tokio::process::Command::new("kill")
|
||||||
|
.args(["-9", &pid.to_string()])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map(|output| output.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
if success {
|
||||||
|
// 短暂等待并验证进程是否真正终止
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
let still_running = self.is_process_running(pid).await.unwrap_or(false);
|
||||||
|
if still_running {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Core,
|
||||||
|
true,
|
||||||
|
"进程 {} (PID: {}) 终止命令成功但进程仍在运行",
|
||||||
|
process_name,
|
||||||
|
pid
|
||||||
|
);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Core,
|
||||||
|
true,
|
||||||
|
"成功终止进程: {} (PID: {})",
|
||||||
|
process_name,
|
||||||
|
pid
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Core,
|
||||||
|
true,
|
||||||
|
"无法终止进程: {} (PID: {})",
|
||||||
|
process_name,
|
||||||
|
pid
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Windows API检查进程
|
||||||
|
async fn is_process_running(&self, pid: u32) -> Result<bool> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use winapi::shared::minwindef::DWORD;
|
||||||
|
use winapi::um::handleapi::CloseHandle;
|
||||||
|
use winapi::um::processthreadsapi::GetExitCodeProcess;
|
||||||
|
use winapi::um::processthreadsapi::OpenProcess;
|
||||||
|
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
|
||||||
|
|
||||||
|
let result = tokio::task::spawn_blocking(move || -> Result<bool> {
|
||||||
|
unsafe {
|
||||||
|
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
|
||||||
|
if process_handle.is_null() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let mut exit_code: DWORD = 0;
|
||||||
|
let result = GetExitCodeProcess(process_handle, &mut exit_code);
|
||||||
|
CloseHandle(process_handle);
|
||||||
|
|
||||||
|
if result == 0 {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
Ok(exit_code == 259)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let output = tokio::process::Command::new("ps")
|
||||||
|
.args(["-p", &pid.to_string()])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(output.status.success() && !output.stdout.is_empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn start_core_by_sidecar(&self) -> Result<()> {
|
async fn start_core_by_sidecar(&self) -> Result<()> {
|
||||||
logging!(trace, Type::Core, true, "Running core by sidecar");
|
logging!(trace, Type::Core, true, "Running core by sidecar");
|
||||||
let config_file = &Config::generate_file(ConfigType::Run)?;
|
let config_file = &Config::generate_file(ConfigType::Run)?;
|
||||||
@@ -450,7 +748,7 @@ impl CoreManager {
|
|||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
|
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
|
||||||
|
|
||||||
let log_path = service_log_dir.join(format!("sidecar_{}.log", timestamp));
|
let log_path = service_log_dir.join(format!("sidecar_{timestamp}.log"));
|
||||||
|
|
||||||
let mut log_file = File::create(log_path)?;
|
let mut log_file = File::create(log_path)?;
|
||||||
|
|
||||||
@@ -566,7 +864,7 @@ impl CoreManager {
|
|||||||
let mut state = service::ServiceState::get();
|
let mut state = service::ServiceState::get();
|
||||||
if !state.prefer_sidecar {
|
if !state.prefer_sidecar {
|
||||||
state.prefer_sidecar = true;
|
state.prefer_sidecar = true;
|
||||||
state.last_error = Some(format!("通过服务启动核心失败: {}", e));
|
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
|
||||||
if let Err(save_err) = state.save() {
|
if let Err(save_err) = state.save() {
|
||||||
logging!(
|
logging!(
|
||||||
error,
|
error,
|
||||||
@@ -585,6 +883,17 @@ impl CoreManager {
|
|||||||
pub async fn init(&self) -> Result<()> {
|
pub async fn init(&self) -> Result<()> {
|
||||||
logging!(trace, Type::Core, "Initializing core");
|
logging!(trace, Type::Core, "Initializing core");
|
||||||
|
|
||||||
|
// 应用启动时先清理任何遗留的 mihomo 进程
|
||||||
|
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Core,
|
||||||
|
true,
|
||||||
|
"应用初始化时清理多余 mihomo 进程失败: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut core_started_successfully = false;
|
let mut core_started_successfully = false;
|
||||||
|
|
||||||
if service::is_service_available().await.is_ok() {
|
if service::is_service_available().await.is_ok() {
|
||||||
@@ -732,9 +1041,8 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logging!(trace, Type::Core, "Initied core logic completed");
|
logging!(trace, Type::Core, "Initied core logic completed");
|
||||||
#[cfg(target_os = "macos")]
|
// #[cfg(target_os = "macos")]
|
||||||
logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
|
// logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,6 +1095,7 @@ impl CoreManager {
|
|||||||
/// 重启内核
|
/// 重启内核
|
||||||
pub async fn restart_core(&self) -> Result<()> {
|
pub async fn restart_core(&self) -> Result<()> {
|
||||||
self.stop_core().await?;
|
self.stop_core().await?;
|
||||||
|
|
||||||
self.start_core().await?;
|
self.start_core().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -800,7 +1109,7 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
let core: &str = &clash_core.clone().unwrap();
|
let core: &str = &clash_core.clone().unwrap();
|
||||||
if !IVerge::VALID_CLASH_CORES.contains(&core) {
|
if !IVerge::VALID_CLASH_CORES.contains(&core) {
|
||||||
let error_message = format!("Clash core invalid name: {}", core);
|
let error_message = format!("Clash core invalid name: {core}");
|
||||||
logging!(error, Type::Core, true, "{}", error_message);
|
logging!(error, Type::Core, true, "{}", error_message);
|
||||||
return Err(error_message);
|
return Err(error_message);
|
||||||
}
|
}
|
||||||
|
|||||||
571
src-tauri/src/core/event_driven_proxy.rs
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
use tokio::time::{sleep, timeout, Duration};
|
||||||
|
|
||||||
|
use crate::config::{Config, IVerge};
|
||||||
|
use crate::core::async_proxy_query::AsyncProxyQuery;
|
||||||
|
use crate::logging_error;
|
||||||
|
use crate::utils::logging::Type;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use sysproxy::{Autoproxy, Sysproxy};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ProxyEvent {
|
||||||
|
/// 配置变更事件
|
||||||
|
ConfigChanged,
|
||||||
|
/// 强制检查代理状态
|
||||||
|
#[allow(dead_code)]
|
||||||
|
ForceCheck,
|
||||||
|
/// 启用系统代理
|
||||||
|
#[allow(dead_code)]
|
||||||
|
EnableProxy,
|
||||||
|
/// 禁用系统代理
|
||||||
|
#[allow(dead_code)]
|
||||||
|
DisableProxy,
|
||||||
|
/// 切换到PAC模式
|
||||||
|
#[allow(dead_code)]
|
||||||
|
SwitchToPac,
|
||||||
|
/// 切换到HTTP代理模式
|
||||||
|
#[allow(dead_code)]
|
||||||
|
SwitchToHttp,
|
||||||
|
/// 应用启动事件
|
||||||
|
AppStarted,
|
||||||
|
/// 应用关闭事件
|
||||||
|
#[allow(dead_code)]
|
||||||
|
AppStopping,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProxyState {
|
||||||
|
pub sys_enabled: bool,
|
||||||
|
pub pac_enabled: bool,
|
||||||
|
pub auto_proxy: Autoproxy,
|
||||||
|
pub sys_proxy: Sysproxy,
|
||||||
|
pub last_updated: std::time::Instant,
|
||||||
|
pub is_healthy: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProxyState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
sys_enabled: false,
|
||||||
|
pac_enabled: false,
|
||||||
|
auto_proxy: Autoproxy {
|
||||||
|
enable: false,
|
||||||
|
url: "".to_string(),
|
||||||
|
},
|
||||||
|
sys_proxy: Sysproxy {
|
||||||
|
enable: false,
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 7890,
|
||||||
|
bypass: "".to_string(),
|
||||||
|
},
|
||||||
|
last_updated: std::time::Instant::now(),
|
||||||
|
is_healthy: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EventDrivenProxyManager {
|
||||||
|
state: Arc<RwLock<ProxyState>>,
|
||||||
|
event_sender: mpsc::UnboundedSender<ProxyEvent>,
|
||||||
|
query_sender: mpsc::UnboundedSender<QueryRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct QueryRequest {
|
||||||
|
response_tx: oneshot::Sender<Autoproxy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置结构体移到外部
|
||||||
|
struct ProxyConfig {
|
||||||
|
sys_enabled: bool,
|
||||||
|
pac_enabled: bool,
|
||||||
|
guard_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
|
||||||
|
|
||||||
|
impl EventDrivenProxyManager {
|
||||||
|
pub fn global() -> &'static EventDrivenProxyManager {
|
||||||
|
&PROXY_MANAGER
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
let state = Arc::new(RwLock::new(ProxyState::default()));
|
||||||
|
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||||
|
let (query_tx, query_rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
Self::start_event_loop(state.clone(), event_rx, query_rx);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
event_sender: event_tx,
|
||||||
|
query_sender: query_tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取自动代理配置(缓存)
|
||||||
|
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
|
||||||
|
self.state.read().auto_proxy.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 异步获取最新的自动代理配置
|
||||||
|
pub async fn get_auto_proxy_async(&self) -> Autoproxy {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let query = QueryRequest { response_tx: tx };
|
||||||
|
|
||||||
|
if self.query_sender.send(query).is_err() {
|
||||||
|
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
|
||||||
|
return self.get_auto_proxy_cached();
|
||||||
|
}
|
||||||
|
|
||||||
|
match timeout(Duration::from_secs(5), rx).await {
|
||||||
|
Ok(Ok(result)) => result,
|
||||||
|
_ => {
|
||||||
|
log::warn!(target: "app", "查询超时,返回缓存数据");
|
||||||
|
self.get_auto_proxy_cached()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通知配置变更
|
||||||
|
pub fn notify_config_changed(&self) {
|
||||||
|
self.send_event(ProxyEvent::ConfigChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通知应用启动
|
||||||
|
pub fn notify_app_started(&self) {
|
||||||
|
self.send_event(ProxyEvent::AppStarted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通知应用即将关闭
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn notify_app_stopping(&self) {
|
||||||
|
self.send_event(ProxyEvent::AppStopping);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启用系统代理
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn enable_proxy(&self) {
|
||||||
|
self.send_event(ProxyEvent::EnableProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 禁用系统代理
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn disable_proxy(&self) {
|
||||||
|
self.send_event(ProxyEvent::DisableProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 强制检查代理状态
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn force_check(&self) {
|
||||||
|
self.send_event(ProxyEvent::ForceCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_event(&self, event: ProxyEvent) {
|
||||||
|
if let Err(e) = self.event_sender.send(event) {
|
||||||
|
log::error!(target: "app", "发送代理事件失败: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_event_loop(
|
||||||
|
state: Arc<RwLock<ProxyState>>,
|
||||||
|
mut event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
|
||||||
|
mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
||||||
|
) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
log::info!(target: "app", "事件驱动代理管理器启动");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
event = event_rx.recv() => {
|
||||||
|
match event {
|
||||||
|
Some(event) => {
|
||||||
|
log::debug!(target: "app", "处理代理事件: {event:?}");
|
||||||
|
Self::handle_event(&state, event).await;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
log::info!(target: "app", "事件通道关闭,代理管理器停止");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query = query_rx.recv() => {
|
||||||
|
match query {
|
||||||
|
Some(query) => {
|
||||||
|
let result = Self::handle_query(&state).await;
|
||||||
|
let _ = query.response_tx.send(result);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
log::info!(target: "app", "查询通道关闭");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
|
||||||
|
match event {
|
||||||
|
ProxyEvent::ConfigChanged | ProxyEvent::ForceCheck => {
|
||||||
|
Self::update_proxy_config(state).await;
|
||||||
|
}
|
||||||
|
ProxyEvent::EnableProxy => {
|
||||||
|
Self::enable_system_proxy(state).await;
|
||||||
|
}
|
||||||
|
ProxyEvent::DisableProxy => {
|
||||||
|
Self::disable_system_proxy(state).await;
|
||||||
|
}
|
||||||
|
ProxyEvent::SwitchToPac => {
|
||||||
|
Self::switch_proxy_mode(state, true).await;
|
||||||
|
}
|
||||||
|
ProxyEvent::SwitchToHttp => {
|
||||||
|
Self::switch_proxy_mode(state, false).await;
|
||||||
|
}
|
||||||
|
ProxyEvent::AppStarted => {
|
||||||
|
Self::initialize_proxy_state(state).await;
|
||||||
|
}
|
||||||
|
ProxyEvent::AppStopping => {
|
||||||
|
log::info!(target: "app", "清理代理状态");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_query(state: &Arc<RwLock<ProxyState>>) -> Autoproxy {
|
||||||
|
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||||
|
|
||||||
|
Self::update_state_timestamp(state, |s| {
|
||||||
|
s.auto_proxy = auto_proxy.clone();
|
||||||
|
});
|
||||||
|
|
||||||
|
auto_proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
||||||
|
log::info!(target: "app", "初始化代理状态");
|
||||||
|
|
||||||
|
let config = Self::get_proxy_config();
|
||||||
|
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||||
|
let sys_proxy = Self::get_sys_proxy_with_timeout().await;
|
||||||
|
|
||||||
|
Self::update_state_timestamp(state, |s| {
|
||||||
|
s.sys_enabled = config.sys_enabled;
|
||||||
|
s.pac_enabled = config.pac_enabled;
|
||||||
|
s.auto_proxy = auto_proxy;
|
||||||
|
s.sys_proxy = sys_proxy;
|
||||||
|
s.is_healthy = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
||||||
|
log::debug!(target: "app", "更新代理配置");
|
||||||
|
|
||||||
|
let config = Self::get_proxy_config();
|
||||||
|
|
||||||
|
Self::update_state_timestamp(state, |s| {
|
||||||
|
s.sys_enabled = config.sys_enabled;
|
||||||
|
s.pac_enabled = config.pac_enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
if config.guard_enabled && config.sys_enabled {
|
||||||
|
Self::check_and_restore_proxy(state).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||||
|
let (sys_enabled, pac_enabled) = {
|
||||||
|
let s = state.read();
|
||||||
|
(s.sys_enabled, s.pac_enabled)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !sys_enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!(target: "app", "检查代理状态");
|
||||||
|
|
||||||
|
if pac_enabled {
|
||||||
|
Self::check_and_restore_pac_proxy(state).await;
|
||||||
|
} else {
|
||||||
|
Self::check_and_restore_sys_proxy(state).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||||
|
let current = Self::get_auto_proxy_with_timeout().await;
|
||||||
|
let expected = Self::get_expected_pac_config();
|
||||||
|
|
||||||
|
Self::update_state_timestamp(state, |s| {
|
||||||
|
s.auto_proxy = current.clone();
|
||||||
|
});
|
||||||
|
|
||||||
|
if !current.enable || current.url != expected.url {
|
||||||
|
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
|
||||||
|
Self::restore_pac_proxy(&expected.url).await;
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
let restored = Self::get_auto_proxy_with_timeout().await;
|
||||||
|
|
||||||
|
Self::update_state_timestamp(state, |s| {
|
||||||
|
s.is_healthy = restored.enable && restored.url == expected.url;
|
||||||
|
s.auto_proxy = restored;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||||
|
let current = Self::get_sys_proxy_with_timeout().await;
|
||||||
|
let expected = Self::get_expected_sys_proxy();
|
||||||
|
|
||||||
|
Self::update_state_timestamp(state, |s| {
|
||||||
|
s.sys_proxy = current.clone();
|
||||||
|
});
|
||||||
|
|
||||||
|
if !current.enable || current.host != expected.host || current.port != expected.port {
|
||||||
|
log::info!(target: "app", "系统代理设置异常,正在恢复...");
|
||||||
|
Self::restore_sys_proxy(&expected).await;
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
let restored = Self::get_sys_proxy_with_timeout().await;
|
||||||
|
|
||||||
|
Self::update_state_timestamp(state, |s| {
|
||||||
|
s.is_healthy = restored.enable
|
||||||
|
&& restored.host == expected.host
|
||||||
|
&& restored.port == expected.port;
|
||||||
|
s.sys_proxy = restored;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||||
|
log::info!(target: "app", "启用系统代理");
|
||||||
|
|
||||||
|
let pac_enabled = state.read().pac_enabled;
|
||||||
|
|
||||||
|
if pac_enabled {
|
||||||
|
let expected = Self::get_expected_pac_config();
|
||||||
|
Self::restore_pac_proxy(&expected.url).await;
|
||||||
|
} else {
|
||||||
|
let expected = Self::get_expected_sys_proxy();
|
||||||
|
Self::restore_sys_proxy(&expected).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::check_and_restore_proxy(state).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
|
||||||
|
log::info!(target: "app", "禁用系统代理");
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
let disabled_sys = Sysproxy::default();
|
||||||
|
let disabled_auto = Autoproxy::default();
|
||||||
|
|
||||||
|
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
||||||
|
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
|
||||||
|
log::info!(target: "app", "切换到{}模式", if to_pac { "PAC" } else { "HTTP代理" });
|
||||||
|
|
||||||
|
if to_pac {
|
||||||
|
let disabled_sys = Sysproxy::default();
|
||||||
|
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
||||||
|
|
||||||
|
let expected = Self::get_expected_pac_config();
|
||||||
|
Self::restore_pac_proxy(&expected.url).await;
|
||||||
|
} else {
|
||||||
|
let disabled_auto = Autoproxy::default();
|
||||||
|
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
||||||
|
|
||||||
|
let expected = Self::get_expected_sys_proxy();
|
||||||
|
Self::restore_sys_proxy(&expected).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac);
|
||||||
|
Self::check_and_restore_proxy(state).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_auto_proxy_with_timeout() -> Autoproxy {
|
||||||
|
let async_proxy = AsyncProxyQuery::get_auto_proxy().await;
|
||||||
|
|
||||||
|
// 转换为兼容的结构
|
||||||
|
Autoproxy {
|
||||||
|
enable: async_proxy.enable,
|
||||||
|
url: async_proxy.url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_sys_proxy_with_timeout() -> Sysproxy {
|
||||||
|
let async_proxy = AsyncProxyQuery::get_system_proxy().await;
|
||||||
|
|
||||||
|
// 转换为兼容的结构
|
||||||
|
Sysproxy {
|
||||||
|
enable: async_proxy.enable,
|
||||||
|
host: async_proxy.host,
|
||||||
|
port: async_proxy.port,
|
||||||
|
bypass: async_proxy.bypass,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的状态更新方法
|
||||||
|
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut ProxyState),
|
||||||
|
{
|
||||||
|
let mut state_guard = state.write();
|
||||||
|
update_fn(&mut state_guard);
|
||||||
|
state_guard.last_updated = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_proxy_config() -> ProxyConfig {
|
||||||
|
let verge_config = Config::verge();
|
||||||
|
let verge = verge_config.latest();
|
||||||
|
ProxyConfig {
|
||||||
|
sys_enabled: verge.enable_system_proxy.unwrap_or(false),
|
||||||
|
pac_enabled: verge.proxy_auto_config.unwrap_or(false),
|
||||||
|
guard_enabled: verge.enable_proxy_guard.unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_expected_pac_config() -> Autoproxy {
|
||||||
|
let verge_config = Config::verge();
|
||||||
|
let verge = verge_config.latest();
|
||||||
|
let (proxy_host, pac_port) = (
|
||||||
|
verge
|
||||||
|
.proxy_host
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "127.0.0.1".to_string()),
|
||||||
|
IVerge::get_singleton_port(),
|
||||||
|
);
|
||||||
|
Autoproxy {
|
||||||
|
enable: true,
|
||||||
|
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_expected_sys_proxy() -> Sysproxy {
|
||||||
|
let verge_config = Config::verge();
|
||||||
|
let verge = verge_config.latest();
|
||||||
|
let port = verge
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
|
let proxy_host = verge
|
||||||
|
.proxy_host
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||||
|
|
||||||
|
Sysproxy {
|
||||||
|
enable: true,
|
||||||
|
host: proxy_host,
|
||||||
|
port,
|
||||||
|
bypass: Self::get_bypass_config(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_bypass_config() -> String {
|
||||||
|
let verge_config = Config::verge();
|
||||||
|
let verge = verge_config.latest();
|
||||||
|
let use_default = verge.use_default_bypass.unwrap_or(true);
|
||||||
|
let custom_bypass = verge.system_proxy_bypass.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let default_bypass = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let default_bypass =
|
||||||
|
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let default_bypass = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||||
|
|
||||||
|
if custom_bypass.is_empty() {
|
||||||
|
default_bypass.to_string()
|
||||||
|
} else if use_default {
|
||||||
|
format!("{default_bypass},{custom_bypass}")
|
||||||
|
} else {
|
||||||
|
custom_bypass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_pac_proxy(expected_url: &str) {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
let new_autoproxy = Autoproxy {
|
||||||
|
enable: true,
|
||||||
|
url: expected_url.to_string(),
|
||||||
|
};
|
||||||
|
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
Self::execute_sysproxy_command(&["pac", expected_url]).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_sys_proxy(expected: &Sysproxy) {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
logging_error!(Type::System, true, expected.set_system_proxy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let address = format!("{}:{}", expected.host, expected.port);
|
||||||
|
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
async fn execute_sysproxy_command(args: &[&str]) {
|
||||||
|
use crate::utils::dirs;
|
||||||
|
#[allow(unused_imports)] // creation_flags必须
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
let binary_path = match dirs::service_path() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "获取服务路径失败: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||||
|
if !sysproxy_exe.exists() {
|
||||||
|
log::error!(target: "app", "sysproxy.exe 不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Command::new(sysproxy_exe)
|
||||||
|
.args(args)
|
||||||
|
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if !output.status.success() {
|
||||||
|
log::error!(target: "app", "执行sysproxy命令失败: {:?}", args);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
log::error!(target: "app", "sysproxy错误输出: {}", stderr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::debug!(target: "app", "成功执行sysproxy命令: {:?}", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "执行sysproxy命令出错: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -125,7 +125,7 @@ impl NotificationSystem {
|
|||||||
match serde_json::to_value((status, message)) {
|
match serde_json::to_value((status, message)) {
|
||||||
Ok(p) => ("verge://notice-message", Ok(p)),
|
Ok(p) => ("verge://notice-message", Ok(p)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to serialize NoticeMessage payload: {}", e);
|
log::error!("Failed to serialize NoticeMessage payload: {e}");
|
||||||
("verge://notice-message", Err(e))
|
("verge://notice-message", Err(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,11 +153,11 @@ impl NotificationSystem {
|
|||||||
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||||
// 记录成功发送的事件
|
// 记录成功发送的事件
|
||||||
if log::log_enabled!(log::Level::Debug) {
|
if log::log_enabled!(log::Level::Debug) {
|
||||||
log::debug!("Successfully emitted event: {}", event_name_str);
|
log::debug!("Successfully emitted event: {event_name_str}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to emit event {}: {}", event_name_str, e);
|
log::warn!("Failed to emit event {event_name_str}: {e}");
|
||||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||||
|
|
||||||
@@ -165,8 +165,7 @@ impl NotificationSystem {
|
|||||||
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
||||||
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Reached {} emit errors, entering emergency mode",
|
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
|
||||||
EMIT_ERROR_THRESHOLD
|
|
||||||
);
|
);
|
||||||
*system.emergency_mode.write() = true;
|
*system.emergency_mode.write() = true;
|
||||||
}
|
}
|
||||||
@@ -175,7 +174,7 @@ impl NotificationSystem {
|
|||||||
} else {
|
} else {
|
||||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||||
log::warn!("Skipped emitting event due to payload serialization error for {}", event_name_str);
|
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("No window found, skipping event emit.");
|
log::warn!("No window found, skipping event emit.");
|
||||||
@@ -215,7 +214,7 @@ impl NotificationSystem {
|
|||||||
match sender.send(event) {
|
match sender.send(event) {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to send event to notification queue: {:?}", e);
|
log::warn!("Failed to send event to notification queue: {e:?}");
|
||||||
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
*self.stats.last_error_time.write() = Some(Instant::now());
|
*self.stats.last_error_time.write() = Some(Instant::now());
|
||||||
false
|
false
|
||||||
@@ -243,7 +242,7 @@ impl NotificationSystem {
|
|||||||
log::info!("NotificationSystem worker thread joined successfully");
|
log::info!("NotificationSystem worker thread joined successfully");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("NotificationSystem worker thread join failed: {:?}", e);
|
log::error!("NotificationSystem worker thread join failed: {e:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +499,7 @@ impl Handle {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Err(e) = thread_result {
|
if let Err(e) = thread_result {
|
||||||
log::error!("Failed to spawn startup errors thread: {}", e);
|
log::error!("Failed to spawn startup errors thread: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
|
use crate::utils::notification::{notify_event, NotificationEvent};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config, core::handle, feat, logging, logging_error,
|
||||||
core::handle,
|
module::lightweight::entry_lightweight_mode, utils::logging::Type,
|
||||||
feat, logging, logging_error,
|
|
||||||
module::lightweight::entry_lightweight_mode,
|
|
||||||
process::AsyncHandler,
|
|
||||||
utils::{logging::Type, resolve},
|
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@@ -14,7 +11,7 @@ use tauri::Manager;
|
|||||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||||
|
|
||||||
pub struct Hotkey {
|
pub struct Hotkey {
|
||||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
current: Arc<Mutex<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hotkey {
|
impl Hotkey {
|
||||||
@@ -38,7 +35,6 @@ impl Hotkey {
|
|||||||
enable_global_hotkey
|
enable_global_hotkey
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果全局热键被禁用,则不注册热键
|
|
||||||
if !enable_global_hotkey {
|
if !enable_global_hotkey {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -138,14 +134,11 @@ impl Hotkey {
|
|||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let f = match func.trim() {
|
let app_handle_clone = app_handle.clone();
|
||||||
|
let f: Box<dyn Fn() + Send + Sync> = match func.trim() {
|
||||||
"open_or_close_dashboard" => {
|
"open_or_close_dashboard" => {
|
||||||
logging!(
|
let app_handle = app_handle_clone.clone();
|
||||||
debug,
|
Box::new(move || {
|
||||||
Type::Hotkey,
|
|
||||||
"Registering open_or_close_dashboard function"
|
|
||||||
);
|
|
||||||
|| {
|
|
||||||
logging!(
|
logging!(
|
||||||
debug,
|
debug,
|
||||||
Type::Hotkey,
|
Type::Hotkey,
|
||||||
@@ -153,94 +146,89 @@ impl Hotkey {
|
|||||||
"=== Hotkey Dashboard Window Operation Start ==="
|
"=== Hotkey Dashboard Window Operation Start ==="
|
||||||
);
|
);
|
||||||
|
|
||||||
// 检查是否在轻量模式下,如果是,需要同步处理
|
logging!(
|
||||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
info,
|
||||||
logging!(
|
Type::Hotkey,
|
||||||
info,
|
true,
|
||||||
Type::Hotkey,
|
"Using unified WindowManager for hotkey operation (bypass debounce)"
|
||||||
true,
|
);
|
||||||
"In lightweight mode, calling open_or_close_dashboard directly"
|
|
||||||
);
|
|
||||||
crate::feat::open_or_close_dashboard();
|
|
||||||
} else {
|
|
||||||
AsyncHandler::spawn(move || async move {
|
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"Toggle dashboard window visibility (async)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 检查窗口是否存在
|
crate::feat::open_or_close_dashboard_hotkey();
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
|
||||||
// 如果窗口可见,则隐藏
|
|
||||||
match window.is_visible() {
|
|
||||||
Ok(visible) => {
|
|
||||||
if visible {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Window,
|
|
||||||
true,
|
|
||||||
"Window is visible, hiding it"
|
|
||||||
);
|
|
||||||
let _ = window.hide();
|
|
||||||
} else {
|
|
||||||
// 如果窗口不可见,则显示
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Window,
|
|
||||||
true,
|
|
||||||
"Window is hidden, showing it"
|
|
||||||
);
|
|
||||||
if window.is_minimized().unwrap_or(false) {
|
|
||||||
let _ = window.unminimize();
|
|
||||||
}
|
|
||||||
let _ = window.show();
|
|
||||||
let _ = window.set_focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Window,
|
|
||||||
true,
|
|
||||||
"Failed to check window visibility: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
let _ = window.show();
|
|
||||||
let _ = window.set_focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果窗口不存在,创建一个新窗口
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Window,
|
|
||||||
true,
|
|
||||||
"Window does not exist, creating a new one"
|
|
||||||
);
|
|
||||||
resolve::create_window(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(
|
logging!(
|
||||||
debug,
|
debug,
|
||||||
Type::Hotkey,
|
Type::Hotkey,
|
||||||
"=== Hotkey Dashboard Window Operation End ==="
|
"=== Hotkey Dashboard Window Operation End ==="
|
||||||
);
|
);
|
||||||
}
|
notify_event(&app_handle, NotificationEvent::DashboardToggled);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"clash_mode_rule" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::change_clash_mode("rule".into());
|
||||||
|
notify_event(
|
||||||
|
&app_handle,
|
||||||
|
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"clash_mode_global" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::change_clash_mode("global".into());
|
||||||
|
notify_event(
|
||||||
|
&app_handle,
|
||||||
|
NotificationEvent::ClashModeChanged { mode: "Global" },
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"clash_mode_direct" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::change_clash_mode("direct".into());
|
||||||
|
notify_event(
|
||||||
|
&app_handle,
|
||||||
|
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"toggle_system_proxy" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::toggle_system_proxy();
|
||||||
|
notify_event(&app_handle, NotificationEvent::SystemProxyToggled);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"toggle_tun_mode" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::toggle_tun_mode(None);
|
||||||
|
notify_event(&app_handle, NotificationEvent::TunModeToggled);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"entry_lightweight_mode" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
entry_lightweight_mode();
|
||||||
|
notify_event(&app_handle, NotificationEvent::LightweightModeEntered);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"quit" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::quit();
|
||||||
|
notify_event(&app_handle, NotificationEvent::AppQuit);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
|
||||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
|
||||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
|
||||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
|
||||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
|
||||||
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
|
||||||
"quit" => || feat::quit(),
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
"hide" => || feat::hide(),
|
"hide" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::hide();
|
||||||
|
notify_event(&app_handle, NotificationEvent::AppHidden);
|
||||||
|
})
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
||||||
bail!("invalid function \"{func}\"");
|
bail!("invalid function \"{func}\"");
|
||||||
@@ -261,10 +249,8 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 直接执行函数,不做任何状态检查
|
|
||||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||||
|
|
||||||
// 获取全局热键状态
|
|
||||||
let is_enable_global_hotkey = Config::verge()
|
let is_enable_global_hotkey = Config::verge()
|
||||||
.latest()
|
.latest()
|
||||||
.enable_global_hotkey
|
.enable_global_hotkey
|
||||||
@@ -274,7 +260,6 @@ impl Hotkey {
|
|||||||
f();
|
f();
|
||||||
} else {
|
} else {
|
||||||
use crate::utils::window_manager::WindowManager;
|
use crate::utils::window_manager::WindowManager;
|
||||||
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
|
||||||
let is_visible = WindowManager::is_main_window_visible();
|
let is_visible = WindowManager::is_main_window_visible();
|
||||||
let is_focused = WindowManager::is_main_window_focused();
|
let is_focused = WindowManager::is_main_window_focused();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
pub mod async_proxy_query;
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
mod core;
|
mod core;
|
||||||
|
pub mod event_driven_proxy;
|
||||||
pub mod handle;
|
pub mod handle;
|
||||||
pub mod hotkey;
|
pub mod hotkey;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
@@ -10,4 +12,4 @@ pub mod timer;
|
|||||||
pub mod tray;
|
pub mod tray;
|
||||||
pub mod win_uwp;
|
pub mod win_uwp;
|
||||||
|
|
||||||
pub use self::{core::*, timer::Timer};
|
pub use self::{core::*, event_driven_proxy::EventDrivenProxyManager, timer::Timer};
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ pub async fn reinstall_service() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error = format!("failed to install service: {}", err);
|
let error = format!("failed to install service: {err}");
|
||||||
service_state.last_error = Some(error.clone());
|
service_state.last_error = Some(error.clone());
|
||||||
service_state.prefer_sidecar = true;
|
service_state.prefer_sidecar = true;
|
||||||
service_state.save()?;
|
service_state.save()?;
|
||||||
@@ -686,7 +686,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
|||||||
// 检查版本和可用性
|
// 检查版本和可用性
|
||||||
match check_service_version().await {
|
match check_service_version().await {
|
||||||
Ok(version) => {
|
Ok(version) => {
|
||||||
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
|
log::info!(target: "app", "服务版本检测:当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
|
||||||
/* logging!(
|
/* logging!(
|
||||||
info,
|
info,
|
||||||
Type::Service,
|
Type::Service,
|
||||||
@@ -698,8 +698,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
|||||||
|
|
||||||
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
||||||
if needs_reinstall {
|
if needs_reinstall {
|
||||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
|
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
|
||||||
version, REQUIRED_SERVICE_VERSION);
|
|
||||||
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装");
|
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装");
|
||||||
|
|
||||||
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
||||||
@@ -717,7 +716,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
|||||||
// 检查服务是否可用
|
// 检查服务是否可用
|
||||||
match is_service_available().await {
|
match is_service_available().await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::info!(target: "app", "服务正在运行但版本检查失败: {}", err);
|
log::info!(target: "app", "服务正在运行但版本检查失败: {err}");
|
||||||
/* logging!(
|
/* logging!(
|
||||||
info,
|
info,
|
||||||
Type::Service,
|
Type::Service,
|
||||||
@@ -827,8 +826,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
// 先检查服务版本,不受冷却期限制
|
// 先检查服务版本,不受冷却期限制
|
||||||
let version_check = match check_service_version().await {
|
let version_check = match check_service_version().await {
|
||||||
Ok(version) => {
|
Ok(version) => {
|
||||||
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
|
log::info!(target: "app", "检测到服务版本: {version}, 要求版本: {REQUIRED_SERVICE_VERSION}");
|
||||||
version, REQUIRED_SERVICE_VERSION);
|
|
||||||
|
|
||||||
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
||||||
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
||||||
@@ -839,7 +837,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(target: "app", "无法获取服务版本: {}", err);
|
log::warn!(target: "app", "无法获取服务版本: {err}");
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -865,7 +863,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
|
|
||||||
log::info!(target: "app", "开始重装服务");
|
log::info!(target: "app", "开始重装服务");
|
||||||
if let Err(err) = reinstall_service().await {
|
if let Err(err) = reinstall_service().await {
|
||||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
log::warn!(target: "app", "服务重装失败: {err}");
|
||||||
|
|
||||||
log::info!(target: "app", "尝试使用现有服务");
|
log::info!(target: "app", "尝试使用现有服务");
|
||||||
return start_with_existing_service(config_file).await;
|
return start_with_existing_service(config_file).await;
|
||||||
@@ -884,7 +882,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(target: "app", "服务检查失败: {}", err);
|
log::warn!(target: "app", "服务检查失败: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,7 +891,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
log::info!(target: "app", "服务需要重装");
|
log::info!(target: "app", "服务需要重装");
|
||||||
|
|
||||||
if let Err(err) = reinstall_service().await {
|
if let Err(err) = reinstall_service().await {
|
||||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
log::warn!(target: "app", "服务重装失败: {err}");
|
||||||
bail!("Failed to reinstall service: {}", err);
|
bail!("Failed to reinstall service: {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,7 +984,7 @@ pub async fn force_reinstall_service() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "强制重装服务失败: {}", err);
|
log::error!(target: "app", "强制重装服务失败: {err}");
|
||||||
bail!("强制重装服务失败: {}", err)
|
bail!("强制重装服务失败: {}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ pub async fn send_ipc_request(
|
|||||||
|
|
||||||
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
|
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
|
||||||
|
|
||||||
let command_type = format!("{:?}", command);
|
let command_type = format!("{command:?}");
|
||||||
|
|
||||||
let request = match create_signed_request(command, payload) {
|
let request = match create_signed_request(command, payload) {
|
||||||
Ok(req) => req,
|
Ok(req) => req,
|
||||||
|
|||||||
@@ -2,25 +2,21 @@
|
|||||||
use crate::utils::autostart as startup_shortcut;
|
use crate::utils::autostart as startup_shortcut;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::handle::Handle,
|
core::{handle::Handle, EventDrivenProxyManager},
|
||||||
logging, logging_error,
|
logging, logging_error,
|
||||||
process::AsyncHandler,
|
|
||||||
utils::logging::Type,
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
use sysproxy::{Autoproxy, Sysproxy};
|
use sysproxy::{Autoproxy, Sysproxy};
|
||||||
use tauri::async_runtime::Mutex as TokioMutex;
|
use tauri::async_runtime::Mutex as TokioMutex;
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
use tokio::time::{sleep, Duration};
|
|
||||||
|
|
||||||
pub struct Sysopt {
|
pub struct Sysopt {
|
||||||
update_sysproxy: Arc<TokioMutex<bool>>,
|
update_sysproxy: Arc<TokioMutex<bool>>,
|
||||||
reset_sysproxy: Arc<TokioMutex<bool>>,
|
reset_sysproxy: Arc<TokioMutex<bool>>,
|
||||||
/// record whether the guard async is running or not
|
|
||||||
guard_state: Arc<Mutex<bool>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -47,7 +43,7 @@ fn get_bypass() -> String {
|
|||||||
if custom_bypass.is_empty() {
|
if custom_bypass.is_empty() {
|
||||||
DEFAULT_BYPASS.to_string()
|
DEFAULT_BYPASS.to_string()
|
||||||
} else if use_default {
|
} else if use_default {
|
||||||
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
|
format!("{DEFAULT_BYPASS},{custom_bypass}")
|
||||||
} else {
|
} else {
|
||||||
custom_bypass
|
custom_bypass
|
||||||
}
|
}
|
||||||
@@ -59,12 +55,15 @@ impl Sysopt {
|
|||||||
SYSOPT.get_or_init(|| Sysopt {
|
SYSOPT.get_or_init(|| Sysopt {
|
||||||
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||||
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||||
guard_state: Arc::new(false.into()),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
||||||
self.guard_proxy();
|
// 使用事件驱动代理管理器
|
||||||
|
let proxy_manager = EventDrivenProxyManager::global();
|
||||||
|
proxy_manager.notify_app_started();
|
||||||
|
|
||||||
|
log::info!(target: "app", "已启用事件驱动代理守卫");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,12 +100,14 @@ impl Sysopt {
|
|||||||
};
|
};
|
||||||
let mut auto = Autoproxy {
|
let mut auto = Autoproxy {
|
||||||
enable: false,
|
enable: false,
|
||||||
url: format!("http://{}:{}/commands/pac", proxy_host, pac_port),
|
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !sys_enable {
|
if !sys_enable {
|
||||||
sys.set_system_proxy()?;
|
sys.set_system_proxy()?;
|
||||||
auto.set_auto_proxy()?;
|
auto.set_auto_proxy()?;
|
||||||
|
let proxy_manager = EventDrivenProxyManager::global();
|
||||||
|
proxy_manager.notify_config_changed();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +116,8 @@ impl Sysopt {
|
|||||||
auto.enable = true;
|
auto.enable = true;
|
||||||
sys.set_system_proxy()?;
|
sys.set_system_proxy()?;
|
||||||
auto.set_auto_proxy()?;
|
auto.set_auto_proxy()?;
|
||||||
|
let proxy_manager = EventDrivenProxyManager::global();
|
||||||
|
proxy_manager.notify_config_changed();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,13 +126,18 @@ impl Sysopt {
|
|||||||
sys.enable = true;
|
sys.enable = true;
|
||||||
auto.set_auto_proxy()?;
|
auto.set_auto_proxy()?;
|
||||||
sys.set_system_proxy()?;
|
sys.set_system_proxy()?;
|
||||||
|
let proxy_manager = EventDrivenProxyManager::global();
|
||||||
|
proxy_manager.notify_config_changed();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
if !sys_enable {
|
if !sys_enable {
|
||||||
return self.reset_sysproxy().await;
|
let result = self.reset_sysproxy().await;
|
||||||
|
let proxy_manager = EventDrivenProxyManager::global();
|
||||||
|
proxy_manager.notify_config_changed();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
use crate::{core::handle::Handle, utils::dirs};
|
use crate::{core::handle::Handle, utils::dirs};
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
@@ -169,6 +177,8 @@ impl Sysopt {
|
|||||||
bail!("sysproxy exe run failed");
|
bail!("sysproxy exe run failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let proxy_manager = EventDrivenProxyManager::global();
|
||||||
|
proxy_manager.notify_config_changed();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -180,7 +190,16 @@ impl Sysopt {
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
|
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
|
||||||
let mut autoproxy = Autoproxy::get_auto_proxy()?;
|
let mut autoproxy = match Autoproxy::get_auto_proxy() {
|
||||||
|
Ok(ap) => ap,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
|
||||||
|
Autoproxy {
|
||||||
|
enable: false,
|
||||||
|
url: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
sysproxy.enable = false;
|
sysproxy.enable = false;
|
||||||
autoproxy.enable = false;
|
autoproxy.enable = false;
|
||||||
autoproxy.set_auto_proxy()?;
|
autoproxy.set_auto_proxy()?;
|
||||||
@@ -286,176 +305,13 @@ impl Sysopt {
|
|||||||
|
|
||||||
match autostart_manager.is_enabled() {
|
match autostart_manager.is_enabled() {
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
log::info!(target: "app", "Auto launch status: {}", status);
|
log::info!(target: "app", "Auto launch status: {status}");
|
||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to get auto launch status: {}", e);
|
log::error!(target: "app", "Failed to get auto launch status: {e}");
|
||||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn guard_proxy(&self) {
|
|
||||||
let _lock = self.guard_state.lock();
|
|
||||||
|
|
||||||
AsyncHandler::spawn(move || async move {
|
|
||||||
// default duration is 10s
|
|
||||||
let mut wait_secs = 10u64;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
sleep(Duration::from_secs(wait_secs)).await;
|
|
||||||
|
|
||||||
let (enable, guard, guard_duration, pac, proxy_host) = {
|
|
||||||
let verge = Config::verge();
|
|
||||||
let verge = verge.latest();
|
|
||||||
(
|
|
||||||
verge.enable_system_proxy.unwrap_or(false),
|
|
||||||
verge.enable_proxy_guard.unwrap_or(false),
|
|
||||||
verge.proxy_guard_duration.unwrap_or(10),
|
|
||||||
verge.proxy_auto_config.unwrap_or(false),
|
|
||||||
verge
|
|
||||||
.proxy_host
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| String::from("127.0.0.1")),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// stop loop
|
|
||||||
if !enable || !guard {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update duration
|
|
||||||
wait_secs = guard_duration;
|
|
||||||
|
|
||||||
log::debug!(target: "app", "try to guard the system proxy");
|
|
||||||
|
|
||||||
// 获取期望的代理端口
|
|
||||||
let port = Config::verge()
|
|
||||||
.latest()
|
|
||||||
.verge_mixed_port
|
|
||||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
|
||||||
let pac_port = IVerge::get_singleton_port();
|
|
||||||
let bypass = get_bypass();
|
|
||||||
|
|
||||||
// 检查系统代理配置
|
|
||||||
if pac {
|
|
||||||
// 检查 PAC 代理设置
|
|
||||||
let expected_url = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
|
||||||
let autoproxy = match Autoproxy::get_auto_proxy() {
|
|
||||||
Ok(ap) => ap,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "failed to get the auto proxy: {}", e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查自动代理是否启用且URL是否正确
|
|
||||||
if !autoproxy.enable || autoproxy.url != expected_url {
|
|
||||||
log::info!(target: "app", "auto proxy settings changed, restoring...");
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
let new_autoproxy = Autoproxy {
|
|
||||||
enable: true,
|
|
||||||
url: expected_url,
|
|
||||||
};
|
|
||||||
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use crate::{core::handle::Handle, utils::dirs};
|
|
||||||
use tauri_plugin_shell::ShellExt;
|
|
||||||
|
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
|
||||||
let binary_path = match dirs::service_path() {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "failed to get service path: {}", e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
|
||||||
if !sysproxy_exe.exists() {
|
|
||||||
log::error!(target: "app", "sysproxy.exe not found");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let shell = app_handle.shell();
|
|
||||||
let output = shell
|
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
|
||||||
.args(["pac", expected_url.as_str()])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
log::error!(target: "app", "failed to set auto proxy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 检查常规系统代理设置
|
|
||||||
let sysproxy = match Sysproxy::get_system_proxy() {
|
|
||||||
Ok(sp) => sp,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "failed to get the system proxy: {}", e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查系统代理是否启用且配置是否匹配
|
|
||||||
if !sysproxy.enable || sysproxy.host != proxy_host || sysproxy.port != port {
|
|
||||||
log::info!(target: "app", "system proxy settings changed, restoring...");
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
let new_sysproxy = Sysproxy {
|
|
||||||
enable: true,
|
|
||||||
host: proxy_host.clone(),
|
|
||||||
port,
|
|
||||||
bypass: bypass.clone(),
|
|
||||||
};
|
|
||||||
logging_error!(Type::System, true, new_sysproxy.set_system_proxy());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use crate::{core::handle::Handle, utils::dirs};
|
|
||||||
use tauri_plugin_shell::ShellExt;
|
|
||||||
|
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
|
||||||
let binary_path = match dirs::service_path() {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "failed to get service path: {}", e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
|
||||||
if !sysproxy_exe.exists() {
|
|
||||||
log::error!(target: "app", "sysproxy.exe not found");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let address = format!("{}:{}", proxy_host, port);
|
|
||||||
let shell = app_handle.shell();
|
|
||||||
let output = shell
|
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
|
||||||
.args(["global", address.as_str(), bypass.as_ref()])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
log::error!(target: "app", "failed to set system proxy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use futures::StreamExt;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub use speed_rate::{SpeedRate, Traffic};
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
sync::atomic::{AtomicBool, Ordering},
|
sync::atomic::{AtomicBool, Ordering},
|
||||||
@@ -31,20 +23,37 @@ use tauri::{
|
|||||||
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
||||||
AppHandle, Wry,
|
AppHandle, Wry,
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use tokio::sync::broadcast;
|
|
||||||
|
|
||||||
use super::handle;
|
use super::handle;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct TrayState {}
|
struct TrayState {}
|
||||||
|
|
||||||
|
// 托盘点击防抖机制
|
||||||
|
static TRAY_CLICK_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
|
||||||
|
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
||||||
|
|
||||||
|
fn get_tray_click_debounce() -> &'static Mutex<Instant> {
|
||||||
|
TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_handle_tray_click() -> bool {
|
||||||
|
let debounce_lock = get_tray_click_debounce();
|
||||||
|
let mut last_click = debounce_lock.lock();
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
||||||
|
*last_click = now;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
|
||||||
|
now.duration_since(*last_click).as_millis());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub struct Tray {
|
pub struct Tray {
|
||||||
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
|
||||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
|
||||||
is_subscribed: Arc<RwLock<bool>>,
|
|
||||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
|
||||||
last_menu_update: Mutex<Option<Instant>>,
|
last_menu_update: Mutex<Option<Instant>>,
|
||||||
menu_updating: AtomicBool,
|
menu_updating: AtomicBool,
|
||||||
}
|
}
|
||||||
@@ -105,7 +114,7 @@ impl TrayState {
|
|||||||
if tray_icon_colorful == "monochrome" {
|
if tray_icon_colorful == "monochrome" {
|
||||||
(
|
(
|
||||||
false,
|
false,
|
||||||
include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
|
include_bytes!("../../../icons/tray-icon-sys-mono-new.ico").to_vec(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
@@ -139,7 +148,7 @@ impl TrayState {
|
|||||||
if tray_icon_colorful == "monochrome" {
|
if tray_icon_colorful == "monochrome" {
|
||||||
(
|
(
|
||||||
false,
|
false,
|
||||||
include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
|
include_bytes!("../../../icons/tray-icon-tun-mono-new.ico").to_vec(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
@@ -164,10 +173,6 @@ impl Tray {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
return TRAY.get_or_init(|| Tray {
|
return TRAY.get_or_init(|| Tray {
|
||||||
speed_rate: Arc::new(Mutex::new(None)),
|
|
||||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
|
||||||
is_subscribed: Arc::new(RwLock::new(false)),
|
|
||||||
rate_cache: Arc::new(Mutex::new(None)),
|
|
||||||
last_menu_update: Mutex::new(None),
|
last_menu_update: Mutex::new(None),
|
||||||
menu_updating: AtomicBool::new(false),
|
menu_updating: AtomicBool::new(false),
|
||||||
});
|
});
|
||||||
@@ -180,11 +185,6 @@ impl Tray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self) -> Result<()> {
|
pub fn init(&self) -> Result<()> {
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
let mut speed_rate = self.speed_rate.lock();
|
|
||||||
*speed_rate = Some(SpeedRate::new());
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ impl Tray {
|
|||||||
|
|
||||||
/// 更新托盘图标
|
/// 更新托盘图标
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
|
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||||
let app_handle = match handle::Handle::global().app_handle() {
|
let app_handle = match handle::Handle::global().app_handle() {
|
||||||
Some(handle) => handle,
|
Some(handle) => handle,
|
||||||
None => {
|
None => {
|
||||||
@@ -312,55 +312,18 @@ impl Tray {
|
|||||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||||
|
|
||||||
let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||||
(true, true) => TrayState::get_tun_tray_icon(),
|
(true, true) => TrayState::get_tun_tray_icon(),
|
||||||
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
||||||
(false, true) => TrayState::get_tun_tray_icon(),
|
(false, true) => TrayState::get_tun_tray_icon(),
|
||||||
(false, false) => TrayState::get_common_tray_icon(),
|
(false, false) => TrayState::get_common_tray_icon(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false);
|
|
||||||
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
|
||||||
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||||
let is_colorful = colorful == "colorful";
|
let is_colorful = colorful == "colorful";
|
||||||
|
|
||||||
if !enable_tray_speed {
|
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
let _ = tray.set_icon_as_template(!is_colorful);
|
||||||
let _ = tray.set_icon_as_template(!is_colorful);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let rate = if let Some(rate) = rate {
|
|
||||||
Some(rate)
|
|
||||||
} else {
|
|
||||||
let guard = self.speed_rate.lock();
|
|
||||||
if let Some(guard) = guard.as_ref() {
|
|
||||||
if let Some(rate) = guard.get_curent_rate() {
|
|
||||||
Some(rate)
|
|
||||||
} else {
|
|
||||||
Some(Rate::default())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some(Rate::default())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut rate_guard = self.rate_cache.lock();
|
|
||||||
if *rate_guard != rate {
|
|
||||||
*rate_guard = rate;
|
|
||||||
|
|
||||||
let bytes = if enable_tray_icon {
|
|
||||||
Some(icon_bytes)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let rate = rate_guard.as_ref();
|
|
||||||
if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) {
|
|
||||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
|
||||||
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,155 +438,9 @@ impl Tray {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 订阅流量数据
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub async fn subscribe_traffic(&self) -> Result<()> {
|
|
||||||
log::info!(target: "app", "subscribe traffic");
|
|
||||||
|
|
||||||
// 如果已经订阅,先取消订阅
|
|
||||||
if *self.is_subscribed.read() {
|
|
||||||
self.unsubscribe_traffic();
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (shutdown_tx, shutdown_rx) = broadcast::channel(3);
|
|
||||||
*self.shutdown_tx.write() = Some(shutdown_tx);
|
|
||||||
*self.is_subscribed.write() = true;
|
|
||||||
|
|
||||||
let speed_rate = Arc::clone(&self.speed_rate);
|
|
||||||
let is_subscribed = Arc::clone(&self.is_subscribed);
|
|
||||||
|
|
||||||
// 使用单线程防止阻塞主线程
|
|
||||||
std::thread::Builder::new()
|
|
||||||
.name("traffic-monitor".into())
|
|
||||||
.spawn(move || {
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.expect("Failed to build tokio runtime for traffic monitor");
|
|
||||||
// 在单独的运行时中执行异步任务
|
|
||||||
rt.block_on(async move {
|
|
||||||
let mut shutdown = shutdown_rx;
|
|
||||||
let speed_rate = speed_rate.clone();
|
|
||||||
let is_subscribed = is_subscribed.clone();
|
|
||||||
let mut consecutive_errors = 0;
|
|
||||||
let max_consecutive_errors = 5;
|
|
||||||
|
|
||||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
|
|
||||||
|
|
||||||
'outer: loop {
|
|
||||||
if !*is_subscribed.read() {
|
|
||||||
log::info!(target: "app", "Traffic subscription has been cancelled");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
match tokio::time::timeout(
|
|
||||||
std::time::Duration::from_secs(5),
|
|
||||||
Traffic::get_traffic_stream()
|
|
||||||
).await {
|
|
||||||
Ok(stream_result) => {
|
|
||||||
match stream_result {
|
|
||||||
Ok(mut stream) => {
|
|
||||||
consecutive_errors = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
traffic_result = stream.next() => {
|
|
||||||
match traffic_result {
|
|
||||||
Some(Ok(traffic)) => {
|
|
||||||
if let Ok(Some(rate)) = tokio::time::timeout(
|
|
||||||
std::time::Duration::from_millis(50),
|
|
||||||
async {
|
|
||||||
let guard = speed_rate.try_lock();
|
|
||||||
if let Some(guard) = guard {
|
|
||||||
if let Some(sr) = guard.as_ref() {
|
|
||||||
sr.update_and_check_changed(traffic.up, traffic.down)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).await {
|
|
||||||
let _ = tokio::time::timeout(
|
|
||||||
std::time::Duration::from_millis(100),
|
|
||||||
async { let _ = Tray::global().update_icon(Some(rate)); }
|
|
||||||
).await;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Err(e)) => {
|
|
||||||
log::error!(target: "app", "Traffic stream error: {}", e);
|
|
||||||
consecutive_errors += 1;
|
|
||||||
if consecutive_errors >= max_consecutive_errors {
|
|
||||||
log::error!(target: "app", "Too many errors, reconnecting traffic stream");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
log::info!(target: "app", "Traffic stream ended, reconnecting");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ = shutdown.recv() => {
|
|
||||||
log::info!(target: "app", "Received shutdown signal for traffic stream");
|
|
||||||
break 'outer;
|
|
||||||
},
|
|
||||||
_ = interval.tick() => {
|
|
||||||
if !*is_subscribed.read() {
|
|
||||||
log::info!(target: "app", "Traffic monitor detected subscription cancelled");
|
|
||||||
break 'outer;
|
|
||||||
}
|
|
||||||
log::debug!(target: "app", "Traffic subscription periodic health check");
|
|
||||||
},
|
|
||||||
_ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
|
|
||||||
log::info!(target: "app", "Traffic stream max active time reached, reconnecting");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "Failed to get traffic stream: {}", e);
|
|
||||||
consecutive_errors += 1;
|
|
||||||
if consecutive_errors >= max_consecutive_errors {
|
|
||||||
log::error!(target: "app", "Too many consecutive errors, pausing traffic monitoring");
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
|
||||||
consecutive_errors = 0;
|
|
||||||
} else {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
log::error!(target: "app", "Traffic stream initialization timed out");
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*is_subscribed.read() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log::info!(target: "app", "Traffic subscription thread terminated");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.expect("Failed to spawn traffic monitor thread");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 取消订阅 traffic 数据
|
/// 取消订阅 traffic 数据
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn unsubscribe_traffic(&self) {
|
pub fn unsubscribe_traffic(&self) {}
|
||||||
log::info!(target: "app", "unsubscribe traffic");
|
|
||||||
*self.is_subscribed.write() = false;
|
|
||||||
if let Some(tx) = self.shutdown_tx.write().take() {
|
|
||||||
drop(tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
||||||
log::info!(target: "app", "正在从AppHandle创建系统托盘");
|
log::info!(target: "app", "正在从AppHandle创建系统托盘");
|
||||||
@@ -656,7 +473,7 @@ impl Tray {
|
|||||||
tray.on_tray_icon_event(|_, event| {
|
tray.on_tray_icon_event(|_, event| {
|
||||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||||
log::debug!(target: "app","tray event: {:?}", tray_event);
|
log::debug!(target: "app","tray event: {tray_event:?}");
|
||||||
|
|
||||||
if let TrayIconEvent::Click {
|
if let TrayIconEvent::Click {
|
||||||
button: MouseButton::Left,
|
button: MouseButton::Left,
|
||||||
@@ -664,6 +481,11 @@ impl Tray {
|
|||||||
..
|
..
|
||||||
} = event
|
} = event
|
||||||
{
|
{
|
||||||
|
// 添加防抖检查,防止快速连击
|
||||||
|
if !should_handle_tray_click() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match tray_event.as_str() {
|
match tray_event.as_str() {
|
||||||
"system_proxy" => feat::toggle_system_proxy(),
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
"tun_mode" => feat::toggle_tun_mode(None),
|
"tun_mode" => feat::toggle_tun_mode(None),
|
||||||
@@ -675,7 +497,7 @@ impl Tray {
|
|||||||
crate::module::lightweight::exit_lightweight_mode();
|
crate::module::lightweight::exit_lightweight_mode();
|
||||||
}
|
}
|
||||||
let result = WindowManager::show_main_window();
|
let result = WindowManager::show_main_window();
|
||||||
log::info!(target: "app", "窗口显示结果: {:?}", result);
|
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -736,7 +558,7 @@ fn create_tray_menu(
|
|||||||
.is_current_profile_index(profile_uid.to_string());
|
.is_current_profile_index(profile_uid.to_string());
|
||||||
CheckMenuItem::with_id(
|
CheckMenuItem::with_id(
|
||||||
app_handle,
|
app_handle,
|
||||||
format!("profiles_{}", profile_uid),
|
format!("profiles_{profile_uid}"),
|
||||||
t(profile_name),
|
t(profile_name),
|
||||||
true,
|
true,
|
||||||
is_current_profile,
|
is_current_profile,
|
||||||
@@ -949,14 +771,17 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
"open_window" => {
|
"open_window" => {
|
||||||
use crate::utils::window_manager::WindowManager;
|
use crate::utils::window_manager::WindowManager;
|
||||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
||||||
// 如果在轻量模式中,先退出轻量模式
|
|
||||||
|
if !should_handle_tray_click() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||||
log::info!(target: "app", "当前在轻量模式,正在退出");
|
log::info!(target: "app", "当前在轻量模式,正在退出");
|
||||||
crate::module::lightweight::exit_lightweight_mode();
|
crate::module::lightweight::exit_lightweight_mode();
|
||||||
}
|
}
|
||||||
// 使用统一的窗口管理器显示窗口
|
|
||||||
let result = WindowManager::show_main_window();
|
let result = WindowManager::show_main_window();
|
||||||
log::info!(target: "app", "窗口显示结果: {:?}", result);
|
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||||
}
|
}
|
||||||
"system_proxy" => {
|
"system_proxy" => {
|
||||||
feat::toggle_system_proxy();
|
feat::toggle_system_proxy();
|
||||||
@@ -977,7 +802,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
"restart_clash" => feat::restart_clash_core(),
|
"restart_clash" => feat::restart_clash_core(),
|
||||||
"restart_app" => feat::restart_app(),
|
"restart_app" => feat::restart_app(),
|
||||||
"entry_lightweight_mode" => {
|
"entry_lightweight_mode" => {
|
||||||
// 处理轻量模式的切换
|
if !should_handle_tray_click() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
|
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
|
||||||
if was_lightweight {
|
if was_lightweight {
|
||||||
crate::module::lightweight::exit_lightweight_mode();
|
crate::module::lightweight::exit_lightweight_mode();
|
||||||
@@ -985,11 +813,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
crate::module::lightweight::entry_lightweight_mode();
|
crate::module::lightweight::entry_lightweight_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退出轻量模式后显示主窗口
|
|
||||||
if was_lightweight {
|
if was_lightweight {
|
||||||
use crate::utils::window_manager::WindowManager;
|
use crate::utils::window_manager::WindowManager;
|
||||||
let result = WindowManager::show_main_window();
|
let result = WindowManager::show_main_window();
|
||||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {:?}", result);
|
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"quit" => {
|
"quit" => {
|
||||||
@@ -1002,8 +829,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一调用状态更新
|
|
||||||
if let Err(e) = Tray::global().update_all_states() {
|
if let Err(e) = Tray::global().update_all_states() {
|
||||||
log::warn!(target: "app", "更新托盘状态失败: {}", e);
|
log::warn!(target: "app", "更新托盘状态失败: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,336 +1 @@
|
|||||||
use crate::{
|
|
||||||
module::mihomo::{MihomoManager, Rate},
|
|
||||||
utils::help::format_bytes_speed,
|
|
||||||
};
|
|
||||||
use ab_glyph::FontArc;
|
|
||||||
use anyhow::Result;
|
|
||||||
use futures::Stream;
|
|
||||||
use image::{GenericImageView, Rgba, RgbaImage};
|
|
||||||
use imageproc::drawing::draw_text_mut;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::{io::Cursor, sync::Arc};
|
|
||||||
use tokio_tungstenite::tungstenite::http;
|
|
||||||
use tungstenite::client::IntoClientRequest;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SpeedRate {
|
|
||||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
|
||||||
last_update: Arc<Mutex<std::time::Instant>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SpeedRate {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
|
|
||||||
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option<Rate> {
|
|
||||||
let mut rates = self.rate.lock();
|
|
||||||
let mut last_update = self.last_update.lock();
|
|
||||||
let now = std::time::Instant::now();
|
|
||||||
|
|
||||||
// 限制更新频率为每秒最多2次(500ms)
|
|
||||||
if now.duration_since(*last_update).as_millis() < 500 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (current, previous) = &mut *rates;
|
|
||||||
|
|
||||||
// Avoid unnecessary float conversions for small value checks
|
|
||||||
let should_update = if current.up < 1000 && down < 1000 {
|
|
||||||
// For small values, always update to ensure accuracy
|
|
||||||
current.up != up || current.down != down
|
|
||||||
} else {
|
|
||||||
// For larger values, use integer math to check for >5% change
|
|
||||||
// Multiply by 20 instead of dividing by 0.05 to avoid floating point
|
|
||||||
let up_threshold = current.up / 20;
|
|
||||||
let down_threshold = current.down / 20;
|
|
||||||
|
|
||||||
(up > current.up && up - current.up > up_threshold)
|
|
||||||
|| (up < current.up && current.up - up > up_threshold)
|
|
||||||
|| (down > current.down && down - current.down > down_threshold)
|
|
||||||
|| (down < current.down && current.down - down > down_threshold)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !should_update {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
*previous = current.clone();
|
|
||||||
current.up = up;
|
|
||||||
current.down = down;
|
|
||||||
*last_update = now;
|
|
||||||
|
|
||||||
if previous != current {
|
|
||||||
Some(current.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_curent_rate(&self) -> Option<Rate> {
|
|
||||||
let rates = self.rate.lock();
|
|
||||||
let (current, _) = &*rates;
|
|
||||||
Some(current.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分离图标加载和速率渲染
|
|
||||||
pub fn add_speed_text(
|
|
||||||
is_custom_icon: bool,
|
|
||||||
icon_bytes: Option<Vec<u8>>,
|
|
||||||
rate: Option<&Rate>,
|
|
||||||
) -> Result<Vec<u8>> {
|
|
||||||
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
|
|
||||||
|
|
||||||
let (mut icon_width, mut icon_height) = (0, 256);
|
|
||||||
let icon_image = if let Some(bytes) = icon_bytes.clone() {
|
|
||||||
let icon_image = image::load_from_memory(&bytes)?;
|
|
||||||
icon_width = icon_image.width();
|
|
||||||
icon_height = icon_image.height();
|
|
||||||
icon_image
|
|
||||||
} else {
|
|
||||||
// 返回一个空的 RGBA 图像
|
|
||||||
image::DynamicImage::new_rgba8(0, 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
let total_width = match (is_custom_icon, icon_bytes.is_some()) {
|
|
||||||
(true, true) => 510,
|
|
||||||
(true, false) => 740,
|
|
||||||
(false, false) => 740,
|
|
||||||
(false, true) => icon_width + 740,
|
|
||||||
};
|
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "icon_height: {}, icon_wight: {}, total_width: {}",
|
|
||||||
// icon_height, icon_width, total_width
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 创建新的透明画布
|
|
||||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
|
||||||
|
|
||||||
// 将原始图标绘制到新画布的左侧
|
|
||||||
if icon_bytes.is_some() {
|
|
||||||
for y in 0..icon_height {
|
|
||||||
for x in 0..icon_width {
|
|
||||||
let pixel = icon_image.get_pixel(x, y);
|
|
||||||
combined_image.put_pixel(x, y, pixel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_colorful = if let Some(bytes) = icon_bytes.clone() {
|
|
||||||
!crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
// 选择文本颜色
|
|
||||||
let (text_color, shadow_color) = if is_colorful {
|
|
||||||
(
|
|
||||||
Rgba([144u8, 144u8, 144u8, 255u8]),
|
|
||||||
// Rgba([255u8, 255u8, 255u8, 128u8]),
|
|
||||||
Rgba([0u8, 0u8, 0u8, 128u8]),
|
|
||||||
)
|
|
||||||
// (
|
|
||||||
// Rgba([160u8, 160u8, 160u8, 255u8]),
|
|
||||||
// // Rgba([255u8, 255u8, 255u8, 128u8]),
|
|
||||||
// Rgba([0u8, 0u8, 0u8, 255u8]),
|
|
||||||
// )
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
Rgba([255u8, 255u8, 255u8, 255u8]),
|
|
||||||
Rgba([0u8, 0u8, 0u8, 128u8]),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
// 减小字体大小以适应文本区域
|
|
||||||
let font_data = include_bytes!("../../../assets/fonts/SF-Pro.ttf");
|
|
||||||
let font = FontArc::try_from_vec(font_data.to_vec()).unwrap();
|
|
||||||
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
|
|
||||||
let scale = ab_glyph::PxScale::from(font_size);
|
|
||||||
|
|
||||||
// 使用更简洁的速率格式
|
|
||||||
let up_text = format!("↑ {}", format_bytes_speed(rate.up));
|
|
||||||
let down_text = format!("↓ {}", format_bytes_speed(rate.down));
|
|
||||||
|
|
||||||
// For test rate display
|
|
||||||
// let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024));
|
|
||||||
|
|
||||||
// 计算文本位置,确保垂直间距合适
|
|
||||||
// 修改文本位置为居右显示
|
|
||||||
// 计算右对齐的文本位置
|
|
||||||
// let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
|
|
||||||
// let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
|
|
||||||
// let up_text_x = total_width - up_text_width;
|
|
||||||
// let down_text_x = total_width - down_text_width;
|
|
||||||
|
|
||||||
// 计算左对齐的文本位置
|
|
||||||
let (up_text_x, down_text_x) = {
|
|
||||||
if is_custom_icon || icon_bytes.is_some() {
|
|
||||||
let text_left_offset = 30;
|
|
||||||
let left_begin = icon_width + text_left_offset;
|
|
||||||
(left_begin, left_begin)
|
|
||||||
} else {
|
|
||||||
(icon_width, icon_width)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
|
|
||||||
let text_height = font_size as i32;
|
|
||||||
let total_text_height = text_height * 2;
|
|
||||||
let up_y = (icon_height as i32 - total_text_height) / 2;
|
|
||||||
let down_y = up_y + text_height;
|
|
||||||
|
|
||||||
// 绘制速率文本(先阴影后文字)
|
|
||||||
let shadow_offset = 1;
|
|
||||||
|
|
||||||
// 绘制上行速率
|
|
||||||
draw_text_mut(
|
|
||||||
&mut combined_image,
|
|
||||||
shadow_color,
|
|
||||||
up_text_x as i32 + shadow_offset,
|
|
||||||
up_y + shadow_offset,
|
|
||||||
scale,
|
|
||||||
&font,
|
|
||||||
&up_text,
|
|
||||||
);
|
|
||||||
draw_text_mut(
|
|
||||||
&mut combined_image,
|
|
||||||
text_color,
|
|
||||||
up_text_x as i32,
|
|
||||||
up_y,
|
|
||||||
scale,
|
|
||||||
&font,
|
|
||||||
&up_text,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 绘制下行速率
|
|
||||||
draw_text_mut(
|
|
||||||
&mut combined_image,
|
|
||||||
shadow_color,
|
|
||||||
down_text_x as i32 + shadow_offset,
|
|
||||||
down_y + shadow_offset,
|
|
||||||
scale,
|
|
||||||
&font,
|
|
||||||
&down_text,
|
|
||||||
);
|
|
||||||
draw_text_mut(
|
|
||||||
&mut combined_image,
|
|
||||||
text_color,
|
|
||||||
down_text_x as i32,
|
|
||||||
down_y,
|
|
||||||
scale,
|
|
||||||
&font,
|
|
||||||
&down_text,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 将结果转换为 PNG 数据
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
|
|
||||||
Ok(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Traffic {
|
|
||||||
pub up: u64,
|
|
||||||
pub down: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Traffic {
|
|
||||||
pub async fn get_traffic_stream() -> Result<impl Stream<Item = Result<Traffic, anyhow::Error>>>
|
|
||||||
{
|
|
||||||
use futures::{
|
|
||||||
future::FutureExt,
|
|
||||||
stream::{self, StreamExt},
|
|
||||||
};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
// 先处理错误和超时情况
|
|
||||||
let stream = Box::pin(
|
|
||||||
stream::unfold((), move |_| async move {
|
|
||||||
'retry: loop {
|
|
||||||
log::info!(target: "app", "establishing traffic websocket connection");
|
|
||||||
let (url, token) = MihomoManager::get_traffic_ws_url();
|
|
||||||
let mut request = match url.into_client_request() {
|
|
||||||
Ok(req) => req,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "failed to create websocket request: {}", e);
|
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
||||||
continue 'retry;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
request.headers_mut().insert(http::header::AUTHORIZATION, token);
|
|
||||||
|
|
||||||
match tokio::time::timeout(Duration::from_secs(3),
|
|
||||||
tokio_tungstenite::connect_async(request)
|
|
||||||
).await {
|
|
||||||
Ok(Ok((ws_stream, _))) => {
|
|
||||||
log::info!(target: "app", "traffic websocket connection established");
|
|
||||||
// 设置流超时控制
|
|
||||||
let traffic_stream = ws_stream
|
|
||||||
.take_while(|msg| {
|
|
||||||
let continue_stream = msg.is_ok();
|
|
||||||
async move { continue_stream }.boxed()
|
|
||||||
})
|
|
||||||
.filter_map(|msg| async move {
|
|
||||||
match msg {
|
|
||||||
Ok(msg) => {
|
|
||||||
if !msg.is_text() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
match tokio::time::timeout(
|
|
||||||
Duration::from_millis(200),
|
|
||||||
async { msg.into_text() }
|
|
||||||
).await {
|
|
||||||
Ok(Ok(text)) => {
|
|
||||||
match serde_json::from_str::<serde_json::Value>(&text) {
|
|
||||||
Ok(json) => {
|
|
||||||
let up = json["up"].as_u64().unwrap_or(0);
|
|
||||||
let down = json["down"].as_u64().unwrap_or(0);
|
|
||||||
Some(Ok(Traffic { up, down }))
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!(target: "app", "traffic json parse error: {} for {}", e, text);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
log::warn!(target: "app", "traffic text conversion error: {}", e);
|
|
||||||
None
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
log::warn!(target: "app", "traffic text processing timeout");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "traffic websocket error: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Some((traffic_stream, ()));
|
|
||||||
},
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
log::error!(target: "app", "traffic websocket connection failed: {}", e);
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
log::error!(target: "app", "traffic websocket connection timed out");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flatten(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use std::fs;
|
|||||||
/// Create a backup and upload to WebDAV
|
/// Create a backup and upload to WebDAV
|
||||||
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||||
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
||||||
log::error!(target: "app", "Failed to create backup: {:#?}", err);
|
log::error!(target: "app", "Failed to create backup: {err:#?}");
|
||||||
err
|
err
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -19,12 +19,12 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
|||||||
.upload(temp_file_path.clone(), file_name)
|
.upload(temp_file_path.clone(), file_name)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
|
log::error!(target: "app", "Failed to upload to WebDAV: {err:#?}");
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = std::fs::remove_file(&temp_file_path) {
|
if let Err(err) = std::fs::remove_file(&temp_file_path) {
|
||||||
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
|
log::warn!(target: "app", "Failed to remove temp file: {err:#?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -33,7 +33,7 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
|||||||
/// List WebDAV backups
|
/// List WebDAV backups
|
||||||
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
|
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
|
||||||
backup::WebDavClient::global().list().await.map_err(|err| {
|
backup::WebDavClient::global().list().await.map_err(|err| {
|
||||||
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
|
log::error!(target: "app", "Failed to list WebDAV backup files: {err:#?}");
|
||||||
err
|
err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
|||||||
.delete(filename)
|
.delete(filename)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
|
log::error!(target: "app", "Failed to delete WebDAV backup file: {err:#?}");
|
||||||
err
|
err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
|||||||
.download(filename, backup_storage_path.clone())
|
.download(filename, backup_storage_path.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
|
log::error!(target: "app", "Failed to download WebDAV backup file: {err:#?}");
|
||||||
err
|
err
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ fn after_change_clash_mode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "Failed to get connections: {}", err);
|
log::error!(target: "app", "Failed to get connections: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -108,12 +108,12 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
let response = NetworkManager::global()
|
let response = NetworkManager::global()
|
||||||
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
|
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false, false)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
log::trace!(target: "app", "test_delay response: {response:#?}");
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
Ok(start.elapsed().as_millis() as u32)
|
Ok(start.elapsed().as_millis() as u32)
|
||||||
} else {
|
} else {
|
||||||
@@ -121,7 +121,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
log::trace!(target: "app", "test_delay error: {err:#?}");
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ pub async fn update_profile(
|
|||||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||||
|
|
||||||
if !is_remote {
|
if !is_remote {
|
||||||
log::info!(target: "app", "[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
|
||||||
None // 非远程订阅直接更新
|
None // 非远程订阅直接更新
|
||||||
} else if item.url.is_none() {
|
} else if item.url.is_none() {
|
||||||
log::warn!(target: "app", "[订阅更新] {} 缺少URL,无法更新", uid);
|
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL,无法更新");
|
||||||
bail!("failed to get the profile item url");
|
bail!("failed to get the profile item url");
|
||||||
} else {
|
} else {
|
||||||
log::info!(target: "app",
|
log::info!(target: "app",
|
||||||
@@ -70,12 +70,12 @@ pub async fn update_profile(
|
|||||||
profiles.update_item(uid.clone(), item)?;
|
profiles.update_item(uid.clone(), item)?;
|
||||||
|
|
||||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||||
is_current && auto_refresh
|
is_current && auto_refresh
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// 首次更新失败,尝试使用Clash代理
|
// 首次更新失败,尝试使用Clash代理
|
||||||
log::warn!(target: "app", "[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
|
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err},尝试使用Clash代理更新");
|
||||||
|
|
||||||
// 发送通知
|
// 发送通知
|
||||||
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
||||||
@@ -112,14 +112,14 @@ pub async fn update_profile(
|
|||||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||||
|
|
||||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||||
is_current && auto_refresh
|
is_current && auto_refresh
|
||||||
}
|
}
|
||||||
Err(retry_err) => {
|
Err(retry_err) => {
|
||||||
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
|
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
|
||||||
handle::Handle::notice_message(
|
handle::Handle::notice_message(
|
||||||
"update_failed_even_with_clash",
|
"update_failed_even_with_clash",
|
||||||
format!("{}", retry_err),
|
format!("{retry_err}"),
|
||||||
);
|
);
|
||||||
return Err(retry_err);
|
return Err(retry_err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub fn toggle_system_proxy() {
|
|||||||
.close_all_connections()
|
.close_all_connections()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
log::error!(target: "app", "Failed to close all connections: {}", err);
|
log::error!(target: "app", "Failed to close all connections: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +75,8 @@ pub fn copy_clash_env() {
|
|||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
||||||
let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
|
let http_proxy = format!("http://{clash_verge_rev_ip}:{port}");
|
||||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
|
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
|
||||||
|
|
||||||
let cliboard = app_handle.clipboard();
|
let cliboard = app_handle.clipboard();
|
||||||
let env_type = { Config::verge().latest().env_type.clone() };
|
let env_type = { Config::verge().latest().env_type.clone() };
|
||||||
|
|||||||
@@ -11,23 +11,54 @@ use crate::{
|
|||||||
/// Open or close the dashboard window
|
/// Open or close the dashboard window
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn open_or_close_dashboard() {
|
pub fn open_or_close_dashboard() {
|
||||||
|
open_or_close_dashboard_internal(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open or close the dashboard window (hotkey call, dispatched to main thread)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn open_or_close_dashboard_hotkey() {
|
||||||
|
open_or_close_dashboard_internal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal implementation for opening/closing dashboard
|
||||||
|
fn open_or_close_dashboard_internal(bypass_debounce: bool) {
|
||||||
|
use crate::process::AsyncHandler;
|
||||||
use crate::utils::window_manager::WindowManager;
|
use crate::utils::window_manager::WindowManager;
|
||||||
|
|
||||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
|
||||||
|
|
||||||
// 检查是否在轻量模式下
|
// 热键调用调度到主线程执行,避免 WebView 创建死锁
|
||||||
|
if bypass_debounce {
|
||||||
|
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
|
||||||
|
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
log::info!(target: "app", "主线程中执行热键窗口操作");
|
||||||
|
|
||||||
|
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||||
|
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||||
|
crate::module::lightweight::exit_lightweight_mode();
|
||||||
|
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||||
|
let result = WindowManager::show_main_window();
|
||||||
|
log::info!(target: "app", "Window operation result: {result:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = WindowManager::toggle_main_window();
|
||||||
|
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||||
crate::module::lightweight::exit_lightweight_mode();
|
crate::module::lightweight::exit_lightweight_mode();
|
||||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||||
let result = WindowManager::show_main_window();
|
let result = WindowManager::show_main_window();
|
||||||
log::info!(target: "app", "Window operation result: {:?}", result);
|
log::info!(target: "app", "Window operation result: {result:?}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用统一的窗口管理器切换窗口状态
|
|
||||||
let result = WindowManager::toggle_main_window();
|
let result = WindowManager::toggle_main_window();
|
||||||
log::info!(target: "app", "Window toggle result: {:?}", result);
|
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 异步优化的应用退出函数
|
/// 异步优化的应用退出函数
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ pub fn run() {
|
|||||||
|
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut builder = tauri::Builder::default()
|
let mut builder = tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
@@ -342,7 +343,7 @@ pub fn run() {
|
|||||||
.get_webview_window("main")
|
.get_webview_window("main")
|
||||||
{
|
{
|
||||||
logging!(info, Type::Window, true, "设置macOS窗口标题");
|
logging!(info, Type::Window, true, "设置macOS窗口标题");
|
||||||
let _ = window.set_title("Clash Verge");
|
let _ = window.set_title("Clash Verge Rev Lite");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,17 @@ use crate::AppHandleManager;
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use delay_timer::prelude::TaskBuilder;
|
use delay_timer::prelude::TaskBuilder;
|
||||||
use std::sync::Mutex;
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Mutex,
|
||||||
|
};
|
||||||
use tauri::{Listener, Manager};
|
use tauri::{Listener, Manager};
|
||||||
|
|
||||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||||
|
|
||||||
|
// 添加退出轻量模式的锁,防止并发调用
|
||||||
|
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
fn with_lightweight_status<F, R>(f: F) -> R
|
fn with_lightweight_status<F, R>(f: F) -> R
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut LightWeightState) -> R,
|
F: FnOnce(&mut LightWeightState) -> R,
|
||||||
@@ -30,24 +36,24 @@ where
|
|||||||
|
|
||||||
pub fn run_once_auto_lightweight() {
|
pub fn run_once_auto_lightweight() {
|
||||||
LightWeightState::default().run_once_time(|| {
|
LightWeightState::default().run_once_time(|| {
|
||||||
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true);
|
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(false);
|
||||||
let enable_auto = Config::verge()
|
let enable_auto = Config::verge()
|
||||||
.data()
|
.data()
|
||||||
.enable_auto_light_weight_mode
|
.enable_auto_light_weight_mode
|
||||||
.unwrap_or(true);
|
.unwrap_or(false);
|
||||||
if enable_auto && is_silent_start {
|
if enable_auto && is_silent_start {
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::Lightweight,
|
Type::Lightweight,
|
||||||
true,
|
true,
|
||||||
"正常创建窗口和添加定时器监听器"
|
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
|
||||||
);
|
);
|
||||||
set_lightweight_mode(false);
|
set_lightweight_mode(false);
|
||||||
disable_auto_light_weight_mode();
|
enable_auto_light_weight_mode();
|
||||||
|
|
||||||
// 触发托盘更新
|
// 触发托盘更新
|
||||||
if let Err(e) = Tray::global().update_part() {
|
if let Err(e) = Tray::global().update_part() {
|
||||||
log::warn!("Failed to update tray: {}", e);
|
log::warn!("Failed to update tray: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -59,14 +65,19 @@ pub fn auto_lightweight_mode_init() {
|
|||||||
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(false);
|
||||||
let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
|
let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
|
||||||
|
|
||||||
if enable_auto && is_silent_start {
|
if enable_auto && !is_silent_start {
|
||||||
logging!(info, Type::Lightweight, true, "自动轻量模式静默启动");
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
true,
|
||||||
|
"非静默启动直接挂载自动进入轻量模式监听器!"
|
||||||
|
);
|
||||||
set_lightweight_mode(true);
|
set_lightweight_mode(true);
|
||||||
enable_auto_light_weight_mode();
|
enable_auto_light_weight_mode();
|
||||||
|
|
||||||
// 确保托盘状态更新
|
// 确保托盘状态更新
|
||||||
if let Err(e) = Tray::global().update_part() {
|
if let Err(e) = Tray::global().update_part() {
|
||||||
log::warn!("Failed to update tray: {}", e);
|
log::warn!("Failed to update tray: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,14 +89,14 @@ pub fn is_in_lightweight_mode() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 设置轻量模式状态
|
// 设置轻量模式状态
|
||||||
fn set_lightweight_mode(value: bool) {
|
pub fn set_lightweight_mode(value: bool) {
|
||||||
with_lightweight_status(|state| {
|
with_lightweight_status(|state| {
|
||||||
state.set_lightweight_mode(value);
|
state.set_lightweight_mode(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 触发托盘更新
|
// 触发托盘更新
|
||||||
if let Err(e) = Tray::global().update_part() {
|
if let Err(e) = Tray::global().update_part() {
|
||||||
log::warn!("Failed to update tray: {}", e);
|
log::warn!("Failed to update tray: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +131,6 @@ pub fn entry_lightweight_mode() {
|
|||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
AppHandleManager::global().set_activation_policy_accessory();
|
AppHandleManager::global().set_activation_policy_accessory();
|
||||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
|
||||||
}
|
}
|
||||||
set_lightweight_mode(true);
|
set_lightweight_mode(true);
|
||||||
let _ = cancel_light_weight_timer();
|
let _ = cancel_light_weight_timer();
|
||||||
@@ -131,6 +141,25 @@ pub fn entry_lightweight_mode() {
|
|||||||
|
|
||||||
// 添加从轻量模式恢复的函数
|
// 添加从轻量模式恢复的函数
|
||||||
pub fn exit_lightweight_mode() {
|
pub fn exit_lightweight_mode() {
|
||||||
|
// 使用原子操作检查是否已经在退出过程中,防止并发调用
|
||||||
|
if EXITING_LIGHTWEIGHT
|
||||||
|
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
true,
|
||||||
|
"轻量模式退出操作已在进行中,跳过重复调用"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用defer确保无论如何都会重置标志
|
||||||
|
let _guard = scopeguard::guard((), |_| {
|
||||||
|
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
|
||||||
// 确保当前确实处于轻量模式才执行退出操作
|
// 确保当前确实处于轻量模式才执行退出操作
|
||||||
if !is_in_lightweight_mode() {
|
if !is_in_lightweight_mode() {
|
||||||
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
|
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
|
||||||
@@ -138,7 +167,6 @@ pub fn exit_lightweight_mode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set_lightweight_mode(false);
|
set_lightweight_mode(false);
|
||||||
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
|
|
||||||
|
|
||||||
// macOS激活策略
|
// macOS激活策略
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ use once_cell::sync::Lazy;
|
|||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tauri::http::HeaderMap;
|
use tauri::http::HeaderMap;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use tauri::http::HeaderValue;
|
|
||||||
|
|
||||||
// 缓存的最大有效期(5秒)
|
// 缓存的最大有效期(5秒)
|
||||||
const CACHE_TTL: Duration = Duration::from_secs(5);
|
const CACHE_TTL: Duration = Duration::from_secs(5);
|
||||||
@@ -99,38 +97,12 @@ impl MihomoManager {
|
|||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||||
if let Some(secret) = client.secret {
|
if let Some(secret) = client.secret {
|
||||||
let secret = format!("Bearer {}", secret).parse().unwrap();
|
let secret = format!("Bearer {secret}").parse().unwrap();
|
||||||
headers.insert("Authorization", secret);
|
headers.insert("Authorization", secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((server, headers))
|
Some((server, headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提供默认值的版本,避免在connection_info为None时panic
|
// 已移除未使用的 get_clash_client_info_or_default 和 get_traffic_ws_url 方法
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
fn get_clash_client_info_or_default() -> (String, HeaderMap) {
|
|
||||||
Self::get_clash_client_info().unwrap_or_else(|| {
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
|
||||||
("http://127.0.0.1:9090".to_string(), headers)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn get_traffic_ws_url() -> (String, HeaderValue) {
|
|
||||||
let (url, headers) = MihomoManager::get_clash_client_info_or_default();
|
|
||||||
let ws_url = url.replace("http://", "ws://") + "/traffic";
|
|
||||||
let auth = headers
|
|
||||||
.get("Authorization")
|
|
||||||
.map(|val| val.to_str().unwrap_or("").to_string())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// 创建默认的空HeaderValue而不是使用unwrap_or_default
|
|
||||||
let token = match HeaderValue::from_str(&auth) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => HeaderValue::from_static(""),
|
|
||||||
};
|
|
||||||
|
|
||||||
(ws_url, token)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Result};
|
|||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use std::{fs, path::Path, path::PathBuf};
|
use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf};
|
||||||
|
|
||||||
/// Windows 下的开机启动文件夹路径
|
/// Windows 下的开机启动文件夹路径
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -59,6 +59,8 @@ pub fn create_shortcut() -> Result<()> {
|
|||||||
|
|
||||||
let output = std::process::Command::new("powershell")
|
let output = std::process::Command::new("powershell")
|
||||||
.args(["-Command", &powershell_command])
|
.args(["-Command", &powershell_command])
|
||||||
|
// 隐藏 PowerShell 窗口
|
||||||
|
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?;
|
.map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?;
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
|||||||
|
|
||||||
// 如果无法获取系统目录,则回退到可执行文件目录
|
// 如果无法获取系统目录,则回退到可执行文件目录
|
||||||
let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID);
|
let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID);
|
||||||
log::warn!(target: "app", "Using fallback data directory: {:?}", fallback_dir);
|
log::warn!(target: "app", "Using fallback data directory: {fallback_dir:?}");
|
||||||
return Ok(fallback_dir);
|
return Ok(fallback_dir);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -102,7 +102,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
|||||||
match app_handle.path().data_dir() {
|
match app_handle.path().data_dir() {
|
||||||
Ok(dir) => Ok(dir.join(APP_ID)),
|
Ok(dir) => Ok(dir.join(APP_ID)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to get the app home directory: {}", e);
|
log::error!(target: "app", "Failed to get the app home directory: {e}");
|
||||||
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
|
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ pub fn app_resources_dir() -> Result<PathBuf> {
|
|||||||
match app_handle.path().resource_dir() {
|
match app_handle.path().resource_dir() {
|
||||||
Ok(dir) => Ok(dir.join("resources")),
|
Ok(dir) => Ok(dir.join("resources")),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to get the resource directory: {}", e);
|
log::error!(target: "app", "Failed to get the resource directory: {e}");
|
||||||
Err(anyhow::anyhow!("Failed to get the resource directory"))
|
Err(anyhow::anyhow!("Failed to get the resource directory"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +203,7 @@ pub fn service_log_file() -> Result<PathBuf> {
|
|||||||
let log_dir = app_logs_dir()?.join("service");
|
let log_dir = app_logs_dir()?.join("service");
|
||||||
|
|
||||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||||
let log_file = format!("{}.log", local_time);
|
let log_file = format!("{local_time}.log");
|
||||||
let log_file = log_dir.join(log_file);
|
let log_file = log_dir.join(log_file);
|
||||||
|
|
||||||
let _ = std::fs::create_dir_all(&log_dir);
|
let _ = std::fs::create_dir_all(&log_dir);
|
||||||
|
|||||||
@@ -125,19 +125,6 @@ pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn is_monochrome_image_from_bytes(data: &[u8]) -> anyhow::Result<bool> {
|
|
||||||
let img = image::load_from_memory(data)?;
|
|
||||||
let rgb_img = img.to_rgb8();
|
|
||||||
|
|
||||||
for pixel in rgb_img.pixels() {
|
|
||||||
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn linux_elevator() -> String {
|
pub fn linux_elevator() -> String {
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -176,39 +163,3 @@ macro_rules! t {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将字节数转换为可读的流量字符串
|
|
||||||
/// 支持 B/s、KB/s、MB/s、GB/s 的自动转换
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```not_run
|
|
||||||
/// format_bytes_speed(1000) // returns "1000B/s"
|
|
||||||
/// format_bytes_speed(1024) // returns "1.0KB/s"
|
|
||||||
/// format_bytes_speed(1024 * 1024) // returns "1.0MB/s"
|
|
||||||
/// ```
|
|
||||||
/// ```
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn format_bytes_speed(speed: u64) -> String {
|
|
||||||
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
|
||||||
let mut size = speed as f64;
|
|
||||||
let mut unit_index = 0;
|
|
||||||
|
|
||||||
while size >= 1000.0 && unit_index < UNITS.len() - 1 {
|
|
||||||
size /= 1024.0;
|
|
||||||
unit_index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("{:.1}{}/s", size, UNITS[unit_index])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
#[test]
|
|
||||||
fn test_format_bytes_speed() {
|
|
||||||
assert_eq!(format_bytes_speed(0), "0.0B/s");
|
|
||||||
assert_eq!(format_bytes_speed(1023), "1.0KB/s");
|
|
||||||
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
|
|
||||||
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
|
|
||||||
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
|
|
||||||
assert_eq!(format_bytes_speed(1024 * 500), "500.0KB/s");
|
|
||||||
assert_eq!(format_bytes_speed(1024 * 1024 * 2), "2.0MB/s");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ static TRANSLATIONS: Lazy<HashMap<String, Value>> = Lazy::new(|| {
|
|||||||
|
|
||||||
if let Some(locales_dir) = get_locales_dir() {
|
if let Some(locales_dir) = get_locales_dir() {
|
||||||
for lang in get_supported_languages() {
|
for lang in get_supported_languages() {
|
||||||
let file_path = locales_dir.join(format!("{}.json", lang));
|
let file_path = locales_dir.join(format!("{lang}.json"));
|
||||||
if let Ok(content) = fs::read_to_string(file_path) {
|
if let Ok(content) = fs::read_to_string(file_path) {
|
||||||
if let Ok(json) = serde_json::from_str(&content) {
|
if let Ok(json) = serde_json::from_str(&content) {
|
||||||
translations.insert(lang.to_string(), json);
|
translations.insert(lang.to_string(), json);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fn init_log() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||||
let log_file = format!("{}.log", local_time);
|
let log_file = format!("{local_time}.log");
|
||||||
let log_file = log_dir.join(log_file);
|
let log_file = log_dir.join(log_file);
|
||||||
|
|
||||||
let log_pattern = match log_level {
|
let log_pattern = match log_level {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ pub mod i18n;
|
|||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod notification;
|
||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod tmpl;
|
pub mod tmpl;
|
||||||
pub mod window_manager;
|
pub mod window_manager;
|
||||||
|
pub mod sys_info;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use tokio::runtime::{Builder, Runtime};
|
use tokio::runtime::{Builder, Runtime};
|
||||||
|
|
||||||
use crate::{config::Config, logging, utils::logging::Type};
|
use crate::{config::Config, logging, utils::logging::Type, utils::sys_info};
|
||||||
|
|
||||||
// HTTP2 相关
|
// HTTP2 相关
|
||||||
const H2_CONNECTION_WINDOW_SIZE: u32 = 1024 * 1024;
|
const H2_CONNECTION_WINDOW_SIZE: u32 = 1024 * 1024;
|
||||||
@@ -248,6 +248,7 @@ impl NetworkManager {
|
|||||||
timeout_secs: Option<u64>,
|
timeout_secs: Option<u64>,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
accept_invalid_certs: bool,
|
accept_invalid_certs: bool,
|
||||||
|
use_hwid: bool,
|
||||||
) -> RequestBuilder {
|
) -> RequestBuilder {
|
||||||
if self.should_reset_clients() {
|
if self.should_reset_clients() {
|
||||||
self.reset_clients();
|
self.reset_clients();
|
||||||
@@ -322,7 +323,7 @@ impl NetworkManager {
|
|||||||
use crate::utils::resolve::VERSION;
|
use crate::utils::resolve::VERSION;
|
||||||
|
|
||||||
let version = match VERSION.get() {
|
let version = match VERSION.get() {
|
||||||
Some(v) => format!("clash-verge/v{}", v),
|
Some(v) => format!("clash-verge/v{v}"),
|
||||||
None => "clash-verge/unknown".to_string(),
|
None => "clash-verge/unknown".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -331,7 +332,18 @@ impl NetworkManager {
|
|||||||
|
|
||||||
let client = builder.build().expect("Failed to build custom HTTP client");
|
let client = builder.build().expect("Failed to build custom HTTP client");
|
||||||
|
|
||||||
client.get(url)
|
let mut request_builder = client.get(url);
|
||||||
|
|
||||||
|
if use_hwid {
|
||||||
|
let sys_info = sys_info::get_system_info();
|
||||||
|
logging!(info, Type::Network, true, "Adding HWID headers to request");
|
||||||
|
request_builder = request_builder
|
||||||
|
.header("x-hwid", &sys_info.hwid)
|
||||||
|
.header("x-device-os", &sys_info.os_type)
|
||||||
|
.header("x-ver-os", &sys_info.os_ver);
|
||||||
|
}
|
||||||
|
|
||||||
|
request_builder
|
||||||
}
|
}
|
||||||
|
|
||||||
/* /// 执行GET请求,添加错误跟踪
|
/* /// 执行GET请求,添加错误跟踪
|
||||||
@@ -378,6 +390,7 @@ impl NetworkManager {
|
|||||||
timeout_secs: Option<u64>,
|
timeout_secs: Option<u64>,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
accept_invalid_certs: bool,
|
accept_invalid_certs: bool,
|
||||||
|
use_hwid: bool,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
let request = self.create_request(
|
let request = self.create_request(
|
||||||
url,
|
url,
|
||||||
@@ -385,6 +398,7 @@ impl NetworkManager {
|
|||||||
timeout_secs,
|
timeout_secs,
|
||||||
user_agent,
|
user_agent,
|
||||||
accept_invalid_certs,
|
accept_invalid_certs,
|
||||||
|
use_hwid,
|
||||||
);
|
);
|
||||||
|
|
||||||
let timeout_duration = timeout_secs.unwrap_or(20);
|
let timeout_duration = timeout_secs.unwrap_or(20);
|
||||||
@@ -401,7 +415,7 @@ impl NetworkManager {
|
|||||||
let result = tokio::select! {
|
let result = tokio::select! {
|
||||||
result = request.send() => result,
|
result = request.send() => result,
|
||||||
_ = cancel_rx => {
|
_ = cancel_rx => {
|
||||||
self.record_connection_error(&format!("Request interrupted for: {}", url));
|
self.record_connection_error(&format!("Request interrupted for: {url}"));
|
||||||
return Err(anyhow::anyhow!("Request interrupted after {} seconds", timeout_duration));
|
return Err(anyhow::anyhow!("Request interrupted after {} seconds", timeout_duration));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
70
src-tauri/src/utils/notification.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use tauri::AppHandle;
|
||||||
|
use tauri_plugin_notification::NotificationExt;
|
||||||
|
|
||||||
|
pub enum NotificationEvent<'a> {
|
||||||
|
DashboardToggled,
|
||||||
|
ClashModeChanged {
|
||||||
|
mode: &'a str,
|
||||||
|
},
|
||||||
|
SystemProxyToggled,
|
||||||
|
TunModeToggled,
|
||||||
|
LightweightModeEntered,
|
||||||
|
AppQuit,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
AppHidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify(app: &AppHandle, title: &str, body: &str) {
|
||||||
|
app.notification()
|
||||||
|
.builder()
|
||||||
|
.title(title)
|
||||||
|
.body(body)
|
||||||
|
.show()
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_event(app: &AppHandle, event: NotificationEvent) {
|
||||||
|
use crate::utils::i18n::t;
|
||||||
|
match event {
|
||||||
|
NotificationEvent::DashboardToggled => {
|
||||||
|
notify(app, &t("DashboardToggledTitle"), &t("DashboardToggledBody"));
|
||||||
|
}
|
||||||
|
NotificationEvent::ClashModeChanged { mode } => {
|
||||||
|
notify(
|
||||||
|
app,
|
||||||
|
&t("ClashModeChangedTitle"),
|
||||||
|
&t_with_args("ClashModeChangedBody", mode),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
NotificationEvent::SystemProxyToggled => {
|
||||||
|
notify(
|
||||||
|
app,
|
||||||
|
&t("SystemProxyToggledTitle"),
|
||||||
|
&t("SystemProxyToggledBody"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
NotificationEvent::TunModeToggled => {
|
||||||
|
notify(app, &t("TunModeToggledTitle"), &t("TunModeToggledBody"));
|
||||||
|
}
|
||||||
|
NotificationEvent::LightweightModeEntered => {
|
||||||
|
notify(
|
||||||
|
app,
|
||||||
|
&t("LightweightModeEnteredTitle"),
|
||||||
|
&t("LightweightModeEnteredBody"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
NotificationEvent::AppQuit => {
|
||||||
|
notify(app, &t("AppQuitTitle"), &t("AppQuitBody"));
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
NotificationEvent::AppHidden => {
|
||||||
|
notify(app, &t("AppHiddenTitle"), &t("AppHiddenBody"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数,带参数的i18n
|
||||||
|
fn t_with_args(key: &str, mode: &str) -> String {
|
||||||
|
use crate::utils::i18n::t;
|
||||||
|
t(key).replace("{mode}", mode)
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ use anyhow::{bail, Result};
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use percent_encoding::percent_decode_str;
|
use percent_encoding::percent_decode_str;
|
||||||
|
use scopeguard;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::{
|
use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@@ -22,6 +23,7 @@ use tauri::{AppHandle, Manager};
|
|||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use tauri::Url;
|
use tauri::Url;
|
||||||
|
use crate::config::PrfOption;
|
||||||
//#[cfg(not(target_os = "linux"))]
|
//#[cfg(not(target_os = "linux"))]
|
||||||
// use window_shadows::set_shadow;
|
// use window_shadows::set_shadow;
|
||||||
|
|
||||||
@@ -51,14 +53,12 @@ pub enum UiReadyStage {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct UiReadyState {
|
struct UiReadyState {
|
||||||
stage: RwLock<UiReadyStage>,
|
stage: RwLock<UiReadyStage>,
|
||||||
last_update: RwLock<Instant>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiReadyState {
|
impl Default for UiReadyState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
stage: RwLock::new(UiReadyStage::NotStarted),
|
stage: RwLock::new(UiReadyStage::NotStarted),
|
||||||
last_update: RwLock::new(Instant::now()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,20 +82,8 @@ fn get_ui_ready_state() -> &'static Arc<UiReadyState> {
|
|||||||
pub fn update_ui_ready_stage(stage: UiReadyStage) {
|
pub fn update_ui_ready_stage(stage: UiReadyStage) {
|
||||||
let state = get_ui_ready_state();
|
let state = get_ui_ready_state();
|
||||||
let mut stage_lock = state.stage.write();
|
let mut stage_lock = state.stage.write();
|
||||||
let mut time_lock = state.last_update.write();
|
|
||||||
|
|
||||||
*stage_lock = stage;
|
*stage_lock = stage;
|
||||||
*time_lock = Instant::now();
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Window,
|
|
||||||
true,
|
|
||||||
"UI准备阶段更新: {:?}, 耗时: {:?}ms",
|
|
||||||
stage,
|
|
||||||
time_lock.elapsed().as_millis()
|
|
||||||
);
|
|
||||||
|
|
||||||
// 如果是最终阶段,标记UI完全就绪
|
// 如果是最终阶段,标记UI完全就绪
|
||||||
if stage == UiReadyStage::Ready {
|
if stage == UiReadyStage::Ready {
|
||||||
mark_ui_ready();
|
mark_ui_ready();
|
||||||
@@ -118,9 +106,7 @@ pub fn reset_ui_ready() {
|
|||||||
{
|
{
|
||||||
let state = get_ui_ready_state();
|
let state = get_ui_ready_state();
|
||||||
let mut stage = state.stage.write();
|
let mut stage = state.stage.write();
|
||||||
let mut time = state.last_update.write();
|
|
||||||
*stage = UiReadyStage::NotStarted;
|
*stage = UiReadyStage::NotStarted;
|
||||||
*time = Instant::now();
|
|
||||||
}
|
}
|
||||||
logging!(info, Type::Window, true, "UI就绪状态已重置");
|
logging!(info, Type::Window, true, "UI就绪状态已重置");
|
||||||
}
|
}
|
||||||
@@ -136,7 +122,7 @@ pub async fn find_unused_port() -> Result<u16> {
|
|||||||
.latest()
|
.latest()
|
||||||
.verge_mixed_port
|
.verge_mixed_port
|
||||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
log::warn!(target: "app", "use default port: {}", port);
|
log::warn!(target: "app", "use default port: {port}");
|
||||||
Ok(port)
|
Ok(port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,6 +280,7 @@ pub fn create_window(is_show: bool) -> bool {
|
|||||||
|
|
||||||
if !is_show {
|
if !is_show {
|
||||||
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
|
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
|
||||||
|
lightweight::set_lightweight_mode(true);
|
||||||
handle::Handle::notify_startup_completed();
|
handle::Handle::notify_startup_completed();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -337,12 +324,18 @@ pub fn create_window(is_show: bool) -> bool {
|
|||||||
|
|
||||||
*creating = (true, Instant::now());
|
*creating = (true, Instant::now());
|
||||||
|
|
||||||
|
// ScopeGuard 确保创建状态重置,防止 webview 卡死
|
||||||
|
let _guard = scopeguard::guard(creating, |mut creating_guard| {
|
||||||
|
*creating_guard = (false, Instant::now());
|
||||||
|
logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置");
|
||||||
|
});
|
||||||
|
|
||||||
match tauri::WebviewWindowBuilder::new(
|
match tauri::WebviewWindowBuilder::new(
|
||||||
&handle::Handle::global().app_handle().unwrap(),
|
&handle::Handle::global().app_handle().unwrap(),
|
||||||
"main", /* the unique window label */
|
"main", /* the unique window label */
|
||||||
tauri::WebviewUrl::App("index.html".into()),
|
tauri::WebviewUrl::App("index.html".into()),
|
||||||
)
|
)
|
||||||
.title("Clash Verge")
|
.title("Clash Verge Rev Lite")
|
||||||
.center()
|
.center()
|
||||||
.decorations(true)
|
.decorations(true)
|
||||||
.fullscreen(false)
|
.fullscreen(false)
|
||||||
@@ -419,8 +412,6 @@ pub fn create_window(is_show: bool) -> bool {
|
|||||||
Ok(newly_created_window) => {
|
Ok(newly_created_window) => {
|
||||||
logging!(debug, Type::Window, true, "主窗口实例创建成功");
|
logging!(debug, Type::Window, true, "主窗口实例创建成功");
|
||||||
|
|
||||||
*creating = (false, Instant::now());
|
|
||||||
|
|
||||||
update_ui_ready_stage(UiReadyStage::NotStarted);
|
update_ui_ready_stage(UiReadyStage::NotStarted);
|
||||||
|
|
||||||
AsyncHandler::spawn(move || async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
@@ -534,14 +525,13 @@ pub fn create_window(is_show: bool) -> bool {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
|
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
|
||||||
*creating = (false, Instant::now()); // Reset the creating state if window creation failed
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_scheme(param: String) -> Result<()> {
|
pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||||
log::info!(target:"app", "received deep link: {}", param);
|
log::info!(target:"app", "received deep link: {param}");
|
||||||
|
|
||||||
let param_str = if param.starts_with("[") && param.len() > 4 {
|
let param_str = if param.starts_with("[") && param.len() > 4 {
|
||||||
param
|
param
|
||||||
@@ -560,29 +550,35 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
||||||
let name = link_parsed
|
let mut name: Option<String> = None;
|
||||||
.query_pairs()
|
let mut url_param: Option<String> = None;
|
||||||
.find(|(key, _)| key == "name")
|
let mut use_hwid = true;
|
||||||
.map(|(_, value)| value.into_owned());
|
|
||||||
|
|
||||||
let url_param = if let Some(query) = link_parsed.query() {
|
for (key, value) in link_parsed.query_pairs() {
|
||||||
let prefix = "url=";
|
match key.as_ref() {
|
||||||
if let Some(pos) = query.find(prefix) {
|
"name" => name = Some(value.into_owned()),
|
||||||
let raw_url = &query[pos + prefix.len()..];
|
"url" => url_param = Some(percent_decode_str(&value).decode_utf8_lossy().to_string()),
|
||||||
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
|
"hwid" => use_hwid = value == "1" || value == "true",
|
||||||
} else {
|
_ => {}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let option = if use_hwid {
|
||||||
|
log::info!(target:"app", "HWID usage requested via deep link");
|
||||||
|
Some(PrfOption {
|
||||||
|
use_hwid: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
match url_param {
|
match url_param {
|
||||||
Some(url) => {
|
Some(url) => {
|
||||||
log::info!(target:"app", "decoded subscription url: {}", url);
|
log::info!(target:"app", "decoded subscription url: {url}");
|
||||||
|
|
||||||
create_window(false);
|
create_window(false);
|
||||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
match PrfItem::from_url(url.as_ref(), name, None, option).await {
|
||||||
Ok(item) => {
|
Ok(item) => {
|
||||||
let uid = item.uid.clone().unwrap();
|
let uid = item.uid.clone().unwrap();
|
||||||
let _ = wrap_err!(Config::profiles().data().append_item(item));
|
let _ = wrap_err!(Config::profiles().data().append_item(item));
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ pub fn embed_server() {
|
|||||||
.latest()
|
.latest()
|
||||||
.verge_mixed_port
|
.verge_mixed_port
|
||||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
let content = content.replace("%mixed-port%", &format!("{}", port));
|
let content = content.replace("%mixed-port%", &format!("{port}"));
|
||||||
warp::http::Response::builder()
|
warp::http::Response::builder()
|
||||||
.header("Content-Type", "application/x-ns-proxy-autoconfig")
|
.header("Content-Type", "application/x-ns-proxy-autoconfig")
|
||||||
.body(content)
|
.body(content)
|
||||||
|
|||||||
22
src-tauri/src/utils/sys_info.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
pub struct SystemInfo {
|
||||||
|
pub hwid: String,
|
||||||
|
pub os_type: String,
|
||||||
|
pub os_ver: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static SYSTEM_INFO: Lazy<SystemInfo> = Lazy::new(|| {
|
||||||
|
let os_info = os_info::get();
|
||||||
|
SystemInfo {
|
||||||
|
hwid: machine_uid::get().unwrap_or_else(|_| "unknown_hwid".to_string()),
|
||||||
|
os_type: os_info.os_type().to_string(),
|
||||||
|
os_ver: os_info.version().to_string(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn get_system_info() -> &'static SystemInfo {
|
||||||
|
&SYSTEM_INFO
|
||||||
|
}
|
||||||
@@ -4,6 +4,14 @@ use tauri::{Manager, WebviewWindow, Wry};
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use crate::AppHandleManager;
|
use crate::AppHandleManager;
|
||||||
|
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use scopeguard;
|
||||||
|
use std::{
|
||||||
|
sync::atomic::{AtomicBool, Ordering},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
/// 窗口操作结果
|
/// 窗口操作结果
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum WindowOperationResult {
|
pub enum WindowOperationResult {
|
||||||
@@ -34,25 +42,71 @@ pub enum WindowState {
|
|||||||
NotExist,
|
NotExist,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 窗口操作防抖机制
|
||||||
|
static WINDOW_OPERATION_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
|
||||||
|
static WINDOW_OPERATION_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
|
||||||
|
const WINDOW_OPERATION_DEBOUNCE_MS: u64 = 500;
|
||||||
|
|
||||||
|
fn get_window_operation_debounce() -> &'static Mutex<Instant> {
|
||||||
|
WINDOW_OPERATION_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_handle_window_operation() -> bool {
|
||||||
|
if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) {
|
||||||
|
log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let debounce_lock = get_window_operation_debounce();
|
||||||
|
let mut last_operation = debounce_lock.lock();
|
||||||
|
let now = Instant::now();
|
||||||
|
let elapsed = now.duration_since(*last_operation);
|
||||||
|
|
||||||
|
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
|
||||||
|
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||||
|
|
||||||
|
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
|
||||||
|
*last_operation = now;
|
||||||
|
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
|
||||||
|
log::info!(target: "app", "[防抖] 窗口操作被允许执行");
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
|
||||||
|
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_window_operation() {
|
||||||
|
WINDOW_OPERATION_IN_PROGRESS.store(false, Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
/// 统一的窗口管理器
|
/// 统一的窗口管理器
|
||||||
pub struct WindowManager;
|
pub struct WindowManager;
|
||||||
|
|
||||||
impl WindowManager {
|
impl WindowManager {
|
||||||
pub fn get_main_window_state() -> WindowState {
|
pub fn get_main_window_state() -> WindowState {
|
||||||
if let Some(window) = Self::get_main_window() {
|
match Self::get_main_window() {
|
||||||
if window.is_minimized().unwrap_or(false) {
|
Some(window) => {
|
||||||
WindowState::Minimized
|
let is_minimized = window.is_minimized().unwrap_or(false);
|
||||||
} else if window.is_visible().unwrap_or(false) {
|
let is_visible = window.is_visible().unwrap_or(false);
|
||||||
if window.is_focused().unwrap_or(false) {
|
let is_focused = window.is_focused().unwrap_or(false);
|
||||||
|
|
||||||
|
if is_minimized {
|
||||||
|
return WindowState::Minimized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_visible {
|
||||||
|
return WindowState::Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_focused {
|
||||||
WindowState::VisibleFocused
|
WindowState::VisibleFocused
|
||||||
} else {
|
} else {
|
||||||
WindowState::VisibleUnfocused
|
WindowState::VisibleUnfocused
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
WindowState::Hidden
|
|
||||||
}
|
}
|
||||||
} else {
|
None => WindowState::NotExist,
|
||||||
WindowState::NotExist
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +119,14 @@ impl WindowManager {
|
|||||||
|
|
||||||
/// 智能显示主窗口
|
/// 智能显示主窗口
|
||||||
pub fn show_main_window() -> WindowOperationResult {
|
pub fn show_main_window() -> WindowOperationResult {
|
||||||
|
// 防抖检查
|
||||||
|
if !should_handle_window_operation() {
|
||||||
|
return WindowOperationResult::NoAction;
|
||||||
|
}
|
||||||
|
let _guard = scopeguard::guard((), |_| {
|
||||||
|
finish_window_operation();
|
||||||
|
});
|
||||||
|
|
||||||
logging!(info, Type::Window, true, "开始智能显示主窗口");
|
logging!(info, Type::Window, true, "开始智能显示主窗口");
|
||||||
logging!(
|
logging!(
|
||||||
debug,
|
debug,
|
||||||
@@ -80,8 +142,11 @@ impl WindowManager {
|
|||||||
WindowState::NotExist => {
|
WindowState::NotExist => {
|
||||||
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
|
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
|
||||||
if Self::create_new_window() {
|
if Self::create_new_window() {
|
||||||
|
logging!(info, Type::Window, true, "窗口创建成功");
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
WindowOperationResult::Created
|
WindowOperationResult::Created
|
||||||
} else {
|
} else {
|
||||||
|
logging!(warn, Type::Window, true, "窗口创建失败");
|
||||||
WindowOperationResult::Failed
|
WindowOperationResult::Failed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,6 +156,16 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
|
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
|
||||||
if let Some(window) = Self::get_main_window() {
|
if let Some(window) = Self::get_main_window() {
|
||||||
|
let state_after_check = Self::get_main_window_state();
|
||||||
|
if state_after_check == WindowState::VisibleFocused {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"窗口在检查期间已变为可见和有焦点状态"
|
||||||
|
);
|
||||||
|
return WindowOperationResult::NoAction;
|
||||||
|
}
|
||||||
Self::activate_window(&window)
|
Self::activate_window(&window)
|
||||||
} else {
|
} else {
|
||||||
WindowOperationResult::Failed
|
WindowOperationResult::Failed
|
||||||
@@ -101,6 +176,14 @@ impl WindowManager {
|
|||||||
|
|
||||||
/// 切换主窗口显示状态(显示/隐藏)
|
/// 切换主窗口显示状态(显示/隐藏)
|
||||||
pub fn toggle_main_window() -> WindowOperationResult {
|
pub fn toggle_main_window() -> WindowOperationResult {
|
||||||
|
// 防抖检查
|
||||||
|
if !should_handle_window_operation() {
|
||||||
|
return WindowOperationResult::NoAction;
|
||||||
|
}
|
||||||
|
let _guard = scopeguard::guard((), |_| {
|
||||||
|
finish_window_operation();
|
||||||
|
});
|
||||||
|
|
||||||
logging!(info, Type::Window, true, "开始切换主窗口显示状态");
|
logging!(info, Type::Window, true, "开始切换主窗口显示状态");
|
||||||
|
|
||||||
let current_state = Self::get_main_window_state();
|
let current_state = Self::get_main_window_state();
|
||||||
@@ -108,37 +191,61 @@ impl WindowManager {
|
|||||||
info,
|
info,
|
||||||
Type::Window,
|
Type::Window,
|
||||||
true,
|
true,
|
||||||
"当前窗口状态: {:?}",
|
"当前窗口状态: {:?} | 详细状态: {}",
|
||||||
current_state
|
current_state,
|
||||||
|
Self::get_window_status_info()
|
||||||
);
|
);
|
||||||
|
|
||||||
match current_state {
|
match current_state {
|
||||||
WindowState::NotExist => {
|
WindowState::NotExist => {
|
||||||
// 窗口不存在,创建新窗口
|
// 窗口不存在,创建新窗口
|
||||||
|
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
|
||||||
|
// 由于已经有防抖保护,直接调用内部方法
|
||||||
if Self::create_new_window() {
|
if Self::create_new_window() {
|
||||||
WindowOperationResult::Created
|
WindowOperationResult::Created
|
||||||
} else {
|
} else {
|
||||||
WindowOperationResult::Failed
|
WindowOperationResult::Failed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowState::VisibleFocused => {
|
WindowState::VisibleFocused | WindowState::VisibleUnfocused => {
|
||||||
// 窗口可见且有焦点,隐藏它
|
logging!(
|
||||||
if let Some(window) = Self::get_main_window() {
|
info,
|
||||||
if window.hide().is_ok() {
|
Type::Window,
|
||||||
logging!(info, Type::Window, true, "窗口已隐藏");
|
true,
|
||||||
WindowOperationResult::Hidden
|
"窗口可见(焦点状态: {}),将隐藏窗口",
|
||||||
|
if current_state == WindowState::VisibleFocused {
|
||||||
|
"有焦点"
|
||||||
} else {
|
} else {
|
||||||
WindowOperationResult::Failed
|
"无焦点"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if let Some(window) = Self::get_main_window() {
|
||||||
|
match window.hide() {
|
||||||
|
Ok(_) => {
|
||||||
|
logging!(info, Type::Window, true, "窗口已成功隐藏");
|
||||||
|
WindowOperationResult::Hidden
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
|
||||||
|
WindowOperationResult::Failed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
logging!(warn, Type::Window, true, "无法获取窗口实例");
|
||||||
WindowOperationResult::Failed
|
WindowOperationResult::Failed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
|
WindowState::Minimized | WindowState::Hidden => {
|
||||||
// 窗口存在但不可见或无焦点,激活它
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"窗口存在但被隐藏或最小化,将激活窗口"
|
||||||
|
);
|
||||||
if let Some(window) = Self::get_main_window() {
|
if let Some(window) = Self::get_main_window() {
|
||||||
Self::activate_window(&window)
|
Self::activate_window(&window)
|
||||||
} else {
|
} else {
|
||||||
|
logging!(warn, Type::Window, true, "无法获取窗口实例");
|
||||||
WindowOperationResult::Failed
|
WindowOperationResult::Failed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,17 +323,21 @@ impl WindowManager {
|
|||||||
pub fn hide_main_window() -> WindowOperationResult {
|
pub fn hide_main_window() -> WindowOperationResult {
|
||||||
logging!(info, Type::Window, true, "开始隐藏主窗口");
|
logging!(info, Type::Window, true, "开始隐藏主窗口");
|
||||||
|
|
||||||
if let Some(window) = Self::get_main_window() {
|
match Self::get_main_window() {
|
||||||
if window.hide().is_ok() {
|
Some(window) => match window.hide() {
|
||||||
logging!(info, Type::Window, true, "窗口已隐藏");
|
Ok(_) => {
|
||||||
WindowOperationResult::Hidden
|
logging!(info, Type::Window, true, "窗口已隐藏");
|
||||||
} else {
|
WindowOperationResult::Hidden
|
||||||
logging!(warn, Type::Window, true, "隐藏窗口失败");
|
}
|
||||||
WindowOperationResult::Failed
|
Err(e) => {
|
||||||
|
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
|
||||||
|
WindowOperationResult::Failed
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
|
||||||
|
WindowOperationResult::NoAction
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
|
|
||||||
WindowOperationResult::NoAction
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +362,7 @@ impl WindowManager {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 创建新窗口现有的实现
|
/// 创建新窗口,防抖避免重复调用
|
||||||
fn create_new_window() -> bool {
|
fn create_new_window() -> bool {
|
||||||
use crate::utils::resolve;
|
use crate::utils::resolve;
|
||||||
resolve::create_window(true)
|
resolve::create_window(true)
|
||||||
@@ -265,8 +376,7 @@ impl WindowManager {
|
|||||||
let is_minimized = Self::is_main_window_minimized();
|
let is_minimized = Self::is_main_window_minimized();
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"窗口状态: {:?} | 可见: {} | 有焦点: {} | 最小化: {}",
|
"窗口状态: {state:?} | 可见: {is_visible} | 有焦点: {is_focused} | 最小化: {is_minimized}"
|
||||||
state, is_visible, is_focused, is_minimized
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"version": "2.3.1",
|
"version": "0.2.0",
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"longDescription": "Clash Verge Rev",
|
"longDescription": "Clash Verge Rev Lite",
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
@@ -12,11 +12,11 @@
|
|||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources": ["resources", "resources/locales/*"],
|
"resources": ["resources", "resources/locales/*"],
|
||||||
"publisher": "Clash Verge Rev",
|
"publisher": "Clash Verge Rev Lite",
|
||||||
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
|
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
|
||||||
"copyright": "GNU General Public License v3.0",
|
"copyright": "GNU General Public License v3.0",
|
||||||
"category": "DeveloperTool",
|
"category": "DeveloperTool",
|
||||||
"shortDescription": "Clash Verge Rev",
|
"shortDescription": "Clash Verge Rev Lite",
|
||||||
"createUpdaterArtifacts": true
|
"createUpdaterArtifacts": true
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
@@ -25,18 +25,14 @@
|
|||||||
"beforeDevCommand": "pnpm run web:dev",
|
"beforeDevCommand": "pnpm run web:dev",
|
||||||
"devUrl": "http://localhost:3000/"
|
"devUrl": "http://localhost:3000/"
|
||||||
},
|
},
|
||||||
"productName": "Clash Verge",
|
"productName": "Clash Verge Rev Lite",
|
||||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
"https://github.com/coolcoala/clash-verge-rev-lite/releases/download/updater/update.json",
|
||||||
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
"https://github.com/coolcoala/clash-verge-rev-lite/releases/download/updater-alpha/update-alpha.json"
|
||||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
|
|
||||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
|
||||||
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
|
||||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json"
|
|
||||||
],
|
],
|
||||||
"windows": {
|
"windows": {
|
||||||
"installMode": "basicUi"
|
"installMode": "basicUi"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
"productName": "Clash Verge",
|
"productName": "Clash Verge Rev Lite",
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"targets": ["app", "dmg"],
|
"targets": ["app", "dmg"],
|
||||||
"macOS": {
|
"macOS": {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"nsis": {
|
"nsis": {
|
||||||
"displayLanguageSelector": true,
|
"displayLanguageSelector": true,
|
||||||
"installerIcon": "icons/icon.ico",
|
"installerIcon": "icons/icon.ico",
|
||||||
"languages": ["SimpChinese", "English"],
|
"languages": ["Russian", "English"],
|
||||||
"installMode": "perMachine",
|
"installMode": "perMachine",
|
||||||
"template": "./packages/windows/installer.nsi"
|
"template": "./packages/windows/installer.nsi"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"nsis": {
|
"nsis": {
|
||||||
"displayLanguageSelector": true,
|
"displayLanguageSelector": true,
|
||||||
"installerIcon": "icons/icon.ico",
|
"installerIcon": "icons/icon.ico",
|
||||||
"languages": ["SimpChinese", "English"],
|
"languages": ["Russian", "English"],
|
||||||
"installMode": "perMachine",
|
"installMode": "perMachine",
|
||||||
"template": "./packages/windows/installer.nsi"
|
"template": "./packages/windows/installer.nsi"
|
||||||
}
|
}
|
||||||
@@ -25,10 +25,10 @@
|
|||||||
"active": true,
|
"active": true,
|
||||||
"dialog": false,
|
"dialog": false,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
|
"https://github.com/coolcoala/clash-verge-rev-lite/releases/download/updater/update-fixed-webview2.json"
|
||||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
|
|
||||||
],
|
],
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"nsis": {
|
"nsis": {
|
||||||
"displayLanguageSelector": true,
|
"displayLanguageSelector": true,
|
||||||
"installerIcon": "icons/icon.ico",
|
"installerIcon": "icons/icon.ico",
|
||||||
"languages": ["SimpChinese", "English"],
|
"languages": ["Russian", "English"],
|
||||||
"installMode": "perMachine",
|
"installMode": "perMachine",
|
||||||
"template": "./packages/windows/installer.nsi"
|
"template": "./packages/windows/installer.nsi"
|
||||||
}
|
}
|
||||||
@@ -25,10 +25,9 @@
|
|||||||
"active": true,
|
"active": true,
|
||||||
"dialog": false,
|
"dialog": false,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
|
"https://github.com/coolcoala/clash-verge-rev-lite/releases/download/updater/update-fixed-webview2.json"
|
||||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
|
|
||||||
],
|
],
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"nsis": {
|
"nsis": {
|
||||||
"displayLanguageSelector": true,
|
"displayLanguageSelector": true,
|
||||||
"installerIcon": "icons/icon.ico",
|
"installerIcon": "icons/icon.ico",
|
||||||
"languages": ["SimpChinese", "English"],
|
"languages": ["Russian", "English"],
|
||||||
"installMode": "perMachine",
|
"installMode": "perMachine",
|
||||||
"template": "./packages/windows/installer.nsi"
|
"template": "./packages/windows/installer.nsi"
|
||||||
}
|
}
|
||||||
@@ -25,10 +25,9 @@
|
|||||||
"active": true,
|
"active": true,
|
||||||
"dialog": false,
|
"dialog": false,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
|
"https://github.com/coolcoala/clash-verge-rev-lite/releases/download/updater/update-fixed-webview2.json"
|
||||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
|
|
||||||
],
|
],
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/App.tsx
@@ -1,12 +1,14 @@
|
|||||||
import { AppDataProvider } from "./providers/app-data-provider";
|
import { AppDataProvider } from "./providers/app-data-provider";
|
||||||
|
import { ThemeProvider } from "@/components/layout/theme-provider";
|
||||||
import Layout from "./pages/_layout";
|
import Layout from "./pages/_layout";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<AppDataProvider>
|
<ThemeProvider>
|
||||||
<Layout />
|
<AppDataProvider>
|
||||||
</AppDataProvider>
|
<Layout />
|
||||||
|
</AppDataProvider>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-family:
|
font-family:
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif,
|
||||||
|
"twemoji mozilla";
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|||||||
@@ -1,68 +1,30 @@
|
|||||||
import React, { useSyncExternalStore } from "react";
|
"use client";
|
||||||
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
|
|
||||||
import { CloseRounded } from "@mui/icons-material";
|
import { Toaster, toast } from "sonner";
|
||||||
|
import { useEffect, useSyncExternalStore } from "react";
|
||||||
import {
|
import {
|
||||||
subscribeNotices,
|
|
||||||
hideNotice,
|
|
||||||
getSnapshotNotices,
|
getSnapshotNotices,
|
||||||
|
hideNotice,
|
||||||
|
subscribeNotices,
|
||||||
} from "@/services/noticeService";
|
} from "@/services/noticeService";
|
||||||
|
|
||||||
export const NoticeManager: React.FC = () => {
|
export const NoticeManager = () => {
|
||||||
const currentNotices = useSyncExternalStore(
|
const currentNotices = useSyncExternalStore(
|
||||||
subscribeNotices,
|
subscribeNotices,
|
||||||
getSnapshotNotices,
|
getSnapshotNotices,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClose = (id: number) => {
|
useEffect(() => {
|
||||||
hideNotice(id);
|
for (const notice of currentNotices) {
|
||||||
};
|
const toastId = toast(notice.message, {
|
||||||
|
id: notice.id,
|
||||||
|
duration: notice.duration,
|
||||||
|
onDismiss: (t) => {
|
||||||
|
hideNotice(t.id as number);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentNotices]);
|
||||||
|
|
||||||
return (
|
return <Toaster />;
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "fixed",
|
|
||||||
top: "20px",
|
|
||||||
right: "20px",
|
|
||||||
zIndex: 1500,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "10px",
|
|
||||||
maxWidth: "360px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentNotices.map((notice) => (
|
|
||||||
<Snackbar
|
|
||||||
key={notice.id}
|
|
||||||
open={true}
|
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
|
||||||
sx={{
|
|
||||||
position: "relative",
|
|
||||||
transform: "none",
|
|
||||||
top: "auto",
|
|
||||||
right: "auto",
|
|
||||||
bottom: "auto",
|
|
||||||
left: "auto",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Alert
|
|
||||||
severity={notice.type}
|
|
||||||
variant="filled"
|
|
||||||
sx={{ width: "100%" }}
|
|
||||||
action={
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
onClick={() => handleClose(notice.id)}
|
|
||||||
>
|
|
||||||
<CloseRounded fontSize="inherit" />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{notice.message}
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import {
|
import { useTranslation } from "react-i18next";
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
type SxProps,
|
|
||||||
type Theme,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { LoadingButton } from "@mui/lab";
|
|
||||||
|
|
||||||
|
// --- Новые импорты ---
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Loader2 } from "lucide-react"; // Иконка для спиннера
|
||||||
|
|
||||||
|
// --- Интерфейсы ---
|
||||||
interface Props {
|
interface Props {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -18,12 +21,12 @@ interface Props {
|
|||||||
disableOk?: boolean;
|
disableOk?: boolean;
|
||||||
disableCancel?: boolean;
|
disableCancel?: boolean;
|
||||||
disableFooter?: boolean;
|
disableFooter?: boolean;
|
||||||
contentSx?: SxProps<Theme>;
|
className?: string; // Замена для contentSx, чтобы передавать классы Tailwind
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
onOk?: () => void;
|
onOk?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void; // onOpenChange в shadcn/ui делает то же самое
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DialogRef {
|
export interface DialogRef {
|
||||||
@@ -38,37 +41,44 @@ export const BaseDialog: React.FC<Props> = (props) => {
|
|||||||
children,
|
children,
|
||||||
okBtn,
|
okBtn,
|
||||||
cancelBtn,
|
cancelBtn,
|
||||||
contentSx,
|
className,
|
||||||
disableCancel,
|
disableCancel,
|
||||||
disableOk,
|
disableOk,
|
||||||
disableFooter,
|
disableFooter,
|
||||||
loading,
|
loading,
|
||||||
|
onClose,
|
||||||
|
onCancel,
|
||||||
|
onOk,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={props.onClose}>
|
// Управляем состоянием через onOpenChange, которое вызывает onClose
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose?.()}>
|
||||||
|
<DialogContent className={className}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogContent sx={contentSx}>{children}</DialogContent>
|
{children}
|
||||||
|
|
||||||
{!disableFooter && (
|
{!disableFooter && (
|
||||||
<DialogActions>
|
<DialogFooter>
|
||||||
{!disableCancel && (
|
{!disableCancel && (
|
||||||
<Button variant="outlined" onClick={props.onCancel}>
|
<Button variant="outline" onClick={onCancel} disabled={loading}>
|
||||||
{cancelBtn}
|
{cancelBtn || t("Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!disableOk && (
|
{!disableOk && (
|
||||||
<LoadingButton
|
<Button disabled={loading || disableOk} onClick={onOk}>
|
||||||
loading={loading}
|
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
variant="contained"
|
{okBtn || t("Confirm")}
|
||||||
onClick={props.onOk}
|
</Button>
|
||||||
>
|
)}
|
||||||
{okBtn}
|
</DialogFooter>
|
||||||
</LoadingButton>
|
)}
|
||||||
)}
|
</DialogContent>
|
||||||
</DialogActions>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { alpha, Box, Typography } from "@mui/material";
|
import { ReactNode } from "react";
|
||||||
import { InboxRounded } from "@mui/icons-material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Inbox } from "lucide-react"; // 1. Импортируем иконку из lucide-react
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text?: React.ReactNode;
|
text?: ReactNode;
|
||||||
extra?: React.ReactNode;
|
extra?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseEmpty = (props: Props) => {
|
export const BaseEmpty = (props: Props) => {
|
||||||
@@ -12,20 +12,15 @@ export const BaseEmpty = (props: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
// 2. Заменяем Box на div и переводим sx в классы Tailwind
|
||||||
sx={({ palette }) => ({
|
<div className="flex h-full w-full flex-col items-center justify-center space-y-4 text-muted-foreground/75">
|
||||||
width: "100%",
|
{/* 3. Заменяем иконку MUI на lucide-react и задаем размер классами */}
|
||||||
height: "100%",
|
<Inbox className="h-20 w-20" />
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
{/* 4. Заменяем Typography на p */}
|
||||||
alignItems: "center",
|
<p className="text-xl">{t(`${text}`)}</p>
|
||||||
justifyContent: "center",
|
|
||||||
color: alpha(palette.text.secondary, 0.75),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<InboxRounded sx={{ fontSize: "4em" }} />
|
|
||||||
<Typography sx={{ fontSize: "1.25em" }}>{t(`${text}`)}</Typography>
|
|
||||||
{extra}
|
{extra}
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AlertTriangle } from "lucide-react"; // Импортируем иконку
|
||||||
|
|
||||||
|
// Новый, стилизованный компонент для отображения ошибки
|
||||||
function ErrorFallback({ error }: FallbackProps) {
|
function ErrorFallback({ error }: FallbackProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="alert" style={{ padding: 16 }}>
|
<div
|
||||||
<h4>Something went wrong:(</h4>
|
role="alert"
|
||||||
|
className="m-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-destructive"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5" />
|
||||||
|
<h3 className="font-semibold">{t("Something went wrong")}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<pre>{error.message}</pre>
|
<pre className="mt-2 whitespace-pre-wrap rounded-md bg-destructive/10 p-2 text-xs font-mono">
|
||||||
|
{error.message}
|
||||||
|
</pre>
|
||||||
|
|
||||||
<details title="Error Stack">
|
<details className="mt-4">
|
||||||
<summary>Error Stack</summary>
|
<summary className="cursor-pointer text-xs font-medium hover:underline">
|
||||||
<pre>{error.stack}</pre>
|
{t("Error Stack")}
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-2 whitespace-pre-wrap rounded-md bg-muted p-2 text-xs font-mono text-muted-foreground">
|
||||||
|
{error.stack}
|
||||||
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,38 +1,30 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, styled } from "@mui/material";
|
import { cn } from "@root/lib/utils"; // Импортируем утилиту для объединения классов
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string;
|
label: string;
|
||||||
fontSize?: string;
|
|
||||||
width?: string;
|
|
||||||
padding?: string;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
className?: string; // Пропс для дополнительной стилизации
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BaseFieldset: React.FC<Props> = (props: Props) => {
|
export const BaseFieldset: React.FC<Props> = (props) => {
|
||||||
const Fieldset = styled(Box)<{ component?: string }>(() => ({
|
const { label, children, className } = props;
|
||||||
position: "relative",
|
|
||||||
border: "1px solid #bbb",
|
|
||||||
borderRadius: "5px",
|
|
||||||
width: props.width ?? "auto",
|
|
||||||
padding: props.padding ?? "15px",
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Label = styled("legend")(({ theme }) => ({
|
|
||||||
position: "absolute",
|
|
||||||
top: "-10px",
|
|
||||||
left: props.padding ?? "15px",
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
backgroundImage:
|
|
||||||
"linear-gradient(rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.16))",
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
fontSize: props.fontSize ?? "1em",
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset component="fieldset">
|
// 1. Используем тег fieldset для семантики. Он позиционирован как relative.
|
||||||
<Label>{props.label}</Label>
|
<fieldset
|
||||||
{props.children}
|
className={cn(
|
||||||
</Fieldset>
|
"relative rounded-md border border-border p-4", // Базовые стили
|
||||||
|
className, // Дополнительные классы от пользователя
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* 2. Используем legend. Он абсолютно спозиционирован относительно fieldset. */}
|
||||||
|
<legend className="absolute -top-2.5 left-3 bg-background px-1 text-sm text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
{/* 3. Здесь будет содержимое филдсета */}
|
||||||
|
{children}
|
||||||
|
</fieldset>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, CircularProgress } from "@mui/material";
|
import { BaseLoading } from "./base-loading"; // 1. Импортируем наш собственный компонент загрузки
|
||||||
|
import { cn } from "@root/lib/utils";
|
||||||
|
|
||||||
export interface BaseLoadingOverlayProps {
|
export interface BaseLoadingOverlayProps {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
|
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
if (!isLoading) return null;
|
if (!isLoading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
// 2. Заменяем Box на div и переводим sx в классы Tailwind
|
||||||
sx={{
|
<div
|
||||||
position: "absolute",
|
className={cn(
|
||||||
top: 0,
|
"absolute inset-0 z-50 flex items-center justify-center bg-background/70 backdrop-blur-sm",
|
||||||
left: 0,
|
className,
|
||||||
right: 0,
|
)}
|
||||||
bottom: 0,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
|
||||||
zIndex: 1000,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CircularProgress />
|
{/* 3. Используем наш BaseLoading и делаем его немного больше */}
|
||||||
</Box>
|
<BaseLoading className="h-8 w-8 text-primary" />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,14 @@
|
|||||||
import { styled } from "@mui/material";
|
import { Loader2 } from "lucide-react"; // 1. Импортируем стандартную иконку загрузки
|
||||||
|
import { cn } from "@root/lib/utils"; // Утилита для объединения классов
|
||||||
|
|
||||||
const Loading = styled("div")`
|
interface Props {
|
||||||
position: relative;
|
className?: string;
|
||||||
display: flex;
|
}
|
||||||
height: 100%;
|
|
||||||
min-height: 18px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
& > div {
|
export const BaseLoading: React.FC<Props> = ({ className }) => {
|
||||||
box-sizing: border-box;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
margin: 2px;
|
|
||||||
border-radius: 100%;
|
|
||||||
animation: loading 0.7s -0.15s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > div:nth-child(2n-1) {
|
|
||||||
animation-delay: -0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loading {
|
|
||||||
50% {
|
|
||||||
opacity: 0.2;
|
|
||||||
transform: scale(0.75);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LoadingItem = styled("div")(({ theme }) => ({
|
|
||||||
background: theme.palette.text.secondary,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const BaseLoading = () => {
|
|
||||||
return (
|
return (
|
||||||
<Loading>
|
// 2. Используем иконку с анимацией вращения от Tailwind
|
||||||
<LoadingItem />
|
// Мы можем легко менять ее размер и цвет через className
|
||||||
<LoadingItem />
|
<Loader2 className={cn("h-5 w-5 animate-spin", className)} />
|
||||||
<LoadingItem />
|
|
||||||
</Loading>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,50 +1,36 @@
|
|||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { Typography } from "@mui/material";
|
|
||||||
import { BaseErrorBoundary } from "./base-error-boundary";
|
import { BaseErrorBoundary } from "./base-error-boundary";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { cn } from "@root/lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: React.ReactNode; // the page title
|
title?: ReactNode; // Заголовок страницы
|
||||||
header?: React.ReactNode; // something behind title
|
header?: ReactNode; // Элементы в правой части шапки (кнопки и т.д.)
|
||||||
contentStyle?: React.CSSProperties;
|
children?: ReactNode; // Основное содержимое страницы
|
||||||
children?: ReactNode;
|
className?: string; // Дополнительные классы для основной области контента
|
||||||
full?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BasePage: React.FC<Props> = (props) => {
|
export const BasePage: React.FC<Props> = (props) => {
|
||||||
const { title, header, contentStyle, full, children } = props;
|
const { title, header, children, className } = props;
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const isDark = theme.palette.mode === "dark";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseErrorBoundary>
|
<BaseErrorBoundary>
|
||||||
<div className="base-page">
|
{/* 1. Корневой контейнер: flex-колонка на всю высоту */}
|
||||||
<header data-tauri-drag-region="true" style={{ userSelect: "none" }}>
|
<div className="h-full flex flex-col bg-background text-foreground">
|
||||||
<Typography
|
{/* 2. Шапка: не растягивается, имеет фиксированную высоту и нижнюю границу */}
|
||||||
sx={{ fontSize: "20px", fontWeight: "700 " }}
|
<header
|
||||||
data-tauri-drag-region="true"
|
data-tauri-drag-region="true"
|
||||||
>
|
className="flex-shrink-0 flex items-center justify-between h-16 px-4 border-b border-border"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-bold" data-tauri-drag-region="true">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</h2>
|
||||||
|
<div data-tauri-drag-region="true">{header}</div>
|
||||||
{header}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
{/* 3. Основная область: занимает все оставшееся место и прокручивается */}
|
||||||
className={full ? "base-container no-padding" : "base-container"}
|
<main className={cn("flex-1 overflow-y-auto min-h-0", className)}>
|
||||||
style={{ backgroundColor: isDark ? "#1e1f27" : "#ffffff" }}
|
{children}
|
||||||
>
|
</main>
|
||||||
<section
|
|
||||||
style={{
|
|
||||||
backgroundColor: isDark ? "#1e1f27" : "var(--background-color)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="base-content" style={contentStyle}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</BaseErrorBoundary>
|
</BaseErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { Box, SvgIcon, TextField, styled } from "@mui/material";
|
import { ChangeEvent, useEffect, useMemo, useState } from "react";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
|
||||||
import { ChangeEvent, useEffect, useRef, useState, useMemo } from "react";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
|
import { cn } from "@root/lib/utils";
|
||||||
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
|
|
||||||
import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
|
// Новые импорты
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { CaseSensitive, WholeWord, Regex } from "lucide-react"; // Иконки из lucide-react
|
||||||
|
|
||||||
export type SearchState = {
|
export type SearchState = {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -16,87 +22,55 @@ export type SearchState = {
|
|||||||
|
|
||||||
type SearchProps = {
|
type SearchProps = {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
matchCase?: boolean;
|
|
||||||
matchWholeWord?: boolean;
|
|
||||||
useRegularExpression?: boolean;
|
|
||||||
onSearch: (match: (content: string) => boolean, state: SearchState) => void;
|
onSearch: (match: (content: string) => boolean, state: SearchState) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledTextField = styled(TextField)(({ theme }) => ({
|
|
||||||
"& .MuiInputBase-root": {
|
|
||||||
background: theme.palette.mode === "light" ? "#fff" : undefined,
|
|
||||||
paddingRight: "4px",
|
|
||||||
},
|
|
||||||
"& .MuiInputBase-root svg[aria-label='active'] path": {
|
|
||||||
fill: theme.palette.primary.light,
|
|
||||||
},
|
|
||||||
"& .MuiInputBase-root svg[aria-label='inactive'] path": {
|
|
||||||
fill: "#A7A7A7",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const BaseSearchBox = (props: SearchProps) => {
|
export const BaseSearchBox = (props: SearchProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const [text, setText] = useState("");
|
||||||
const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
|
const [matchCase, setMatchCase] = useState(false);
|
||||||
const [matchWholeWord, setMatchWholeWord] = useState(
|
const [matchWholeWord, setMatchWholeWord] = useState(false);
|
||||||
props.matchWholeWord ?? false,
|
const [useRegularExpression, setUseRegularExpression] = useState(false);
|
||||||
);
|
|
||||||
const [useRegularExpression, setUseRegularExpression] = useState(
|
|
||||||
props.useRegularExpression ?? false,
|
|
||||||
);
|
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
const iconStyle = {
|
|
||||||
style: {
|
|
||||||
height: "24px",
|
|
||||||
width: "24px",
|
|
||||||
cursor: "pointer",
|
|
||||||
} as React.CSSProperties,
|
|
||||||
inheritViewBox: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const createMatcher = useMemo(() => {
|
const createMatcher = useMemo(() => {
|
||||||
return (searchText: string) => {
|
return (searchText: string) => {
|
||||||
try {
|
try {
|
||||||
|
setErrorMessage(""); // Сбрасываем ошибку при новой попытке
|
||||||
return (content: string) => {
|
return (content: string) => {
|
||||||
if (!searchText) return true;
|
if (!searchText) return true;
|
||||||
|
const flags = matchCase ? "" : "i";
|
||||||
let item = !matchCase ? content.toLowerCase() : content;
|
|
||||||
let searchItem = !matchCase ? searchText.toLowerCase() : searchText;
|
|
||||||
|
|
||||||
if (useRegularExpression) {
|
if (useRegularExpression) {
|
||||||
return new RegExp(searchItem).test(item);
|
return new RegExp(searchText, flags).test(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pattern = searchText.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); // Экранируем спецсимволы
|
||||||
if (matchWholeWord) {
|
if (matchWholeWord) {
|
||||||
return new RegExp(`\\b${searchItem}\\b`).test(item);
|
pattern = `\\b${pattern}\\b`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return item.includes(searchItem);
|
return new RegExp(pattern, flags).test(content);
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
setErrorMessage(`${err}`);
|
setErrorMessage(err.message);
|
||||||
return () => true;
|
return () => true; // Возвращаем "безопасный" матчер в случае ошибки
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [matchCase, matchWholeWord, useRegularExpression]);
|
}, [matchCase, matchWholeWord, useRegularExpression]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inputRef.current) return;
|
props.onSearch(createMatcher(text), {
|
||||||
const value = inputRef.current.value;
|
text,
|
||||||
setErrorMessage("");
|
|
||||||
props.onSearch(createMatcher(value), {
|
|
||||||
text: value,
|
|
||||||
matchCase,
|
matchCase,
|
||||||
matchWholeWord,
|
matchWholeWord,
|
||||||
useRegularExpression,
|
useRegularExpression,
|
||||||
});
|
});
|
||||||
}, [matchCase, matchWholeWord, useRegularExpression, createMatcher]);
|
}, [matchCase, matchWholeWord, useRegularExpression, createMatcher]); // Убрали text из зависимостей
|
||||||
|
|
||||||
const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target?.value ?? "";
|
const value = e.target.value;
|
||||||
setErrorMessage("");
|
setText(value);
|
||||||
props.onSearch(createMatcher(value), {
|
props.onSearch(createMatcher(value), {
|
||||||
text: value,
|
text: value,
|
||||||
matchCase,
|
matchCase,
|
||||||
@@ -105,61 +79,74 @@ export const BaseSearchBox = (props: SearchProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getToggleVariant = (isActive: boolean) =>
|
||||||
|
isActive ? "secondary" : "ghost";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={errorMessage} placement="bottom-start">
|
<div className="w-full">
|
||||||
<StyledTextField
|
<div className="relative">
|
||||||
autoComplete="new-password"
|
{/* Добавляем правый отступ, чтобы текст не заезжал под иконки */}
|
||||||
inputRef={inputRef}
|
<Input
|
||||||
hiddenLabel
|
placeholder={props.placeholder ?? t("Filter conditions")}
|
||||||
fullWidth
|
value={text}
|
||||||
size="small"
|
onChange={handleChange}
|
||||||
variant="outlined"
|
className="pr-28" // pr-[112px]
|
||||||
spellCheck="false"
|
/>
|
||||||
placeholder={props.placeholder ?? t("Filter conditions")}
|
{/* Контейнер для иконок, абсолютно спозиционированный справа */}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
<div className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
onChange={onChange}
|
<TooltipProvider delayDuration={100}>
|
||||||
slotProps={{
|
<Tooltip>
|
||||||
input: {
|
<TooltipTrigger asChild>
|
||||||
sx: { pr: 1 },
|
<Button
|
||||||
endAdornment: (
|
variant={getToggleVariant(matchCase)}
|
||||||
<Box display="flex">
|
size="icon"
|
||||||
<Tooltip title={t("Match Case")}>
|
className="h-7 w-7"
|
||||||
<div>
|
onClick={() => setMatchCase(!matchCase)}
|
||||||
<SvgIcon
|
>
|
||||||
component={matchCaseIcon}
|
<CaseSensitive className="h-4 w-4" />
|
||||||
{...iconStyle}
|
</Button>
|
||||||
aria-label={matchCase ? "active" : "inactive"}
|
</TooltipTrigger>
|
||||||
onClick={() => setMatchCase(!matchCase)}
|
<TooltipContent>
|
||||||
/>
|
<p>{t("Match Case")}</p>
|
||||||
</div>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("Match Whole Word")}>
|
<Tooltip>
|
||||||
<div>
|
<TooltipTrigger asChild>
|
||||||
<SvgIcon
|
<Button
|
||||||
component={matchWholeWordIcon}
|
variant={getToggleVariant(matchWholeWord)}
|
||||||
{...iconStyle}
|
size="icon"
|
||||||
aria-label={matchWholeWord ? "active" : "inactive"}
|
className="h-7 w-7"
|
||||||
onClick={() => setMatchWholeWord(!matchWholeWord)}
|
onClick={() => setMatchWholeWord(!matchWholeWord)}
|
||||||
/>
|
>
|
||||||
</div>
|
<WholeWord className="h-4 w-4" />
|
||||||
</Tooltip>
|
</Button>
|
||||||
<Tooltip title={t("Use Regular Expression")}>
|
</TooltipTrigger>
|
||||||
<div>
|
<TooltipContent>
|
||||||
<SvgIcon
|
<p>{t("Match Whole Word")}</p>
|
||||||
component={useRegularExpressionIcon}
|
</TooltipContent>
|
||||||
aria-label={useRegularExpression ? "active" : "inactive"}
|
</Tooltip>
|
||||||
{...iconStyle}
|
<Tooltip>
|
||||||
onClick={() =>
|
<TooltipTrigger asChild>
|
||||||
setUseRegularExpression(!useRegularExpression)
|
<Button
|
||||||
}
|
variant={getToggleVariant(useRegularExpression)}
|
||||||
/>{" "}
|
size="icon"
|
||||||
</div>
|
className="h-7 w-7"
|
||||||
</Tooltip>
|
onClick={() => setUseRegularExpression(!useRegularExpression)}
|
||||||
</Box>
|
>
|
||||||
),
|
<Regex className="h-4 w-4" />
|
||||||
},
|
</Button>
|
||||||
}}
|
</TooltipTrigger>
|
||||||
/>
|
<TooltipContent>
|
||||||
</Tooltip>
|
<p>{t("Use Regular Expression")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Отображение ошибки под полем ввода */}
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="mt-1 text-xs text-destructive">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,37 @@
|
|||||||
import { Select, SelectProps, styled } from "@mui/material";
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { cn } from "@root/lib/utils";
|
||||||
|
|
||||||
|
// Определяем новые пропсы для нашего компонента
|
||||||
|
export interface BaseStyledSelectProps {
|
||||||
|
children: React.ReactNode; // Сюда будут передаваться <SelectItem>
|
||||||
|
value?: string;
|
||||||
|
onValueChange?: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string; // для дополнительной стилизации
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseStyledSelect: React.FC<BaseStyledSelectProps> = (props) => {
|
||||||
|
const { value, onValueChange, placeholder, children, className } = props;
|
||||||
|
|
||||||
export const BaseStyledSelect = styled((props: SelectProps<string>) => {
|
|
||||||
return (
|
return (
|
||||||
<Select
|
// Используем композицию компонентов Select из shadcn/ui
|
||||||
size="small"
|
<Select value={value} onValueChange={onValueChange}>
|
||||||
autoComplete="new-password"
|
<SelectTrigger
|
||||||
sx={{
|
className={cn(
|
||||||
width: 120,
|
"h-9 w-[180px]", // Задаем стандартные размеры, как у других селектов
|
||||||
height: 33.375,
|
className,
|
||||||
mr: 1,
|
)}
|
||||||
'[role="button"]': { py: 0.65 },
|
>
|
||||||
}}
|
<SelectValue placeholder={placeholder} />
|
||||||
{...props}
|
</SelectTrigger>
|
||||||
/>
|
<SelectContent>{children}</SelectContent>
|
||||||
|
</Select>
|
||||||
);
|
);
|
||||||
})(({ theme }) => ({
|
};
|
||||||
background: theme.palette.mode === "light" ? "#fff" : undefined,
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
import { TextField, type TextFieldProps, styled } from "@mui/material";
|
import * as React from "react"; // 1. Убедимся, что React импортирован
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { cn } from "@root/lib/utils";
|
||||||
|
import { Input } from "@/components/ui/input"; // 2. Убираем импорт несуществующего типа InputProps
|
||||||
|
|
||||||
export const BaseStyledTextField = styled((props: TextFieldProps) => {
|
// 3. Определяем наши пропсы, расширяя стандартный тип для input-элементов из React
|
||||||
|
export interface BaseStyledTextFieldProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
export const BaseStyledTextField = React.forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
BaseStyledTextFieldProps // Используем наш правильный тип
|
||||||
|
>((props, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { className, ...restProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<Input
|
||||||
autoComplete="new-password"
|
ref={ref}
|
||||||
hiddenLabel
|
className={cn(
|
||||||
fullWidth
|
"h-9", // Задаем стандартную компактную высоту
|
||||||
size="small"
|
className,
|
||||||
variant="outlined"
|
)}
|
||||||
|
placeholder={props.placeholder ?? t("Filter conditions")}
|
||||||
|
autoComplete="off"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder={t("Filter conditions")}
|
{...restProps}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})(({ theme }) => ({
|
});
|
||||||
"& .MuiInputBase-root": {
|
|
||||||
background: theme.palette.mode === "light" ? "#fff" : undefined,
|
BaseStyledTextField.displayName = "BaseStyledTextField";
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,58 +1,16 @@
|
|||||||
import { styled } from "@mui/material/styles";
|
import * as React from "react";
|
||||||
import { default as MuiSwitch, SwitchProps } from "@mui/material/Switch";
|
import { Switch as ShadcnSwitch } from "@/components/ui/switch";
|
||||||
|
import { cn } from "@root/lib/utils";
|
||||||
|
|
||||||
export const Switch = styled((props: SwitchProps) => (
|
// Тип пропсов остается без изменений
|
||||||
<MuiSwitch
|
export type SwitchProps = React.ComponentPropsWithoutRef<typeof ShadcnSwitch>;
|
||||||
focusVisibleClassName=".Mui-focusVisible"
|
|
||||||
disableRipple
|
const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
||||||
{...props}
|
({ className, ...props }, ref) => {
|
||||||
/>
|
return <ShadcnSwitch className={cn(className)} ref={ref} {...props} />;
|
||||||
))(({ theme }) => ({
|
|
||||||
width: 42,
|
|
||||||
height: 26,
|
|
||||||
padding: 0,
|
|
||||||
marginRight: 1,
|
|
||||||
"& .MuiSwitch-switchBase": {
|
|
||||||
padding: 0,
|
|
||||||
margin: 2,
|
|
||||||
transitionDuration: "300ms",
|
|
||||||
"&.Mui-checked": {
|
|
||||||
transform: "translateX(16px)",
|
|
||||||
color: "#fff",
|
|
||||||
"& + .MuiSwitch-track": {
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
opacity: 1,
|
|
||||||
border: 0,
|
|
||||||
},
|
|
||||||
"&.Mui-disabled + .MuiSwitch-track": {
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"&.Mui-focusVisible .MuiSwitch-thumb": {
|
|
||||||
color: "#33cf4d",
|
|
||||||
border: "6px solid #fff",
|
|
||||||
},
|
|
||||||
"&.Mui-disabled .MuiSwitch-thumb": {
|
|
||||||
color:
|
|
||||||
theme.palette.mode === "light"
|
|
||||||
? theme.palette.grey[100]
|
|
||||||
: theme.palette.grey[600],
|
|
||||||
},
|
|
||||||
"&.Mui-disabled + .MuiSwitch-track": {
|
|
||||||
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"& .MuiSwitch-thumb": {
|
);
|
||||||
boxSizing: "border-box",
|
|
||||||
width: 22,
|
Switch.displayName = "Switch";
|
||||||
height: 22,
|
|
||||||
},
|
export { Switch };
|
||||||
"& .MuiSwitch-track": {
|
|
||||||
borderRadius: 26 / 2,
|
|
||||||
backgroundColor: theme.palette.mode === "light" ? "#BBBBBB" : "#39393D",
|
|
||||||
opacity: 1,
|
|
||||||
transition: theme.transitions.create(["background-color"], {
|
|
||||||
duration: 500,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,24 +1,52 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@root/lib/utils";
|
||||||
|
|
||||||
|
// 1. Убираем импорт несуществующего типа ButtonProps
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
IconButton,
|
TooltipContent,
|
||||||
IconButtonProps,
|
TooltipProvider,
|
||||||
SvgIconProps,
|
TooltipTrigger,
|
||||||
} from "@mui/material";
|
} from "@/components/ui/tooltip";
|
||||||
import { InfoRounded } from "@mui/icons-material";
|
import { Info } from "lucide-react";
|
||||||
|
|
||||||
interface Props extends IconButtonProps {
|
// 2. Определяем наши пропсы, расширяя стандартный тип для кнопок из React
|
||||||
title?: string;
|
export interface TooltipIconProps
|
||||||
icon?: React.ElementType<SvgIconProps>;
|
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
tooltip: React.ReactNode;
|
||||||
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TooltipIcon: React.FC<Props> = (props: Props) => {
|
export const TooltipIcon = React.forwardRef<
|
||||||
const { title = "", icon: Icon = InfoRounded, ...restProps } = props;
|
HTMLButtonElement,
|
||||||
|
TooltipIconProps
|
||||||
|
>(({ tooltip, icon, className, ...props }, ref) => {
|
||||||
|
const displayIcon = icon || <Info className="h-4 w-4" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={title} placement="top">
|
<TooltipProvider>
|
||||||
<IconButton color="inherit" size="small" {...restProps}>
|
<Tooltip>
|
||||||
<Icon fontSize="inherit" style={{ cursor: "pointer", opacity: 0.75 }} />
|
<TooltipTrigger asChild>
|
||||||
</IconButton>
|
<Button
|
||||||
</Tooltip>
|
ref={ref}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn("h-7 w-7 text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{displayIcon}
|
||||||
|
<span className="sr-only">
|
||||||
|
{typeof tooltip === "string" ? tooltip : "Icon button"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{typeof tooltip === "string" ? <p>{tooltip}</p> : tooltip}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
TooltipIcon.displayName = "TooltipIcon";
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Box, Button, Snackbar, useTheme } from "@mui/material";
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
import { deleteConnection } from "@/services/api";
|
import { deleteConnection } from "@/services/api";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export interface ConnectionDetailRef {
|
export interface ConnectionDetailRef {
|
||||||
open: (detail: IConnectionsItem) => void;
|
open: (detail: IConnectionsItem) => void;
|
||||||
@@ -14,38 +20,37 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
|||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: (detail: IConnectionsItem) => {
|
open: (detail: IConnectionsItem) => {
|
||||||
if (open) return;
|
|
||||||
setOpen(true);
|
|
||||||
setDetail(detail);
|
setDetail(detail);
|
||||||
|
setOpen(true);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const onClose = () => setOpen(false);
|
const handleOpenChange = (isOpen: boolean) => {
|
||||||
|
setOpen(isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!detail) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Snackbar
|
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
<SheetContent
|
||||||
open={open}
|
side="right"
|
||||||
onClose={onClose}
|
className="w-full max-w-[520px] max-h-[100vh] sm:max-h-[calc(100vh-2rem)] overflow-y-auto p-0 flex flex-col"
|
||||||
sx={{
|
>
|
||||||
".MuiSnackbarContent-root": {
|
<SheetHeader className="p-6 pb-4">
|
||||||
maxWidth: "520px",
|
<SheetTitle>{t("Connection Details")}</SheetTitle>
|
||||||
maxHeight: "480px",
|
</SheetHeader>
|
||||||
overflowY: "auto",
|
<div className="flex-grow overflow-y-auto p-6 pt-0">
|
||||||
backgroundColor: theme.palette.background.paper,
|
<InnerConnectionDetail
|
||||||
color: theme.palette.text.primary,
|
data={detail}
|
||||||
},
|
onClose={() => setOpen(false)}
|
||||||
}}
|
/>
|
||||||
message={
|
</div>
|
||||||
detail ? (
|
</SheetContent>
|
||||||
<InnerConnectionDetail data={detail} onClose={onClose} />
|
</Sheet>
|
||||||
) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -57,7 +62,6 @@ interface InnerProps {
|
|||||||
|
|
||||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||||
const { metadata, rulePayload } = data;
|
const { metadata, rulePayload } = data;
|
||||||
const theme = useTheme();
|
|
||||||
const chains = [...data.chains].reverse().join(" / ");
|
const chains = [...data.chains].reverse().join(" / ");
|
||||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||||
const host = metadata.host
|
const host = metadata.host
|
||||||
@@ -103,24 +107,16 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>
|
<div className="select-text text-muted-foreground">
|
||||||
{information.map((each) => (
|
{information.map((each) => (
|
||||||
<div key={each.label}>
|
<div key={each.label} className="mb-1">
|
||||||
<b>{each.label}</b>
|
<b className="text-foreground">{each.label}</b>
|
||||||
<span
|
<span className="break-all text-foreground">: {each.value}</span>
|
||||||
style={{
|
|
||||||
wordBreak: "break-all",
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
: {each.value}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Box sx={{ textAlign: "right" }}>
|
<div className="text-right mt-4">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
|
||||||
title={t("Close Connection")}
|
title={t("Close Connection")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onDelete();
|
onDelete();
|
||||||
@@ -129,7 +125,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
>
|
>
|
||||||
{t("Close Connection")}
|
{t("Close Connection")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import {
|
import { X } from "lucide-react";
|
||||||
styled,
|
import { Button } from "@/components/ui/button";
|
||||||
ListItem,
|
|
||||||
IconButton,
|
|
||||||
ListItemText,
|
|
||||||
Box,
|
|
||||||
alpha,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { CloseRounded } from "@mui/icons-material";
|
|
||||||
import { deleteConnection } from "@/services/api";
|
import { deleteConnection } from "@/services/api";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
|
||||||
const Tag = styled("span")(({ theme }) => ({
|
interface TagProps {
|
||||||
fontSize: "10px",
|
children: React.ReactNode;
|
||||||
padding: "0 4px",
|
className?: string;
|
||||||
lineHeight: 1.375,
|
}
|
||||||
border: "1px solid",
|
|
||||||
borderRadius: 4,
|
const Tag: React.FC<TagProps> = ({ children, className }) => {
|
||||||
borderColor: alpha(theme.palette.text.secondary, 0.35),
|
const baseClasses =
|
||||||
marginTop: "4px",
|
"text-[10px] px-1 leading-[1.375] border rounded-[4px] border-muted-foreground/35";
|
||||||
marginRight: "4px",
|
return (
|
||||||
}));
|
<span className={`${baseClasses} ${className || ""}`}>{children}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: IConnectionsItem;
|
value: IConnectionsItem;
|
||||||
@@ -37,43 +32,42 @@ export const ConnectionItem = (props: Props) => {
|
|||||||
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
|
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<div className="flex items-center justify-between p-3 border-b border-border dark:border-border">
|
||||||
dense
|
<div
|
||||||
sx={{ borderBottom: "1px solid var(--divider-color)" }}
|
className="flex-grow select-text cursor-pointer mr-2"
|
||||||
secondaryAction={
|
|
||||||
<IconButton edge="end" color="inherit" onClick={onDelete}>
|
|
||||||
<CloseRounded />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
sx={{ userSelect: "text", cursor: "pointer" }}
|
|
||||||
primary={metadata.host || metadata.destinationIP}
|
|
||||||
onClick={onShowDetail}
|
onClick={onShowDetail}
|
||||||
secondary={
|
>
|
||||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
<div className="text-sm font-medium text-foreground">
|
||||||
<Tag sx={{ textTransform: "uppercase", color: "success" }}>
|
{metadata.host || metadata.destinationIP}
|
||||||
{metadata.network}
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
<Tag className="uppercase text-green-600 dark:text-green-500">
|
||||||
|
{metadata.network}
|
||||||
|
</Tag>
|
||||||
|
|
||||||
|
<Tag>{metadata.type}</Tag>
|
||||||
|
|
||||||
|
{!!metadata.process && <Tag>{metadata.process}</Tag>}
|
||||||
|
|
||||||
|
{chains?.length > 0 && <Tag>{[...chains].reverse().join(" / ")}</Tag>}
|
||||||
|
|
||||||
|
<Tag>{dayjs(start).fromNow()}</Tag>
|
||||||
|
|
||||||
|
{showTraffic && (
|
||||||
|
<Tag>
|
||||||
|
{parseTraffic(curUpload!)} / {parseTraffic(curDownload!)}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
)}
|
||||||
<Tag>{metadata.type}</Tag>
|
</div>
|
||||||
|
</div>
|
||||||
{!!metadata.process && <Tag>{metadata.process}</Tag>}
|
<Button
|
||||||
|
variant="ghost"
|
||||||
{chains?.length > 0 && (
|
size="icon"
|
||||||
<Tag>{[...chains].reverse().join(" / ")}</Tag>
|
onClick={onDelete}
|
||||||
)}
|
className="ml-2 flex-shrink-0"
|
||||||
|
>
|
||||||
<Tag>{dayjs(start).fromNow()}</Tag>
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
{showTraffic && (
|
</div>
|
||||||
<Tag>
|
|
||||||
{parseTraffic(curUpload!)} / {parseTraffic(curDownload!)}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,139 +1,77 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useMemo, useState, useEffect } from "react";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
|
import React, { useMemo, useState, useEffect, RefObject } from "react";
|
||||||
import { useThemeMode } from "@/services/states";
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
Row,
|
||||||
|
ColumnSizingState,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { TableVirtuoso, TableComponents } from "react-virtuoso";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
import { truncateStr } from "@/utils/truncate-str";
|
import { truncateStr } from "@/utils/truncate-str";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { cn } from "@root/lib/utils";
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
// Интерфейс для строки данных, которую использует react-table
|
||||||
|
interface ConnectionRow {
|
||||||
|
id: string;
|
||||||
|
host: string;
|
||||||
|
download: number;
|
||||||
|
upload: number;
|
||||||
|
dlSpeed: number;
|
||||||
|
ulSpeed: number;
|
||||||
|
chains: string;
|
||||||
|
rule: string;
|
||||||
|
process: string;
|
||||||
|
time: string;
|
||||||
|
source: string;
|
||||||
|
remoteDestination: string;
|
||||||
|
type: string;
|
||||||
|
connectionData: IConnectionsItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Интерфейс для пропсов, которые компонент получает от родителя
|
||||||
interface Props {
|
interface Props {
|
||||||
connections: IConnectionsItem[];
|
connections: IConnectionsItem[];
|
||||||
onShowDetail: (data: IConnectionsItem) => void;
|
onShowDetail: (data: IConnectionsItem) => void;
|
||||||
|
scrollerRef: (element: HTMLElement | Window | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectionTable = (props: Props) => {
|
export const ConnectionTable = (props: Props) => {
|
||||||
const { connections, onShowDetail } = props;
|
const { connections, onShowDetail, scrollerRef } = props;
|
||||||
const mode = useThemeMode();
|
|
||||||
const isDark = mode === "light" ? false : true;
|
|
||||||
const backgroundColor = isDark ? "#282A36" : "#ffffff";
|
|
||||||
|
|
||||||
const [columnVisible, setColumnVisible] = useState<
|
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>(() => {
|
||||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
try {
|
||||||
>({});
|
|
||||||
|
|
||||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
|
|
||||||
() => {
|
|
||||||
const saved = localStorage.getItem("connection-table-widths");
|
const saved = localStorage.getItem("connection-table-widths");
|
||||||
return saved ? JSON.parse(saved) : {};
|
return saved ? JSON.parse(saved) : {};
|
||||||
},
|
} catch {
|
||||||
);
|
return {};
|
||||||
|
}
|
||||||
const [columns] = useState<GridColDef[]>([
|
});
|
||||||
{
|
|
||||||
field: "host",
|
|
||||||
headerName: t("Host"),
|
|
||||||
width: columnWidths["host"] || 220,
|
|
||||||
minWidth: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "download",
|
|
||||||
headerName: t("Downloaded"),
|
|
||||||
width: columnWidths["download"] || 88,
|
|
||||||
align: "right",
|
|
||||||
headerAlign: "right",
|
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "upload",
|
|
||||||
headerName: t("Uploaded"),
|
|
||||||
width: columnWidths["upload"] || 88,
|
|
||||||
align: "right",
|
|
||||||
headerAlign: "right",
|
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "dlSpeed",
|
|
||||||
headerName: t("DL Speed"),
|
|
||||||
width: columnWidths["dlSpeed"] || 88,
|
|
||||||
align: "right",
|
|
||||||
headerAlign: "right",
|
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "ulSpeed",
|
|
||||||
headerName: t("UL Speed"),
|
|
||||||
width: columnWidths["ulSpeed"] || 88,
|
|
||||||
align: "right",
|
|
||||||
headerAlign: "right",
|
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "chains",
|
|
||||||
headerName: t("Chains"),
|
|
||||||
width: columnWidths["chains"] || 340,
|
|
||||||
minWidth: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "rule",
|
|
||||||
headerName: t("Rule"),
|
|
||||||
width: columnWidths["rule"] || 280,
|
|
||||||
minWidth: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "process",
|
|
||||||
headerName: t("Process"),
|
|
||||||
width: columnWidths["process"] || 220,
|
|
||||||
minWidth: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "time",
|
|
||||||
headerName: t("Time"),
|
|
||||||
width: columnWidths["time"] || 120,
|
|
||||||
minWidth: 100,
|
|
||||||
align: "right",
|
|
||||||
headerAlign: "right",
|
|
||||||
sortComparator: (v1: string, v2: string) =>
|
|
||||||
new Date(v2).getTime() - new Date(v1).getTime(),
|
|
||||||
valueFormatter: (value: number) => dayjs(value).fromNow(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "source",
|
|
||||||
headerName: t("Source"),
|
|
||||||
width: columnWidths["source"] || 200,
|
|
||||||
minWidth: 130,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "remoteDestination",
|
|
||||||
headerName: t("Destination"),
|
|
||||||
width: columnWidths["remoteDestination"] || 200,
|
|
||||||
minWidth: 130,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "type",
|
|
||||||
headerName: t("Type"),
|
|
||||||
width: columnWidths["type"] || 160,
|
|
||||||
minWidth: 100,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Saving column widths:", columnWidths);
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"connection-table-widths",
|
"connection-table-widths",
|
||||||
JSON.stringify(columnWidths),
|
JSON.stringify(columnSizing),
|
||||||
);
|
);
|
||||||
}, [columnWidths]);
|
}, [columnSizing]);
|
||||||
|
|
||||||
const handleColumnResize = (params: GridColumnResizeParams) => {
|
const connRows = useMemo((): ConnectionRow[] => {
|
||||||
const { colDef, width } = params;
|
|
||||||
console.log("Column resize:", colDef.field, width);
|
|
||||||
setColumnWidths((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[colDef.field]: width,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const connRows = useMemo(() => {
|
|
||||||
return connections.map((each) => {
|
return connections.map((each) => {
|
||||||
const { metadata, rulePayload } = each;
|
const { metadata, rulePayload } = each;
|
||||||
const chains = [...each.chains].reverse().join(" / ");
|
const chains = [...each.chains].reverse().join(" / ");
|
||||||
@@ -148,11 +86,11 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
||||||
download: each.download,
|
download: each.download,
|
||||||
upload: each.upload,
|
upload: each.upload,
|
||||||
dlSpeed: each.curDownload,
|
dlSpeed: each.curDownload ?? 0,
|
||||||
ulSpeed: each.curUpload,
|
ulSpeed: each.curUpload ?? 0,
|
||||||
chains,
|
chains,
|
||||||
rule,
|
rule,
|
||||||
process: truncateStr(metadata.process || metadata.processPath),
|
process: truncateStr(metadata.process || metadata.processPath) ?? "",
|
||||||
time: each.start,
|
time: each.start,
|
||||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||||
remoteDestination: Destination,
|
remoteDestination: Destination,
|
||||||
@@ -162,24 +100,198 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
});
|
});
|
||||||
}, [connections]);
|
}, [connections]);
|
||||||
|
|
||||||
|
const columns = useMemo<ColumnDef<ConnectionRow>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: "host",
|
||||||
|
header: () => t("Host"),
|
||||||
|
size: columnSizing?.host || 220,
|
||||||
|
minSize: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "download",
|
||||||
|
header: () => t("Downloaded"),
|
||||||
|
size: columnSizing?.download || 88,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<div className="text-right">
|
||||||
|
{parseTraffic(getValue<number>()).join(" ")}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "upload",
|
||||||
|
header: () => t("Uploaded"),
|
||||||
|
size: columnSizing?.upload || 88,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<div className="text-right">
|
||||||
|
{parseTraffic(getValue<number>()).join(" ")}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "dlSpeed",
|
||||||
|
header: () => t("DL Speed"),
|
||||||
|
size: columnSizing?.dlSpeed || 88,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<div className="text-right">
|
||||||
|
{parseTraffic(getValue<number>()).join(" ")}/s
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "ulSpeed",
|
||||||
|
header: () => t("UL Speed"),
|
||||||
|
size: columnSizing?.ulSpeed || 88,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<div className="text-right">
|
||||||
|
{parseTraffic(getValue<number>()).join(" ")}/s
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "chains",
|
||||||
|
header: () => t("Chains"),
|
||||||
|
size: columnSizing?.chains || 340,
|
||||||
|
minSize: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "rule",
|
||||||
|
header: () => t("Rule"),
|
||||||
|
size: columnSizing?.rule || 280,
|
||||||
|
minSize: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "process",
|
||||||
|
header: () => t("Process"),
|
||||||
|
size: columnSizing?.process || 220,
|
||||||
|
minSize: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "time",
|
||||||
|
header: () => t("Time"),
|
||||||
|
size: columnSizing?.time || 120,
|
||||||
|
minSize: 100,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<div className="text-right">
|
||||||
|
{dayjs(getValue<string>()).fromNow()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "source",
|
||||||
|
header: () => t("Source"),
|
||||||
|
size: columnSizing?.source || 200,
|
||||||
|
minSize: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "remoteDestination",
|
||||||
|
header: () => t("Destination"),
|
||||||
|
size: columnSizing?.remoteDestination || 200,
|
||||||
|
minSize: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "type",
|
||||||
|
header: () => t("Type"),
|
||||||
|
size: columnSizing?.type || 160,
|
||||||
|
minSize: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[columnSizing],
|
||||||
|
);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: connRows,
|
||||||
|
columns,
|
||||||
|
state: { columnSizing },
|
||||||
|
onColumnSizingChange: setColumnSizing,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
columnResizeMode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const VirtuosoTableComponents = useMemo<TableComponents<Row<ConnectionRow>>>(
|
||||||
|
() => ({
|
||||||
|
// Явно типизируем `ref` для каждого компонента
|
||||||
|
Scroller: React.forwardRef<HTMLDivElement>((props, ref) => (
|
||||||
|
<div className="h-full" {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
Table: (props) => <Table {...props} className="w-full border-collapse" />,
|
||||||
|
TableHead: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||||
|
<TableHeader {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
// Явно типизируем пропсы и `ref` для TableRow
|
||||||
|
TableRow: React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
{ item: Row<ConnectionRow> } & React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ item: row, ...props }, ref) => {
|
||||||
|
// `Virtuoso` передает нам готовую строку `row` в пропсе `item`.
|
||||||
|
// Больше не нужно искать ее по индексу!
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
className="cursor-pointer hover:bg-muted/50"
|
||||||
|
onClick={() => onShowDetail(row.original.connectionData)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||||
|
<TableBody {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataGrid
|
<div className="h-full rounded-md border overflow-hidden">
|
||||||
hideFooter
|
{connRows.length > 0 ? (
|
||||||
rows={connRows}
|
<TableVirtuoso
|
||||||
columns={columns}
|
scrollerRef={scrollerRef}
|
||||||
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
data={table.getRowModel().rows}
|
||||||
density="compact"
|
components={VirtuosoTableComponents}
|
||||||
sx={{
|
fixedHeaderContent={() =>
|
||||||
border: "none",
|
table.getHeaderGroups().map((headerGroup) => (
|
||||||
"div:focus": { outline: "none !important" },
|
<TableRow
|
||||||
"& .MuiDataGrid-columnHeader": {
|
key={headerGroup.id}
|
||||||
userSelect: "none",
|
className="hover:bg-transparent bg-background/95 backdrop-blur"
|
||||||
},
|
>
|
||||||
}}
|
{headerGroup.headers.map((header) => (
|
||||||
columnVisibilityModel={columnVisible}
|
<TableHead
|
||||||
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
key={header.id}
|
||||||
onColumnResize={handleColumnResize}
|
style={{ width: header.getSize() }}
|
||||||
disableColumnMenu={false}
|
className="p-2"
|
||||||
/>
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
itemContent={(index, row) => (
|
||||||
|
<>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
className="p-2 whitespace-nowrap"
|
||||||
|
onClick={() => onShowDetail(row.original.connectionData)}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center">
|
||||||
|
<p>No results.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const formatUptime = (uptimeMs: number) => {
|
|||||||
export const ClashInfoCard = () => {
|
export const ClashInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { version: clashVersion } = useClash();
|
const { version: clashVersion } = useClash();
|
||||||
const { clashConfig, sysproxy, rules, uptime } = useAppData();
|
const { clashConfig, rules, uptime, systemProxyAddress } = useAppData();
|
||||||
|
|
||||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||||
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
|
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
|
||||||
@@ -42,7 +42,7 @@ export const ClashInfoCard = () => {
|
|||||||
{t("System Proxy Address")}
|
{t("System Proxy Address")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{sysproxy?.server || "-"}
|
{systemProxyAddress}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -74,7 +74,14 @@ export const ClashInfoCard = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}, [clashConfig, clashVersion, t, formattedUptime, rules.length, sysproxy]);
|
}, [
|
||||||
|
clashConfig,
|
||||||
|
clashVersion,
|
||||||
|
t,
|
||||||
|
formattedUptime,
|
||||||
|
rules.length,
|
||||||
|
systemProxyAddress,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Box, Typography, Paper, Stack, Fade } from "@mui/material";
|
import { Box, Typography, Paper, Stack } from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { closeAllConnections } from "@/services/api";
|
import { closeAllConnections } from "@/services/api";
|
||||||
import { patchClashMode } from "@/services/cmds";
|
import { patchClashMode } from "@/services/cmds";
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { useClashInfo } from "@/hooks/use-clash";
|
|||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { createAuthSockette } from "@/utils/websocket";
|
import { createAuthSockette } from "@/utils/websocket";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { getConnections, isDebugEnabled, gc } from "@/services/api";
|
import { isDebugEnabled, gc } from "@/services/api";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useAppData } from "@/providers/app-data-provider";
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
|
|||||||