Merge branch 'dev' into fix/downgrade-date-grid
This commit is contained in:
49
.github/workflows/autobuild.yml
vendored
49
.github/workflows/autobuild.yml
vendored
@@ -90,20 +90,20 @@ jobs:
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
@@ -169,7 +169,8 @@ jobs:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-shared
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -197,6 +198,14 @@ jobs:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -259,7 +268,8 @@ jobs:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-shared
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -272,6 +282,14 @@ jobs:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -391,7 +409,8 @@ jobs:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-shared
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -404,6 +423,14 @@ jobs:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -538,20 +565,20 @@ jobs:
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
12
.github/workflows/lint-clippy.yml
vendored
12
.github/workflows/lint-clippy.yml
vendored
@@ -59,9 +59,10 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: false
|
||||
cache-all-crates: false
|
||||
shared-key: autobuild-shared
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -72,3 +73,10 @@ jobs:
|
||||
- name: Run Clippy
|
||||
working-directory: ./src-tauri
|
||||
run: cargo clippy-all
|
||||
|
||||
- name: Run Logging Check
|
||||
working-directory: ./src-tauri
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
|
||||
clash-verge-logging-check
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ scripts/_env.sh
|
||||
.idea
|
||||
.old
|
||||
.eslintcache
|
||||
target
|
||||
@@ -27,6 +27,11 @@ if [ -n "$RUST_FILES" ]; then
|
||||
(
|
||||
cd src-tauri
|
||||
cargo clippy-all
|
||||
if ! command -v clash-verge-logging-check >/dev/null 2>&1; then
|
||||
echo "[pre-commit] Installing clash-verge-logging-check..."
|
||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
|
||||
fi
|
||||
clash-verge-logging-check
|
||||
)
|
||||
fi
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
We welcome translations and improvements to existing locales. Please follow the detailed guidelines in [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md) for instructions on extracting strings, file naming conventions, testing translations, and submitting translation PRs.
|
||||
|
||||
## Development Setup
|
||||
|
||||
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
|
||||
|
||||
80
UPDATELOG.md
80
UPDATELOG.md
@@ -1,6 +1,45 @@
|
||||
## v2.4.3
|
||||
|
||||
### ✨ 新增功能
|
||||
感谢 @Slinetrac, @oomeow 以及 @Lythrilla 的出色贡献
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 优化服务模式重装逻辑,避免不必要的重复检查
|
||||
- 修复轻量模式退出无响应的问题
|
||||
- 修复托盘轻量模式支持退出/进入
|
||||
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
|
||||
- macOS Tun/系统代理 模式下图标大小不统一
|
||||
- 托盘节点切换不再显示隐藏组
|
||||
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
|
||||
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
|
||||
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
|
||||
- 修复 Webdav 恢复备份不重启
|
||||
- 修复 Linux 开机后无法正常代理需要手动设置
|
||||
- 修复增加订阅或导入订阅文件时订阅页面无更新
|
||||
- 修复系统代理守卫功能不工作
|
||||
- 修复 KDE + Wayland 下多屏显示 UI 异常
|
||||
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
|
||||
- 修复静默启动不加载完整 WebView 的问题
|
||||
- 修复 Linux WebKit 网络进程的崩溃
|
||||
- 修复无法导入订阅
|
||||
- 修复实际导入成功但显示导入失败的问题
|
||||
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
|
||||
- 修复删除订阅时未能实际删除相关文件
|
||||
- 修复 macOS 连接界面显示异常
|
||||
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
|
||||
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
|
||||
- 修复自动更新使版本回退的问题
|
||||
- 修复首页自定义卡片在切换轻量模式时失效
|
||||
- 修复悬浮跳转导航失效
|
||||
- 修复小键盘热键映射错误
|
||||
- 修复前端无法及时刷新操作状态
|
||||
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
|
||||
- 修复 Linux 系统主题切换不生效
|
||||
- 修复 `允许自动更新` 字段使手动订阅刷新失效
|
||||
- 修复连接界面长时间显示后报错问题
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.15**
|
||||
- 支持前端修改日志(最大文件大小、最大保留数量)
|
||||
@@ -15,8 +54,11 @@
|
||||
- 允许独立控制订阅自动更新
|
||||
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
|
||||
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
|
||||
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
|
||||
</details>
|
||||
|
||||
### 🚀 优化改进
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 重构并简化服务模式启动检测流程,消除重复检测
|
||||
- 重构并简化窗口创建流程
|
||||
@@ -42,37 +84,11 @@
|
||||
- 优化首页当前节点对MATCH规则的支持
|
||||
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
|
||||
- 添加热键绑定错误的提示信息
|
||||
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
|
||||
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
|
||||
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 优化服务模式重装逻辑,避免不必要的重复检查
|
||||
- 修复轻量模式退出无响应的问题
|
||||
- 修复托盘轻量模式支持退出/进入
|
||||
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
|
||||
- macOS Tun/系统代理 模式下图标大小不统一
|
||||
- 托盘节点切换不再显示隐藏组
|
||||
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
|
||||
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
|
||||
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
|
||||
- 修复 Webdav 恢复备份不重启
|
||||
- 修复 Linux 开机后无法正常代理需要手动设置
|
||||
- 修复增加订阅或导入订阅文件时订阅页面无更新
|
||||
- 修复系统代理守卫功能不工作
|
||||
- 修复 KDE + Wayland 下多屏显示 UI 异常
|
||||
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
|
||||
- 修复静默启动不加载完整 WebView 的问题
|
||||
- 修复 Linux WebKit 网络进程的崩溃
|
||||
- 修复无法导入订阅
|
||||
- 修复实际导入成功但显示导入失败的问题
|
||||
- 修复删除订阅时未能实际删除相关文件
|
||||
- 修复 macOS 连接界面显示异常
|
||||
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
|
||||
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
|
||||
- 修复自动更新使版本回退的问题
|
||||
- 修复首页自定义卡片在切换轻量模式时失效
|
||||
- 修复悬浮跳转导航失效
|
||||
- 修复小键盘热键映射错误
|
||||
- 修复连接界面长时间显示后报错问题
|
||||
</details>
|
||||
|
||||
## v2.4.2
|
||||
|
||||
|
||||
81
docs/CONTRIBUTING_i18n.md
Normal file
81
docs/CONTRIBUTING_i18n.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# CONTRIBUTING — i18n
|
||||
|
||||
Thank you for considering contributing to our localization work — your help is appreciated.
|
||||
|
||||
Quick overview
|
||||
|
||||
- cvr-i18 is a CLI that helps manage simple top-level JSON locale files:
|
||||
- Detect duplicated top-level keys
|
||||
- Find keys missing versus a base file (default: en.json)
|
||||
- Export missing entries for translators
|
||||
- Reorder keys to match the base file for predictable diffs
|
||||
- Operate on a directory or a single file
|
||||
|
||||
Get the CLI (No binary provided yet)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
||||
cd clash-verge-rev-i18n-cli
|
||||
cargo install --path .
|
||||
# or
|
||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
||||
```
|
||||
|
||||
Common commands
|
||||
|
||||
- Show help: `cvr-i18`
|
||||
- Directory (auto-detects `./locales` or `./src/locales`): `cvr-i18 -d /path/to/locales`
|
||||
- Check duplicates: `cvr-i18 -k`
|
||||
- Check missing keys: `cvr-i18 -m`
|
||||
- Export missing keys: `cvr-i18 -m -e ./exports`
|
||||
- Sort keys to base file: `cvr-i18 -s`
|
||||
- Use a base file: `cvr-i18 -b base.json`
|
||||
- Single file: `cvr-i18 -f locales/zh.json`
|
||||
|
||||
Options (short)
|
||||
|
||||
- `-d, --directory <DIR>`
|
||||
- `-f, --file <FILE>`
|
||||
- `-k, --duplicated-key`
|
||||
- `-m, --missing-key`
|
||||
- `-e, --export <DIR>`
|
||||
- `-s, --sort`
|
||||
- `-b, --base <FILE>`
|
||||
|
||||
Exit codes
|
||||
|
||||
- `0` — success (no issues)
|
||||
- `1` — issues found (duplicates/missing)
|
||||
- `2` — error (IO/parse/runtime)
|
||||
|
||||
How to contribute (recommended steps)
|
||||
|
||||
- Start small: fix typos, improve phrasing, or refine tone and consistency.
|
||||
- Run the CLI against your locale files to detect duplicates or missing keys.
|
||||
- Export starter JSONs for translators with `-m -e <DIR>`.
|
||||
- Prefer incremental PRs or draft PRs; leave a comment on the issue if you want guidance.
|
||||
- Open an issue to report missing strings, UI context, or localization bugs.
|
||||
- Add or improve docs and tests to make future contributions easier.
|
||||
|
||||
PR checklist
|
||||
|
||||
- Keep JSON files UTF-8 encoded.
|
||||
- Follow the repo’s locale file structure and naming conventions.
|
||||
- Reorder keys to match the base file (`-s`) for minimal diffs.
|
||||
- Test translations in a local dev build before opening a PR.
|
||||
- Reference related issues and explain any context for translations or changes.
|
||||
|
||||
Notes
|
||||
|
||||
- The tool expects simple top-level JSON key/value maps.
|
||||
- Exported JSONs are starter files for translators (fill in values, keep keys).
|
||||
- Sorting keeps diffs consistent and reviewable.
|
||||
|
||||
Repository
|
||||
https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
||||
|
||||
## Feedback & Contributions
|
||||
|
||||
- For tool usage issues or feedback: please open an Issue in the [repository](https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli) so it can be tracked and addressed.
|
||||
- For localization contributions (translations, fixes, context notes, etc.): submit a PR or Issue in this repository and include examples, context, and testing instructions when possible.
|
||||
- If you need help or a review, leave a comment on your submission requesting assistance.
|
||||
@@ -94,9 +94,10 @@ export default defineConfig([
|
||||
"warn",
|
||||
{
|
||||
vars: "all",
|
||||
varsIgnorePattern: "^_+$",
|
||||
varsIgnorePattern: "^_",
|
||||
args: "after-used",
|
||||
argsIgnorePattern: "^_+$",
|
||||
argsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^ignore",
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
53
package.json
53
package.json
@@ -26,8 +26,8 @@
|
||||
"publish-version": "node scripts/publish-version.mjs",
|
||||
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
||||
"clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml",
|
||||
"lint": "eslint -c eslint.config.ts --cache --cache-location .eslintcache src",
|
||||
"lint:fix": "eslint -c eslint.config.ts --cache --cache-location .eslintcache --fix src",
|
||||
"lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src",
|
||||
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
@@ -45,17 +45,17 @@
|
||||
"@mui/material": "^7.3.4",
|
||||
"@mui/x-data-grid": "^7.29.9",
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.1",
|
||||
"@tauri-apps/plugin-fs": "^2.4.3",
|
||||
"@tauri-apps/plugin-http": "~2.5.3",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "2.3.2",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-http": "~2.5.4",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "2.3.3",
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.9.6",
|
||||
"axios": "^1.12.2",
|
||||
"dayjs": "1.11.18",
|
||||
"axios": "^1.13.1",
|
||||
"dayjs": "1.11.19",
|
||||
"foxact": "^0.2.49",
|
||||
"i18next": "^25.6.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -67,34 +67,33 @@
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-i18next": "16.2.1",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-i18next": "16.2.3",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.59.0",
|
||||
"react-router": "^7.9.4",
|
||||
"react-router": "^7.9.5",
|
||||
"react-virtuoso": "^4.14.1",
|
||||
"swr": "^2.3.6",
|
||||
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.8"
|
||||
"types-pac": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@eslint-react/eslint-plugin": "^2.2.4",
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@tauri-apps/cli": "2.9.1",
|
||||
"@eslint-react/eslint-plugin": "^2.3.1",
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@tauri-apps/cli": "2.9.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.9.1",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react": "5.1.0",
|
||||
"@vitejs/plugin-react-swc": "^4.2.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cli-color": "^2.0.4",
|
||||
"commander": "^14.0.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
@@ -103,7 +102,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"glob": "^11.0.3",
|
||||
"globals": "^16.4.0",
|
||||
"globals": "^16.5.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.6.1",
|
||||
@@ -111,19 +110,19 @@
|
||||
"meta-json-schema": "^1.19.14",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.93.2",
|
||||
"tar": "^7.5.1",
|
||||
"sass": "^1.93.3",
|
||||
"tar": "^7.5.2",
|
||||
"terser": "^5.44.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"vite": "^7.1.12",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-monaco-editor-esm": "^2.0.2",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vitest": "^4.0.3"
|
||||
"vitest": "^4.0.6"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
"eslint --fix",
|
||||
"eslint --fix --max-warnings=0",
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
|
||||
977
pnpm-lock.yaml
generated
977
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,6 @@
|
||||
"matchCurrentVersion": "<8.0.0"
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"postUpdateOptions": ["pnpmDedupe", "updateCargoLock"],
|
||||
"ignoreDeps": ["criterion"]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import { log_debug, log_error, log_info, log_success } from "./utils.mjs";
|
||||
* 3. Use file hash to detect changes and skip unnecessary chmod/copy operations
|
||||
* 4. Use --force or -f flag to force re-download and update all resources
|
||||
*
|
||||
* This optimization significantly reduces build time for local development
|
||||
*/
|
||||
|
||||
const cwd = process.cwd();
|
||||
@@ -56,8 +55,7 @@ const ARCH_MAP = {
|
||||
|
||||
const arg1 = process.argv.slice(2)[0];
|
||||
const arg2 = process.argv.slice(2)[1];
|
||||
let target;
|
||||
target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
|
||||
let target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
|
||||
const { platform, arch } = target
|
||||
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
||||
: process;
|
||||
@@ -68,7 +66,9 @@ const SIDECAR_HOST = target
|
||||
.toString()
|
||||
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||
|
||||
/* ======= Version Cache Functions ======= */
|
||||
// =======================
|
||||
// Version Cache
|
||||
// =======================
|
||||
async function loadVersionCache() {
|
||||
try {
|
||||
if (fs.existsSync(VERSION_CACHE_FILE)) {
|
||||
@@ -80,7 +80,6 @@ async function loadVersionCache() {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function saveVersionCache(cache) {
|
||||
try {
|
||||
await fsp.mkdir(TEMP_DIR, { recursive: true });
|
||||
@@ -90,28 +89,24 @@ async function saveVersionCache(cache) {
|
||||
log_debug("Failed to save version cache:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCachedVersion(key) {
|
||||
const cache = await loadVersionCache();
|
||||
const cached = cache[key];
|
||||
if (cached && Date.now() - cached.timestamp < 3600000) {
|
||||
// 1小时内有效
|
||||
log_info(`Using cached version for ${key}: ${cached.version}`);
|
||||
return cached.version;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function setCachedVersion(key, version) {
|
||||
const cache = await loadVersionCache();
|
||||
cache[key] = {
|
||||
version,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
cache[key] = { version, timestamp: Date.now() };
|
||||
await saveVersionCache(cache);
|
||||
}
|
||||
|
||||
/* ======= File Hash Functions ======= */
|
||||
// =======================
|
||||
// Hash Cache & File Hash
|
||||
// =======================
|
||||
async function calculateFileHash(filePath) {
|
||||
try {
|
||||
const fileBuffer = await fsp.readFile(filePath);
|
||||
@@ -122,7 +117,6 @@ async function calculateFileHash(filePath) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHashCache() {
|
||||
try {
|
||||
if (fs.existsSync(HASH_CACHE_FILE)) {
|
||||
@@ -134,7 +128,6 @@ async function loadHashCache() {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function saveHashCache(cache) {
|
||||
try {
|
||||
await fsp.mkdir(TEMP_DIR, { recursive: true });
|
||||
@@ -144,28 +137,20 @@ async function saveHashCache(cache) {
|
||||
log_debug("Failed to save hash cache:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function hasFileChanged(filePath, targetPath) {
|
||||
if (FORCE) return true;
|
||||
if (!fs.existsSync(targetPath)) return true;
|
||||
|
||||
const hashCache = await loadHashCache();
|
||||
const sourceHash = await calculateFileHash(filePath);
|
||||
const targetHash = await calculateFileHash(targetPath);
|
||||
|
||||
if (!sourceHash || !targetHash) return true;
|
||||
|
||||
const cacheKey = targetPath;
|
||||
const cachedHash = hashCache[cacheKey];
|
||||
|
||||
if (cachedHash === sourceHash && sourceHash === targetHash) {
|
||||
// 文件未变化,不输出日志
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function updateHashCache(targetPath) {
|
||||
const hashCache = await loadHashCache();
|
||||
const hash = await calculateFileHash(targetPath);
|
||||
@@ -175,18 +160,25 @@ async function updateHashCache(targetPath) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ======= clash meta alpha======= */
|
||||
// =======================
|
||||
// Meta maps (stable & alpha)
|
||||
// =======================
|
||||
const META_ALPHA_VERSION_URL =
|
||||
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
|
||||
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
|
||||
let META_ALPHA_VERSION;
|
||||
|
||||
const META_VERSION_URL =
|
||||
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
|
||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
||||
let META_VERSION;
|
||||
|
||||
const META_ALPHA_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v1",
|
||||
"darwin-arm64": "mihomo-darwin-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v1-go122",
|
||||
"darwin-arm64": "mihomo-darwin-arm64-go122",
|
||||
"linux-x64": "mihomo-linux-amd64-v2",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
@@ -195,9 +187,24 @@ const META_ALPHA_MAP = {
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// Fetch the latest alpha release version from the version.txt file
|
||||
const META_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v2-go122",
|
||||
"darwin-arm64": "mihomo-darwin-arm64-go122",
|
||||
"linux-x64": "mihomo-linux-amd64-v2",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
"linux-riscv64": "mihomo-linux-riscv64",
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// =======================
|
||||
// Fetch latest versions
|
||||
// =======================
|
||||
async function getLatestAlphaVersion() {
|
||||
// 如果不强制更新,先尝试从缓存获取
|
||||
if (!FORCE) {
|
||||
const cached = await getCachedVersion("META_ALPHA_VERSION");
|
||||
if (cached) {
|
||||
@@ -205,58 +212,33 @@ async function getLatestAlphaVersion() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_ALPHA_VERSION_URL, {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
let v = await response.text();
|
||||
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Failed to fetch ${META_ALPHA_VERSION_URL}: ${response.status}`,
|
||||
);
|
||||
META_ALPHA_VERSION = (await response.text()).trim();
|
||||
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`);
|
||||
|
||||
// 保存到缓存
|
||||
await setCachedVersion("META_ALPHA_VERSION", META_ALPHA_VERSION);
|
||||
} catch (error) {
|
||||
log_error("Error fetching latest alpha version:", error.message);
|
||||
} catch (err) {
|
||||
log_error("Error fetching latest alpha version:", err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ======= clash meta stable ======= */
|
||||
const META_VERSION_URL =
|
||||
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
|
||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
||||
let META_VERSION;
|
||||
|
||||
const META_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v2",
|
||||
"darwin-arm64": "mihomo-darwin-arm64",
|
||||
"linux-x64": "mihomo-linux-amd64-v2",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
"linux-riscv64": "mihomo-linux-riscv64",
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// Fetch the latest release version from the version.txt file
|
||||
async function getLatestReleaseVersion() {
|
||||
// 如果不强制更新,先尝试从缓存获取
|
||||
if (!FORCE) {
|
||||
const cached = await getCachedVersion("META_VERSION");
|
||||
if (cached) {
|
||||
@@ -264,67 +246,57 @@ async function getLatestReleaseVersion() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_VERSION_URL, {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
let v = await response.text();
|
||||
META_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Failed to fetch ${META_VERSION_URL}: ${response.status}`,
|
||||
);
|
||||
META_VERSION = (await response.text()).trim();
|
||||
log_info(`Latest release version: ${META_VERSION}`);
|
||||
|
||||
// 保存到缓存
|
||||
await setCachedVersion("META_VERSION", META_VERSION);
|
||||
} catch (error) {
|
||||
log_error("Error fetching latest release version:", error.message);
|
||||
} catch (err) {
|
||||
log_error("Error fetching latest release version:", err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* check available
|
||||
*/
|
||||
// =======================
|
||||
// Validate availability
|
||||
// =======================
|
||||
if (!META_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`,
|
||||
);
|
||||
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`);
|
||||
}
|
||||
|
||||
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* core info
|
||||
*/
|
||||
// =======================
|
||||
// Build meta objects
|
||||
// =======================
|
||||
function clashMetaAlpha() {
|
||||
const name = META_ALPHA_MAP[`${platform}-${arch}`];
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo-alpha",
|
||||
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
exeFile: `${name}${isWin ? ".exe" : ""}`,
|
||||
zipFile: `${name}-${META_ALPHA_VERSION}.${urlExt}`,
|
||||
downloadURL: `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -332,40 +304,83 @@ function clashMeta() {
|
||||
const name = META_MAP[`${platform}-${arch}`];
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo",
|
||||
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
exeFile: `${name}${isWin ? ".exe" : ""}`,
|
||||
zipFile: `${name}-${META_VERSION}.${urlExt}`,
|
||||
downloadURL: `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* download sidecar and rename
|
||||
*/
|
||||
|
||||
// =======================
|
||||
// download helper (增强:status + magic bytes)
|
||||
// =======================
|
||||
async function downloadFile(url, outPath) {
|
||||
const options = {};
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => "");
|
||||
// 将 body 写到文件以便排查(可通过临时目录查看)
|
||||
await fsp.mkdir(path.dirname(outPath), { recursive: true });
|
||||
await fsp.writeFile(outPath, body);
|
||||
throw new Error(`Failed to download ${url}: status ${response.status}`);
|
||||
}
|
||||
|
||||
const buf = Buffer.from(await response.arrayBuffer());
|
||||
await fsp.mkdir(path.dirname(outPath), { recursive: true });
|
||||
|
||||
// 简单 magic 字节检查
|
||||
if (url.endsWith(".gz") || url.endsWith(".tgz")) {
|
||||
if (!(buf[0] === 0x1f && buf[1] === 0x8b)) {
|
||||
await fsp.writeFile(outPath, buf);
|
||||
throw new Error(
|
||||
`Downloaded file for ${url} is not a valid gzip (magic mismatch).`,
|
||||
);
|
||||
}
|
||||
} else if (url.endsWith(".zip")) {
|
||||
if (!(buf[0] === 0x50 && buf[1] === 0x4b)) {
|
||||
await fsp.writeFile(outPath, buf);
|
||||
throw new Error(
|
||||
`Downloaded file for ${url} is not a valid zip (magic mismatch).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await fsp.writeFile(outPath, buf);
|
||||
log_success(`download finished: ${url}`);
|
||||
}
|
||||
|
||||
// =======================
|
||||
// resolveSidecar (支持 zip / tgz / gz)
|
||||
// =======================
|
||||
async function resolveSidecar(binInfo) {
|
||||
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
|
||||
|
||||
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
||||
const sidecarPath = path.join(sidecarDir, targetFile);
|
||||
|
||||
await fsp.mkdir(sidecarDir, { recursive: true });
|
||||
|
||||
// 检查文件是否已存在,如果存在则跳过重复下载
|
||||
if (!FORCE && fs.existsSync(sidecarPath)) {
|
||||
log_success(`"${name}" already exists, skipping download to save time`);
|
||||
log_success(`"${name}" already exists, skipping download`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tempDir = path.join(TEMP_DIR, name);
|
||||
const tempZip = path.join(tempDir, zipFile);
|
||||
const tempExe = path.join(tempDir, exeFile);
|
||||
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(tempZip)) {
|
||||
await downloadFile(downloadURL, tempZip);
|
||||
@@ -374,78 +389,76 @@ async function resolveSidecar(binInfo) {
|
||||
if (zipFile.endsWith(".zip")) {
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
log_debug(`"${name}" entry name`, entry.entryName);
|
||||
log_debug(`"${name}" entry: ${entry.entryName}`);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
await fsp.rename(tempExe, sidecarPath);
|
||||
// 尝试按 exeFile 重命名,否则找第一个可执行文件
|
||||
if (fs.existsSync(tempExe)) {
|
||||
await fsp.rename(tempExe, sidecarPath);
|
||||
} else {
|
||||
// 搜索候选
|
||||
const files = await fsp.readdir(tempDir);
|
||||
const candidate = files.find(
|
||||
(f) =>
|
||||
f === path.basename(exeFile) ||
|
||||
f.endsWith(".exe") ||
|
||||
!f.includes("."),
|
||||
);
|
||||
if (!candidate)
|
||||
throw new Error(`Expected binary not found in ${tempDir}`);
|
||||
await fsp.rename(path.join(tempDir, candidate), sidecarPath);
|
||||
}
|
||||
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`unzip finished: "${name}"`);
|
||||
} else if (zipFile.endsWith(".tgz")) {
|
||||
// tgz
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
await extract({
|
||||
cwd: tempDir,
|
||||
file: tempZip,
|
||||
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
|
||||
});
|
||||
await extract({ cwd: tempDir, file: tempZip });
|
||||
const files = await fsp.readdir(tempDir);
|
||||
log_debug(`"${name}" files in tempDir:`, files);
|
||||
const extractedFile = files.find((file) => file.startsWith("虚空终端-"));
|
||||
if (extractedFile) {
|
||||
const extractedFilePath = path.join(tempDir, extractedFile);
|
||||
await fsp.rename(extractedFilePath, sidecarPath);
|
||||
log_success(`"${name}" file renamed to "${sidecarPath}"`);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`chmod binary finished: "${name}"`);
|
||||
} else {
|
||||
throw new Error(`Expected file not found in ${tempDir}`);
|
||||
}
|
||||
log_debug(`"${name}" extracted files:`, files);
|
||||
// 优先寻找给定 exeFile 或已知前缀
|
||||
let extracted = files.find(
|
||||
(f) =>
|
||||
f === path.basename(exeFile) ||
|
||||
f.startsWith("虚空终端-") ||
|
||||
!f.includes("."),
|
||||
);
|
||||
if (!extracted) extracted = files[0];
|
||||
if (!extracted) throw new Error(`Expected file not found in ${tempDir}`);
|
||||
await fsp.rename(path.join(tempDir, extracted), sidecarPath);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`tgz processed: "${name}"`);
|
||||
} else {
|
||||
// gz
|
||||
// .gz
|
||||
const readStream = fs.createReadStream(tempZip);
|
||||
const writeStream = fs.createWriteStream(sidecarPath);
|
||||
await new Promise((resolve, reject) => {
|
||||
const onError = (error) => {
|
||||
log_error(`"${name}" gz failed:`, error.message);
|
||||
reject(error);
|
||||
};
|
||||
readStream
|
||||
.pipe(zlib.createGunzip().on("error", onError))
|
||||
.pipe(zlib.createGunzip())
|
||||
.on("error", (e) => {
|
||||
log_error(`gunzip error for ${name}:`, e.message);
|
||||
reject(e);
|
||||
})
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`chmod binary finished: "${name}"`);
|
||||
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
|
||||
resolve();
|
||||
})
|
||||
.on("error", onError);
|
||||
.on("error", (e) => {
|
||||
log_error(`write stream error for ${name}:`, e.message);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
log_success(`gz binary processed: "${name}"`);
|
||||
}
|
||||
} catch (err) {
|
||||
// 需要删除文件
|
||||
await fsp.rm(sidecarPath, { recursive: true, force: true });
|
||||
throw err;
|
||||
} finally {
|
||||
// delete temp dir
|
||||
await fsp.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const resolveSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "set_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/set_dns.sh"),
|
||||
});
|
||||
const resolveUnSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "unset_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/unset_dns.sh"),
|
||||
});
|
||||
|
||||
/**
|
||||
* download the file to the resources dir
|
||||
*/
|
||||
async function resolveResource(binInfo) {
|
||||
const { file, downloadURL, localPath } = binInfo;
|
||||
|
||||
const resDir = path.join(cwd, "src-tauri/resources");
|
||||
const targetPath = path.join(resDir, file);
|
||||
|
||||
@@ -465,12 +478,9 @@ async function resolveResource(binInfo) {
|
||||
}
|
||||
|
||||
if (localPath) {
|
||||
// 检查文件哈希是否变化
|
||||
if (!(await hasFileChanged(localPath, targetPath))) {
|
||||
// 文件未变化,静默跳过
|
||||
return;
|
||||
}
|
||||
|
||||
await fsp.mkdir(resDir, { recursive: true });
|
||||
await fsp.copyFile(localPath, targetPath);
|
||||
await updateHashCache(targetPath);
|
||||
@@ -480,44 +490,17 @@ async function resolveResource(binInfo) {
|
||||
log_success(`${file} finished`);
|
||||
}
|
||||
|
||||
/**
|
||||
* download file and save to `path`
|
||||
*/ async function downloadFile(url, path) {
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
const buffer = await response.arrayBuffer();
|
||||
await fsp.writeFile(path, new Uint8Array(buffer));
|
||||
|
||||
log_success(`download finished: ${url}`);
|
||||
}
|
||||
|
||||
// SimpleSC.dll
|
||||
// SimpleSC.dll (win plugin)
|
||||
const resolvePlugin = async () => {
|
||||
const url =
|
||||
"https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip";
|
||||
|
||||
const tempDir = path.join(TEMP_DIR, "SimpleSC");
|
||||
const tempZip = path.join(
|
||||
tempDir,
|
||||
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip",
|
||||
);
|
||||
const tempDll = path.join(tempDir, "SimpleSC.dll");
|
||||
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
|
||||
const pluginDir = path.join(process.env.APPDATA || "", "Local/NSIS");
|
||||
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
|
||||
await fsp.mkdir(pluginDir, { recursive: true });
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
@@ -527,18 +510,33 @@ const resolvePlugin = async () => {
|
||||
await downloadFile(url, tempZip);
|
||||
}
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
log_debug(`"SimpleSC" entry name`, entry.entryName);
|
||||
});
|
||||
zip
|
||||
.getEntries()
|
||||
.forEach((entry) => log_debug(`"SimpleSC" entry`, entry.entryName));
|
||||
zip.extractAllTo(tempDir, true);
|
||||
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
|
||||
log_success(`unzip finished: "SimpleSC"`);
|
||||
if (fs.existsSync(tempDll)) {
|
||||
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
|
||||
log_success(`unzip finished: "SimpleSC"`);
|
||||
} else {
|
||||
// 如果 dll 名称不同,尝试找到 dll
|
||||
const files = await fsp.readdir(tempDir);
|
||||
const dll = files.find((f) => f.toLowerCase().endsWith(".dll"));
|
||||
if (dll) {
|
||||
await fsp.cp(path.join(tempDir, dll), pluginPath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
log_success(`unzip finished: "SimpleSC" (found ${dll})`);
|
||||
} else {
|
||||
throw new Error("SimpleSC.dll not found in zip");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await fsp.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
// service chmod
|
||||
// service chmod (保留并使用 glob)
|
||||
const resolveServicePermission = async () => {
|
||||
const serviceExecutables = [
|
||||
"clash-verge-service*",
|
||||
@@ -550,23 +548,20 @@ const resolveServicePermission = async () => {
|
||||
let hasChanges = false;
|
||||
|
||||
for (let f of serviceExecutables) {
|
||||
// 使用glob模块来处理通配符
|
||||
const files = glob.sync(path.join(resDir, f));
|
||||
for (let filePath of files) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const currentHash = await calculateFileHash(filePath);
|
||||
const cacheKey = `${filePath}_chmod`;
|
||||
|
||||
// 检查文件哈希是否变化
|
||||
if (!FORCE && hashCache[cacheKey] === currentHash) {
|
||||
// 权限未变化,静默跳过
|
||||
continue;
|
||||
}
|
||||
|
||||
execSync(`chmod 755 ${filePath}`);
|
||||
log_success(`chmod finished: "${filePath}"`);
|
||||
|
||||
// 更新哈希缓存
|
||||
try {
|
||||
execSync(`chmod 755 ${filePath}`);
|
||||
log_success(`chmod finished: "${filePath}"`);
|
||||
} catch (e) {
|
||||
log_error(`chmod failed for ${filePath}:`, e.message);
|
||||
}
|
||||
hashCache[cacheKey] = currentHash;
|
||||
hasChanges = true;
|
||||
}
|
||||
@@ -578,34 +573,22 @@ const resolveServicePermission = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 在 resolveResource 函数后添加新函数
|
||||
// resolve locales (从 src/locales 复制到 resources/locales,并使用 hash 检查)
|
||||
async function resolveLocales() {
|
||||
const srcLocalesDir = path.join(cwd, "src/locales");
|
||||
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
|
||||
|
||||
try {
|
||||
// 确保目标目录存在
|
||||
await fsp.mkdir(targetLocalesDir, { recursive: true });
|
||||
|
||||
// 读取所有语言文件
|
||||
const files = await fsp.readdir(srcLocalesDir);
|
||||
|
||||
// 复制每个文件,只有当哈希变化时才复制
|
||||
for (const file of files) {
|
||||
const srcPath = path.join(srcLocalesDir, file);
|
||||
const targetPath = path.join(targetLocalesDir, file);
|
||||
|
||||
// 检查文件是否需要更新
|
||||
if (!(await hasFileChanged(srcPath, targetPath))) {
|
||||
// 文件未变化,静默跳过
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(await hasFileChanged(srcPath, targetPath))) continue;
|
||||
await fsp.copyFile(srcPath, targetPath);
|
||||
await updateHashCache(targetPath);
|
||||
log_success(`Copied locale file: ${file}`);
|
||||
}
|
||||
|
||||
log_success("All locale files processed successfully");
|
||||
} catch (err) {
|
||||
log_error("Error copying locale files:", err.message);
|
||||
@@ -613,34 +596,30 @@ async function resolveLocales() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* main
|
||||
*/
|
||||
// =======================
|
||||
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
|
||||
// =======================
|
||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
|
||||
|
||||
const resolveService = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
resolveResource({
|
||||
return resolveResource({
|
||||
file: "clash-verge-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveInstall = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
resolveResource({
|
||||
return resolveResource({
|
||||
file: "clash-verge-service-install" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveUninstall = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
|
||||
resolveResource({
|
||||
return resolveResource({
|
||||
file: "clash-verge-service-uninstall" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
|
||||
});
|
||||
@@ -666,15 +645,27 @@ const resolveEnableLoopback = () =>
|
||||
file: "enableLoopback.exe",
|
||||
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
|
||||
});
|
||||
|
||||
const resolveWinSysproxy = () =>
|
||||
resolveResource({
|
||||
file: "sysproxy.exe",
|
||||
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
|
||||
});
|
||||
|
||||
const resolveSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "set_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/set_dns.sh"),
|
||||
});
|
||||
const resolveUnSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "unset_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/unset_dns.sh"),
|
||||
});
|
||||
|
||||
// =======================
|
||||
// Tasks
|
||||
// =======================
|
||||
const tasks = [
|
||||
// { name: "clash", func: resolveClash, retry: 5 },
|
||||
{
|
||||
name: "verge-mihomo-alpha",
|
||||
func: () =>
|
||||
@@ -724,11 +715,7 @@ const tasks = [
|
||||
retry: 5,
|
||||
macosOnly: true,
|
||||
},
|
||||
{
|
||||
name: "locales",
|
||||
func: resolveLocales,
|
||||
retry: 2,
|
||||
},
|
||||
{ name: "locales", func: resolveLocales, retry: 2 },
|
||||
];
|
||||
|
||||
async function runTask() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from "axios";
|
||||
import { readFileSync } from "fs";
|
||||
import { log_success, log_error, log_info } from "./utils.mjs";
|
||||
import { log_error, log_info, log_success } from "./utils.mjs";
|
||||
|
||||
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
|
||||
const CHAT_ID_TEST = "@vergetest"; // 测试频道
|
||||
@@ -71,6 +71,19 @@ async function sendTelegramNotification() {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function normalizeDetailsTags(content) {
|
||||
return content
|
||||
.replace(
|
||||
/<summary>\s*<strong>\s*(.*?)\s*<\/strong>\s*<\/summary>/g,
|
||||
"\n<b>$1</b>\n",
|
||||
)
|
||||
.replace(/<summary>\s*(.*?)\s*<\/summary>/g, "\n<b>$1</b>\n")
|
||||
.replace(/<\/?details>/g, "")
|
||||
.replace(/<\/?strong>/g, (m) => (m === "</strong>" ? "</b>" : "<b>"))
|
||||
.replace(/<br\s*\/?>/g, "\n");
|
||||
}
|
||||
|
||||
releaseContent = normalizeDetailsTags(releaseContent);
|
||||
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
|
||||
|
||||
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
avoid-breaking-exported-api = true
|
||||
avoid-breaking-exported-api = true
|
||||
cognitive-complexity-threshold = 25
|
||||
527
src-tauri/Cargo.lock
generated
527
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@ tauri-build = { version = "2.5.1", features = [] }
|
||||
[dependencies]
|
||||
warp = { version = "0.4.2", features = ["server"] }
|
||||
anyhow = "1.0.100"
|
||||
dirs = "6.0"
|
||||
open = "5.3.2"
|
||||
log = "0.4.28"
|
||||
dunce = "1.0.5"
|
||||
@@ -43,7 +42,7 @@ serde = { version = "1.0.228", features = ["derive"] }
|
||||
reqwest = { version = "0.12.24", features = ["json", "cookies"] }
|
||||
regex = "1.12.2"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
tauri = { version = "2.9.1", features = [
|
||||
tauri = { version = "2.9.2", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
@@ -51,13 +50,13 @@ tauri = { version = "2.9.1", features = [
|
||||
"image-png",
|
||||
] }
|
||||
network-interface = { version = "2.0.3", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.3.2"
|
||||
tauri-plugin-dialog = "2.4.1"
|
||||
tauri-plugin-fs = "2.4.3"
|
||||
tauri-plugin-process = "2.3.0"
|
||||
tauri-plugin-clipboard-manager = "2.3.1"
|
||||
tauri-plugin-deep-link = "2.4.4"
|
||||
tauri-plugin-window-state = "2.4.0"
|
||||
tauri-plugin-shell = "2.3.3"
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-fs = "2.4.4"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
tauri-plugin-deep-link = "2.4.5"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
zip = "6.0.0"
|
||||
reqwest_dav = "0.2.2"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
@@ -67,12 +66,8 @@ futures = "0.3.31"
|
||||
sys-locale = "0.3.2"
|
||||
libc = "0.2.177"
|
||||
gethostname = "1.1.0"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.9"
|
||||
hex = "0.4.3"
|
||||
scopeguard = "1.2.0"
|
||||
dashmap = "6.1.0"
|
||||
tauri-plugin-notification = "2.3.2"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
tokio-stream = "0.1.17"
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"text-decoding",
|
||||
@@ -80,20 +75,17 @@ isahc = { version = "1.7.2", default-features = false, features = [
|
||||
] }
|
||||
backoff = { version = "0.4.0", features = ["tokio"] }
|
||||
compact_str = { version = "0.9.0", features = ["serde"] }
|
||||
tauri-plugin-http = "2.5.3"
|
||||
tauri-plugin-http = "2.5.4"
|
||||
flexi_logger = "0.31.7"
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
console-subscriber = { version = "0.5.0", optional = true }
|
||||
tauri-plugin-devtools = { version = "2.0.1" }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
|
||||
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
|
||||
async-trait = "0.1.89"
|
||||
smartstring = { version = "1.0.1", features = ["serde"] }
|
||||
clash_verge_service_ipc = { version = "2.0.17", features = [
|
||||
clash_verge_service_ipc = { version = "2.0.21", features = [
|
||||
"client",
|
||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||
# clash_verge_service_ipc = { version = "2.0.17", features = [
|
||||
# "client",
|
||||
# ], path = "../../clash-verge-service-ipc" }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
@@ -126,8 +118,8 @@ users = "0.11.0"
|
||||
signal-hook = "0.3.18"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.5.0"
|
||||
tauri-plugin-global-shortcut = "2.3.0"
|
||||
tauri-plugin-autostart = "2.5.1"
|
||||
tauri-plugin-global-shortcut = "2.3.1"
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
|
||||
[features]
|
||||
@@ -232,3 +224,4 @@ needless_raw_string_hashes = "deny" # Too many in existing code
|
||||
#restriction = { level = "allow", priority = -1 }
|
||||
|
||||
or_fun_call = "deny"
|
||||
cognitive_complexity = "deny"
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::hint::black_box;
|
||||
use std::process;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
// 引入业务模型 & Draft 实现
|
||||
use app_lib::config::IVerge;
|
||||
use app_lib::utils::Draft as DraftNew;
|
||||
|
||||
@@ -17,108 +16,86 @@ fn make_draft() -> DraftNew<Box<IVerge>> {
|
||||
DraftNew::from(verge)
|
||||
}
|
||||
|
||||
/// 基准:只读 data_ref(正式数据)
|
||||
fn bench_data_ref(c: &mut Criterion) {
|
||||
c.bench_function("draft_data_ref", |b| {
|
||||
b.iter(|| {
|
||||
let draft = make_draft();
|
||||
let data = draft.data_ref();
|
||||
black_box(data.enable_auto_launch);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:可写 data_mut(正式数据)
|
||||
fn bench_data_mut(c: &mut Criterion) {
|
||||
c.bench_function("draft_data_mut", |b| {
|
||||
b.iter(|| {
|
||||
let draft = make_draft();
|
||||
let mut data = draft.data_mut();
|
||||
data.enable_tun_mode = Some(true);
|
||||
black_box(data.enable_tun_mode);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:首次创建草稿(会触发 clone)
|
||||
fn bench_draft_mut_first(c: &mut Criterion) {
|
||||
c.bench_function("draft_draft_mut_first", |b| {
|
||||
b.iter(|| {
|
||||
let draft = make_draft();
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
black_box(d.enable_auto_launch);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:重复 draft_mut(已存在草稿,不再 clone)
|
||||
fn bench_draft_mut_existing(c: &mut Criterion) {
|
||||
c.bench_function("draft_draft_mut_existing", |b| {
|
||||
b.iter(|| {
|
||||
let draft = make_draft();
|
||||
{
|
||||
let mut first = draft.draft_mut();
|
||||
first.enable_tun_mode = Some(true);
|
||||
}
|
||||
let mut second = draft.draft_mut();
|
||||
second.enable_tun_mode = Some(false);
|
||||
black_box(second.enable_tun_mode);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:零拷贝读取最新视图(latest_ref)
|
||||
fn bench_latest_ref(c: &mut Criterion) {
|
||||
c.bench_function("draft_latest_ref", |b| {
|
||||
b.iter(|| {
|
||||
let draft = make_draft();
|
||||
let latest = draft.latest_ref();
|
||||
black_box(latest.enable_auto_launch);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:apply(提交草稿)
|
||||
fn bench_apply(c: &mut Criterion) {
|
||||
c.bench_function("draft_apply", |b| {
|
||||
b.iter(|| {
|
||||
let draft = make_draft();
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
}
|
||||
let _ = draft.apply();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:discard(丢弃草稿)
|
||||
fn bench_discard(c: &mut Criterion) {
|
||||
c.bench_function("draft_discard", |b| {
|
||||
b.iter(|| {
|
||||
let draft = make_draft();
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
}
|
||||
let _ = draft.discard();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:异步 with_data_modify
|
||||
fn bench_with_data_modify(c: &mut Criterion) {
|
||||
let rt = Runtime::new().unwrap_or_else(|error| {
|
||||
eprintln!("draft benchmarks require a Tokio runtime: {error}");
|
||||
pub fn bench_draft(c: &mut Criterion) {
|
||||
let rt = Runtime::new().unwrap_or_else(|e| {
|
||||
eprintln!("Tokio runtime init failed: {e}");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
c.bench_function("draft_with_data_modify", |b| {
|
||||
let mut group = c.benchmark_group("draft");
|
||||
group.sample_size(100);
|
||||
group.warm_up_time(std::time::Duration::from_millis(300));
|
||||
group.measurement_time(std::time::Duration::from_secs(1));
|
||||
|
||||
group.bench_function("data_mut", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
let mut data = draft.data_mut();
|
||||
data.enable_tun_mode = Some(true);
|
||||
black_box(&data.enable_tun_mode);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("draft_mut_first", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
black_box(&d.enable_auto_launch);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("draft_mut_existing", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
{
|
||||
let mut first = draft.draft_mut();
|
||||
first.enable_tun_mode = Some(true);
|
||||
black_box(&first.enable_tun_mode);
|
||||
}
|
||||
let mut second = draft.draft_mut();
|
||||
second.enable_tun_mode = Some(false);
|
||||
black_box(&second.enable_tun_mode);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("latest_ref", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
let latest = draft.latest_ref();
|
||||
black_box(&latest.enable_auto_launch);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("apply", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
}
|
||||
draft.apply();
|
||||
black_box(&draft);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("discard", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
}
|
||||
draft.discard();
|
||||
black_box(&draft);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("with_data_modify_async", |b| {
|
||||
b.to_async(&rt).iter(|| async {
|
||||
let draft = make_draft();
|
||||
let _res: Result<(), anyhow::Error> = draft
|
||||
.with_data_modify(|mut box_data| async move {
|
||||
let draft = black_box(make_draft());
|
||||
let _: Result<(), anyhow::Error> = draft
|
||||
.with_data_modify::<_, _, _, anyhow::Error>(|mut box_data| async move {
|
||||
box_data.enable_auto_launch =
|
||||
Some(!box_data.enable_auto_launch.unwrap_or(false));
|
||||
Ok((box_data, ()))
|
||||
@@ -126,17 +103,9 @@ fn bench_with_data_modify(c: &mut Criterion) {
|
||||
.await;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_data_ref,
|
||||
bench_data_mut,
|
||||
bench_draft_mut_first,
|
||||
bench_draft_mut_existing,
|
||||
bench_latest_ref,
|
||||
bench_apply,
|
||||
bench_discard,
|
||||
bench_with_data_modify
|
||||
);
|
||||
criterion_group!(benches, bench_draft);
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -12,6 +12,7 @@ use smartstring::alias::String;
|
||||
use std::path::Path;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
/// 打开应用程序所在目录
|
||||
#[tauri::command]
|
||||
@@ -41,6 +42,20 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
open::that(url.as_str()).stringify_err()
|
||||
}
|
||||
|
||||
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
|
||||
/// 打开 Verge 最新日志
|
||||
#[tauri::command]
|
||||
pub async fn open_app_log() -> CmdResult<()> {
|
||||
open::that(dirs::app_latest_log().stringify_err()?).stringify_err()
|
||||
}
|
||||
|
||||
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
|
||||
/// 打开 Clash 最新日志
|
||||
#[tauri::command]
|
||||
pub async fn open_core_log() -> CmdResult<()> {
|
||||
open::that(dirs::clash_latest_log().stringify_err()?).stringify_err()
|
||||
}
|
||||
|
||||
/// 打开/关闭开发者工具
|
||||
#[tauri::command]
|
||||
pub fn open_devtools(app_handle: AppHandle) {
|
||||
@@ -102,7 +117,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
}
|
||||
|
||||
if !icon_cache_dir.exists() {
|
||||
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||
let _ = fs::create_dir_all(&icon_cache_dir).await;
|
||||
}
|
||||
|
||||
let temp_path = icon_cache_dir.join(format!("{}.downloading", name.as_str()));
|
||||
@@ -126,7 +141,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
|
||||
if is_image && !is_html {
|
||||
{
|
||||
let mut file = match std::fs::File::create(&temp_path) {
|
||||
let mut file = match fs::File::create(&temp_path).await {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
if icon_path.exists() {
|
||||
@@ -135,12 +150,12 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
return Err("Failed to create temporary file".into());
|
||||
}
|
||||
};
|
||||
|
||||
std::io::copy(&mut content.as_ref(), &mut file).stringify_err()?;
|
||||
file.write_all(content.as_ref()).await.stringify_err()?;
|
||||
file.flush().await.stringify_err()?;
|
||||
}
|
||||
|
||||
if !icon_path.exists() {
|
||||
match std::fs::rename(&temp_path, &icon_path) {
|
||||
match fs::rename(&temp_path, &icon_path).await {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
let _ = temp_path.remove_if_exists().await;
|
||||
@@ -226,7 +241,7 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<Stri
|
||||
/// 通知UI已准备就绪
|
||||
#[tauri::command]
|
||||
pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
log::info!(target: "app", "前端UI已准备就绪");
|
||||
logging!(info, Type::Cmd, "前端UI已准备就绪");
|
||||
crate::utils::resolve::ui::mark_ui_ready();
|
||||
Ok(())
|
||||
}
|
||||
@@ -234,7 +249,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
/// UI加载阶段
|
||||
#[tauri::command]
|
||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
log::info!(target: "app", "UI加载阶段更新: {}", stage.as_str());
|
||||
logging!(info, Type::Cmd, "UI加载阶段更新: {}", stage.as_str());
|
||||
|
||||
use crate::utils::resolve::ui::UiReadyStage;
|
||||
|
||||
@@ -245,7 +260,12 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
||||
"Ready" => UiReadyStage::Ready,
|
||||
_ => {
|
||||
log::warn!(target: "app", "未知的UI加载阶段: {}", stage.as_str());
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
"Warning: 未知的UI加载阶段: {}",
|
||||
stage.as_str()
|
||||
);
|
||||
return Err(format!("未知的UI加载阶段: {}", stage.as_str()).into());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,8 +11,8 @@ pub async fn create_local_backup() -> CmdResult<()> {
|
||||
|
||||
/// List local backups
|
||||
#[tauri::command]
|
||||
pub fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
||||
feat::list_local_backup().stringify_err()
|
||||
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
||||
feat::list_local_backup().await.stringify_err()
|
||||
}
|
||||
|
||||
/// Delete local backup
|
||||
@@ -29,6 +29,8 @@ pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
|
||||
|
||||
/// Export local backup to a user selected destination
|
||||
#[tauri::command]
|
||||
pub fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
|
||||
feat::export_local_backup(filename, destination).stringify_err()
|
||||
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
|
||||
feat::export_local_backup(filename, destination)
|
||||
.await
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::utils::dirs;
|
||||
use crate::{
|
||||
cmd::StringifyErr,
|
||||
config::Config,
|
||||
constants,
|
||||
core::{CoreManager, handle, validate::CoreConfigValidator},
|
||||
};
|
||||
use crate::{config::*, feat, logging, utils::logging::Type};
|
||||
use compact_str::CompactString;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
|
||||
/// 复制Clash环境变量
|
||||
#[tauri::command]
|
||||
@@ -42,10 +43,7 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||
logging!(info, Type::Config, "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global()
|
||||
.change_core(Some(clash_core.clone()))
|
||||
.await
|
||||
{
|
||||
match CoreManager::global().change_core(&clash_core).await {
|
||||
Ok(_) => {
|
||||
// 切换内核后重启内核
|
||||
match CoreManager::global().restart_core().await {
|
||||
@@ -113,7 +111,7 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
let result = match feat::test_delay(url).await {
|
||||
Ok(delay) => delay,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
10000u32
|
||||
}
|
||||
};
|
||||
@@ -130,7 +128,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
// 获取DNS配置文件路径
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join("dns_config.yaml");
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
|
||||
// 保存DNS配置到文件
|
||||
let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?;
|
||||
@@ -153,18 +151,16 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
// 读取DNS配置文件
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join("dns_config.yaml");
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
|
||||
if !dns_path.exists() {
|
||||
logging!(warn, Type::Config, "DNS config file not found");
|
||||
return Err("DNS config file not found".into());
|
||||
}
|
||||
|
||||
let dns_yaml = tokio::fs::read_to_string(&dns_path)
|
||||
.await
|
||||
.stringify_err_log(|e| {
|
||||
logging!(error, Type::Config, "Failed to read DNS config: {e}");
|
||||
})?;
|
||||
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
|
||||
logging!(error, Type::Config, "Failed to read DNS config: {e}");
|
||||
})?;
|
||||
|
||||
// 解析DNS配置
|
||||
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
|
||||
@@ -233,7 +229,7 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join("dns_config.yaml");
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
|
||||
Ok(dns_path.exists())
|
||||
}
|
||||
@@ -246,7 +242,7 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join("dns_config.yaml");
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
|
||||
if !fs::try_exists(&dns_path).await.stringify_err()? {
|
||||
return Err("DNS config file not found".into());
|
||||
@@ -259,10 +255,8 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||
/// 验证DNS配置文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
||||
use crate::utils::dirs;
|
||||
|
||||
let app_dir = dirs::app_home_dir().stringify_err()?;
|
||||
let dns_path = app_dir.join("dns_config.yaml");
|
||||
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
||||
let dns_path_str = dns_path.to_str().unwrap_or_default();
|
||||
|
||||
if !dns_path.exists() {
|
||||
@@ -275,7 +269,7 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_logs() -> CmdResult<VecDeque<CompactString>> {
|
||||
pub async fn get_clash_logs() -> CmdResult<Vec<CompactString>> {
|
||||
let logs = CoreManager::global()
|
||||
.get_clash_logs()
|
||||
.await
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{logging, utils::logging::Type};
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
let device_api_url = "https://disney.api.edge.bamgrid.com/devices";
|
||||
let auth_header =
|
||||
|
||||
@@ -2,13 +2,14 @@ use super::CmdResult;
|
||||
use crate::cmd::StringifyErr;
|
||||
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml_ng::Mapping;
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
log::debug!(target: "app", "异步获取系统代理配置");
|
||||
logging!(debug, Type::Network, "异步获取系统代理配置");
|
||||
|
||||
let current = AsyncProxyQuery::get_system_proxy().await;
|
||||
|
||||
@@ -20,14 +21,21 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"返回系统代理配置: enable={}, {}:{}",
|
||||
current.enable,
|
||||
current.host,
|
||||
current.port
|
||||
);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// 获取自动代理配置
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
|
||||
logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)");
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
@@ -41,7 +49,13 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.clone().into());
|
||||
|
||||
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"返回自动代理配置(缓存): enable={}, url={}",
|
||||
current.enable,
|
||||
current.url
|
||||
);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ pub async fn enhance_profiles() -> CmdResult {
|
||||
match feat::enhance_profiles().await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
return Err(e.to_string().into());
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url);
|
||||
|
||||
// 直接依赖 PrfItem::from_url 自身的超时/重试逻辑,不再使用 tokio::time::timeout 包裹
|
||||
let item = match PrfItem::from_url(&url, None, None, option).await {
|
||||
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
|
||||
Ok(it) => {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 下载完成,开始保存配置");
|
||||
it
|
||||
@@ -110,7 +110,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
}
|
||||
};
|
||||
|
||||
match profiles_append_item_safe(item.clone()).await {
|
||||
match profiles_append_item_safe(item).await {
|
||||
Ok(_) => match profiles_save_file_safe().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 配置文件保存成功");
|
||||
@@ -145,13 +145,13 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
/// 调整profile的顺序
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
match profiles_reorder_safe(active_id, over_id).await {
|
||||
match profiles_reorder_safe(&active_id, &over_id).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "重新排序配置文件");
|
||||
logging!(info, Type::Cmd, "重新排序配置文件");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "重新排序配置文件失败: {}", err);
|
||||
logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err);
|
||||
Err(format!("重新排序配置文件失败: {}", err).into())
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,7 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
/// 创建一个新的配置文件
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
match profiles_append_item_with_filedata_safe(item.clone(), file_data).await {
|
||||
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
||||
Ok(_) => {
|
||||
// 发送配置变更通知
|
||||
if let Some(uid) = &item.uid {
|
||||
@@ -180,10 +180,10 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
||||
/// 更新配置文件
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
match feat::update_profile(index, option, Some(true)).await {
|
||||
match feat::update_profile(&index, option.as_ref(), true, true).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
@@ -194,9 +194,7 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
println!("delete_profile: {}", index);
|
||||
// 使用Send-safe helper函数
|
||||
let should_update = profiles_delete_item_safe(index.clone())
|
||||
.await
|
||||
.stringify_err()?;
|
||||
let should_update = profiles_delete_item_safe(&index).await.stringify_err()?;
|
||||
profiles_save_file_safe().await.stringify_err()?;
|
||||
|
||||
if should_update {
|
||||
@@ -208,7 +206,7 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
||||
handle::Handle::notify_profile_changed(index);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
return Err(e.to_string().into());
|
||||
}
|
||||
}
|
||||
@@ -216,6 +214,257 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 验证新配置文件的语法
|
||||
async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile);
|
||||
|
||||
// 获取目标配置文件路径
|
||||
let config_file_result = {
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_data = profiles_config.latest_ref();
|
||||
match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
if let Some(file) = &item.file {
|
||||
let path = dirs::app_profiles_dir().map(|dir| dir.join(file.as_str()));
|
||||
path.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 如果获取到文件路径,检查YAML语法
|
||||
if let Some(file_path) = config_file_result {
|
||||
if !file_path.exists() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件不存在: {}",
|
||||
file_path.display()
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_not_found",
|
||||
format!("{}", file_path.display()),
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
|
||||
// 超时保护
|
||||
let file_read_result = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
tokio::fs::read_to_string(&file_path),
|
||||
)
|
||||
.await;
|
||||
|
||||
match file_read_result {
|
||||
Ok(Ok(content)) => {
|
||||
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
|
||||
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
|
||||
})
|
||||
.await;
|
||||
|
||||
match yaml_parse_result {
|
||||
Ok(Ok(_)) => {
|
||||
logging!(info, Type::Cmd, "目标配置文件语法正确");
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!(" {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件存在YAML语法错误:{}",
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
error_msg.clone(),
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
Err(join_err) => {
|
||||
let error_msg = format!("YAML解析任务失败: {join_err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_parse_error",
|
||||
error_msg.clone(),
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_error",
|
||||
error_msg.clone(),
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
Err(_) => {
|
||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_timeout",
|
||||
error_msg.clone(),
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行配置更新并处理结果
|
||||
async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
|
||||
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
Config::profiles()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
.stringify_err()?;
|
||||
Config::profiles().await.apply();
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步保存恢复配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
logging!(info, Type::Cmd, "成功恢复到之前的配置");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_success(current_sequence: u64, current_value: Option<String>) -> CmdResult<bool> {
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"配置更新成功,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_menu().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
|
||||
}
|
||||
|
||||
if let Some(current) = ¤t_value {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"向前端发送配置变更事件: {}, 序列号: {}",
|
||||
current,
|
||||
current_sequence
|
||||
);
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
}
|
||||
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn handle_validation_failure(
|
||||
error_msg: String,
|
||||
current_profile: Option<String>,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().await.discard();
|
||||
if let Some(prev_profile) = current_profile {
|
||||
restore_previous_profile(prev_profile).await?;
|
||||
}
|
||||
handle::Handle::notice_message("config_validate::error", error_msg);
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn handle_update_error<E: std::fmt::Display>(e: E, current_sequence: u64) -> CmdResult<bool> {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
"更新过程发生错误: {}, 序列号: {}",
|
||||
e,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn handle_timeout(current_profile: Option<String>, current_sequence: u64) -> CmdResult<bool> {
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"{}, 序列号: {}",
|
||||
timeout_msg,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
if let Some(prev_profile) = current_profile {
|
||||
restore_previous_profile(prev_profile).await?;
|
||||
}
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn perform_config_update(
|
||||
current_sequence: u64,
|
||||
current_value: Option<String>,
|
||||
current_profile: Option<String>,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"开始内核配置更新,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
CoreManager::global().update_config(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match update_result {
|
||||
Ok(Ok((true, _))) => handle_success(current_sequence, current_value).await,
|
||||
Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await,
|
||||
Ok(Err(e)) => handle_update_error(e, current_sequence).await,
|
||||
Err(_) => handle_timeout(current_profile, current_sequence).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
@@ -256,108 +505,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||
if let Some(new_profile) = profiles.current.as_ref()
|
||||
&& current_profile.as_ref() != Some(new_profile)
|
||||
&& validate_new_profile(new_profile).await.is_err()
|
||||
{
|
||||
logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile);
|
||||
|
||||
// 获取目标配置文件路径
|
||||
let config_file_result = {
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_data = profiles_config.latest_ref();
|
||||
match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
if let Some(file) = &item.file {
|
||||
let path = dirs::app_profiles_dir().map(|dir| dir.join(file.as_str()));
|
||||
path.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 如果获取到文件路径,检查YAML语法
|
||||
if let Some(file_path) = config_file_result {
|
||||
if !file_path.exists() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件不存在: {}",
|
||||
file_path.display()
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_not_found",
|
||||
format!("{}", file_path.display()),
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 超时保护
|
||||
let file_read_result = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
tokio::fs::read_to_string(&file_path),
|
||||
)
|
||||
.await;
|
||||
|
||||
match file_read_result {
|
||||
Ok(Ok(content)) => {
|
||||
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
|
||||
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
|
||||
})
|
||||
.await;
|
||||
|
||||
match yaml_parse_result {
|
||||
Ok(Ok(_)) => {
|
||||
logging!(info, Type::Cmd, "目标配置文件语法正确");
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!(" {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件存在YAML语法错误:{}",
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
error_msg.clone(),
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
Err(join_err) => {
|
||||
let error_msg = format!("YAML解析任务失败: {join_err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_parse_error",
|
||||
error_msg.clone(),
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_error",
|
||||
error_msg.clone(),
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
Err(_) => {
|
||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_timeout",
|
||||
error_msg.clone(),
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 检查请求有效性
|
||||
@@ -399,163 +550,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 为配置更新添加超时保护
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"开始内核配置更新,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30), // 30秒超时
|
||||
CoreManager::global().update_config(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// 更新配置并进行验证
|
||||
match update_result {
|
||||
Ok(Ok((true, _))) => {
|
||||
// 内核操作完成后再次检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"配置更新成功,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||
// crate::process::AsyncHandler::spawn(|| async move {
|
||||
// if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||
// log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
|
||||
// }
|
||||
// });
|
||||
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_menu().await {
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
|
||||
// 保存配置文件
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
||||
}
|
||||
|
||||
// 立即通知前端配置变更
|
||||
if let Some(current) = ¤t_value {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"向前端发送配置变更事件: {}, 序列号: {}",
|
||||
current,
|
||||
current_sequence
|
||||
);
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
}
|
||||
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(true)
|
||||
}
|
||||
Ok(Ok((false, error_msg))) => {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().await.discard();
|
||||
// 如果验证失败,恢复到之前的配置
|
||||
if let Some(prev_profile) = current_profile {
|
||||
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
// 静默恢复,不触发验证
|
||||
Config::profiles()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
.stringify_err()?;
|
||||
Config::profiles().await.apply();
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
logging!(info, Type::Cmd, "成功恢复到之前的配置");
|
||||
}
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", error_msg.to_string());
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
"更新过程发生错误: {}, 序列号: {}",
|
||||
e,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
Err(_) => {
|
||||
// 超时处理
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"{}, 序列号: {}",
|
||||
timeout_msg,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
|
||||
if let Some(prev_profile) = current_profile {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
|
||||
prev_profile,
|
||||
current_sequence
|
||||
);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
Config::profiles()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
.stringify_err()?;
|
||||
Config::profiles().await.apply();
|
||||
}
|
||||
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
perform_config_update(current_sequence, current_value, current_profile).await
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
@@ -575,33 +570,34 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
|
||||
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
// 保存修改前检查是否有更新 update_interval
|
||||
let profiles = Config::profiles().await;
|
||||
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) {
|
||||
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index)
|
||||
&& let Some(new_option) = profile.option.as_ref()
|
||||
{
|
||||
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
let new_interval = new_option.update_interval;
|
||||
let old_allow_auto_update = old_profile
|
||||
.option
|
||||
.as_ref()
|
||||
.and_then(|o| o.allow_auto_update);
|
||||
let new_allow_auto_update = profile.option.as_ref().and_then(|o| o.allow_auto_update);
|
||||
let new_allow_auto_update = new_option.allow_auto_update;
|
||||
(old_interval != new_interval) || (old_allow_auto_update != new_allow_auto_update)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
profiles_patch_item_safe(index.clone(), profile)
|
||||
profiles_patch_item_safe(&index, &profile)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
|
||||
// 如果更新间隔或允许自动更新变更,异步刷新定时器
|
||||
if should_refresh_timer {
|
||||
let index_clone = index.clone();
|
||||
crate::process::AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
|
||||
if let Err(e) = crate::core::Timer::global().refresh().await {
|
||||
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
|
||||
} else {
|
||||
// 刷新成功后发送自定义事件,不触发配置重载
|
||||
crate::core::handle::Handle::notify_timer_updated(index_clone);
|
||||
crate::core::handle::Handle::notify_timer_updated(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -634,10 +630,15 @@ pub async fn view_profile(index: String) -> CmdResult {
|
||||
/// 读取配置文件内容
|
||||
#[tauri::command]
|
||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_ref();
|
||||
let item = profiles_ref.get_item(&index).stringify_err()?;
|
||||
let data = item.read_file().stringify_err()?;
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_ref();
|
||||
PrfItem {
|
||||
file: profiles_ref.get_item(&index).stringify_err()?.file.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let data = item.read_file().await.stringify_err()?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,28 +12,37 @@ use tokio::fs;
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let file_data = match file_data {
|
||||
Some(d) => d,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// 在异步操作前完成所有文件操作
|
||||
let (file_path, original_content, is_merge_file) = {
|
||||
// 在异步操作前获取必要元数据并释放锁
|
||||
let (rel_path, is_merge_file) = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_guard = profiles.latest_ref();
|
||||
let item = profiles_guard.get_item(&index).stringify_err()?;
|
||||
// 确定是否为merge类型文件
|
||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||
let content = item.read_file().stringify_err()?;
|
||||
let path = item.file.clone().ok_or("file field is null")?;
|
||||
let profiles_dir = dirs::app_profiles_dir().stringify_err()?;
|
||||
(profiles_dir.join(path.as_str()), content, is_merge)
|
||||
(path, is_merge)
|
||||
};
|
||||
|
||||
// 读取原始内容(在释放profiles_guard后进行)
|
||||
let original_content = PrfItem {
|
||||
file: Some(rel_path.clone()),
|
||||
..Default::default()
|
||||
}
|
||||
.read_file()
|
||||
.await
|
||||
.stringify_err()?;
|
||||
|
||||
let profiles_dir = dirs::app_profiles_dir().stringify_err()?;
|
||||
let file_path = profiles_dir.join(rel_path.as_str());
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
|
||||
// 保存新的配置文件
|
||||
let file_data = file_data.ok_or("file_data is None")?;
|
||||
fs::write(&file_path, &file_data).await.stringify_err()?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
@@ -42,102 +51,107 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
is_merge_file
|
||||
);
|
||||
|
||||
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||
if is_merge_file {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 检测到merge文件,只进行语法验证"
|
||||
);
|
||||
match CoreConfigValidator::validate_config_file(&file_path_str, Some(true)).await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
|
||||
// 成功后尝试更新整体配置
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
// 配置更新成功,刷新前端
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"[cmd配置save] 更新整体配置时发生错误: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
return handle_merge_file(&file_path_str, &file_path, &original_content).await;
|
||||
}
|
||||
|
||||
handle_full_validation(&file_path_str, &file_path, &original_content).await
|
||||
}
|
||||
|
||||
async fn restore_original(
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
) -> Result<(), String> {
|
||||
fs::write(file_path, original_content).await.stringify_err()
|
||||
}
|
||||
|
||||
fn is_script_error(err: &str, file_path_str: &str) -> bool {
|
||||
file_path_str.ends_with(".js")
|
||||
|| err.contains("Script syntax error")
|
||||
|| err.contains("Script must contain a main function")
|
||||
|| err.contains("Failed to read script file")
|
||||
}
|
||||
|
||||
async fn handle_merge_file(
|
||||
file_path_str: &str,
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
) -> CmdResult {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 检测到merge文件,只进行语法验证"
|
||||
);
|
||||
|
||||
match CoreConfigValidator::validate_config_file(file_path_str, Some(true)).await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"[cmd配置save] merge文件语法验证失败: {}",
|
||||
error_msg
|
||||
"[cmd配置save] 更新整体配置时发生错误: {}",
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
fs::write(&file_path, original_content)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
// 发送合并文件专用错误通知
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
fs::write(&file_path, original_content)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
return Err(e.to_string().into());
|
||||
} else {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"[cmd配置save] merge文件语法验证失败: {}",
|
||||
error_msg
|
||||
);
|
||||
restore_original(file_path, original_content).await?;
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||
restore_original(file_path, original_content).await?;
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非merge文件使用完整验证流程
|
||||
match CoreConfigValidator::validate_config_file(&file_path_str, None).await {
|
||||
async fn handle_full_validation(
|
||||
file_path_str: &str,
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
) -> CmdResult {
|
||||
match CoreConfigValidator::validate_config_file(file_path_str, None).await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, "[cmd配置save] 验证成功");
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg);
|
||||
// 恢复原始配置文件
|
||||
fs::write(&file_path, original_content)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
|
||||
// 智能判断错误类型
|
||||
let is_script_error = file_path_str.ends_with(".js")
|
||||
|| error_msg.contains("Script syntax error")
|
||||
|| error_msg.contains("Script must contain a main function")
|
||||
|| error_msg.contains("Failed to read script file");
|
||||
restore_original(file_path, original_content).await?;
|
||||
|
||||
if error_msg.contains("YAML syntax error")
|
||||
|| error_msg.contains("Failed to read file:")
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error(&error_msg, file_path_str))
|
||||
{
|
||||
// 普通YAML错误使用YAML通知处理
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] YAML配置文件验证失败,发送通知"
|
||||
);
|
||||
let result = (false, error_msg.clone());
|
||||
let result = (false, error_msg.to_owned());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||
} else if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
} else if is_script_error(&error_msg, file_path_str) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 脚本文件验证失败,发送通知"
|
||||
);
|
||||
let result = (false, error_msg.clone());
|
||||
let result = (false, error_msg.to_owned());
|
||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
// 普通配置错误使用一般通知
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
@@ -150,10 +164,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
fs::write(&file_path, original_content)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
restore_original(file_path, original_content).await?;
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
core::service::{self, SERVICE_MANAGER, ServiceStatus},
|
||||
utils::i18n::t,
|
||||
};
|
||||
use smartstring::SmartString;
|
||||
|
||||
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
|
||||
if let Err(e) = SERVICE_MANAGER
|
||||
@@ -12,7 +13,7 @@ async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) ->
|
||||
.await
|
||||
{
|
||||
let emsg = format!("{} Service failed: {}", op_type, e);
|
||||
return Err(t(emsg.as_str()).await.into());
|
||||
return Err(SmartString::from(&*t(emsg.as_str()).await));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let ref_data = verge.latest_ref();
|
||||
ref_data.clone()
|
||||
};
|
||||
let verge_response = IVergeResponse::from(*verge_data);
|
||||
let verge_response = IVergeResponse::from(verge_data);
|
||||
Ok(verge_response)
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
feat::patch_verge(payload, false).await.stringify_err()
|
||||
feat::patch_verge(&payload, false).await.stringify_err()
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(patch.clone());
|
||||
Config::verge().await.draft_mut().patch_config(&patch);
|
||||
Config::verge().await.apply();
|
||||
|
||||
// 分离数据获取和异步调用
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::config::Config;
|
||||
use crate::constants::{network, tun as tun_const};
|
||||
use crate::utils::dirs::{ipc_path, path_to_str};
|
||||
use crate::utils::{dirs, help};
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
@@ -40,15 +42,13 @@ impl IClashTemp {
|
||||
Self(Self::guard(map))
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
template
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template() -> Self {
|
||||
use crate::constants::{network, tun as tun_const};
|
||||
|
||||
let mut map = Mapping::new();
|
||||
let mut tun_config = Mapping::new();
|
||||
let mut cors_map = Mapping::new();
|
||||
@@ -214,9 +214,9 @@ impl IClashTemp {
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7896);
|
||||
.unwrap_or(network::ports::DEFAULT_TPROXY);
|
||||
if port == 0 {
|
||||
port = 7896;
|
||||
port = network::ports::DEFAULT_TPROXY;
|
||||
}
|
||||
port
|
||||
}
|
||||
@@ -330,7 +330,7 @@ impl IClashTemp {
|
||||
.ok()
|
||||
.and_then(|path| path_to_str(&path).ok().map(|s| s.into()))
|
||||
.unwrap_or_else(|| {
|
||||
log::error!(target: "app", "Failed to get IPC path");
|
||||
logging!(error, Type::Config, "Failed to get IPC path");
|
||||
crate::constants::network::DEFAULT_EXTERNAL_CONTROLLER.into()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use crate::{
|
||||
cmd,
|
||||
config::{PrfItem, profiles_append_item_safe},
|
||||
constants::{files, timing},
|
||||
core::{CoreManager, handle, validate::CoreConfigValidator},
|
||||
enhance, logging,
|
||||
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
|
||||
enhance, logging, logging_error,
|
||||
utils::{Draft, dirs, help, logging::Type},
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -53,24 +54,47 @@ impl Config {
|
||||
|
||||
/// 初始化订阅
|
||||
pub async fn init_config() -> Result<()> {
|
||||
if Self::profiles()
|
||||
.await
|
||||
.latest_ref()
|
||||
.get_item(&"Merge".into())
|
||||
.is_err()
|
||||
Self::ensure_default_profile_items().await?;
|
||||
|
||||
// init Tun mode
|
||||
if !cmd::system::is_admin().unwrap_or_default()
|
||||
&& service::is_service_available().await.is_err()
|
||||
{
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".into()))?;
|
||||
profiles_append_item_safe(merge_item.clone()).await?;
|
||||
let verge = Config::verge().await;
|
||||
verge.draft_mut().enable_tun_mode = Some(false);
|
||||
verge.apply();
|
||||
let _ = tray::Tray::global().update_tray_display().await;
|
||||
|
||||
// 分离数据获取和异步调用避免Send问题
|
||||
let verge_data = Config::verge().await.latest_ref().clone();
|
||||
logging_error!(Type::Core, verge_data.save_file().await);
|
||||
}
|
||||
if Self::profiles()
|
||||
.await
|
||||
.latest_ref()
|
||||
.get_item(&"Script".into())
|
||||
.is_err()
|
||||
{
|
||||
let script_item = PrfItem::from_script(Some("Script".into()))?;
|
||||
profiles_append_item_safe(script_item.clone()).await?;
|
||||
|
||||
let validation_result = Self::generate_and_validate().await?;
|
||||
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
sleep(timing::STARTUP_ERROR_DELAY).await;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Ensure "Merge" and "Script" profile items exist, adding them if missing.
|
||||
async fn ensure_default_profile_items() -> Result<()> {
|
||||
let profiles = Self::profiles().await;
|
||||
if profiles.latest_ref().get_item("Merge").is_err() {
|
||||
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
|
||||
profiles_append_item_safe(merge_item).await?;
|
||||
}
|
||||
if profiles.latest_ref().get_item("Script").is_err() {
|
||||
let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
|
||||
profiles_append_item_safe(script_item).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_and_validate() -> Result<Option<(&'static str, String)>> {
|
||||
// 生成运行时配置
|
||||
if let Err(err) = Self::generate().await {
|
||||
logging!(error, Type::Config, "生成运行时配置失败: {}", err);
|
||||
@@ -81,7 +105,7 @@ impl Config {
|
||||
// 生成运行时配置文件并验证
|
||||
let config_result = Self::generate_file(ConfigType::Run).await;
|
||||
|
||||
let validation_result = if config_result.is_ok() {
|
||||
if config_result.is_ok() {
|
||||
// 验证配置文件
|
||||
logging!(info, Type::Config, "开始验证配置");
|
||||
|
||||
@@ -97,12 +121,12 @@ impl Config {
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::boot_error", &error_msg)
|
||||
.await?;
|
||||
Some(("config_validate::boot_error", error_msg))
|
||||
Ok(Some(("config_validate::boot_error", error_msg)))
|
||||
} else {
|
||||
logging!(info, Type::Config, "配置验证成功");
|
||||
// 前端没有必要知道验证成功的消息,也没有事件驱动
|
||||
// Some(("config_validate::success", String::new()))
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -110,7 +134,7 @@ impl Config {
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::process_terminated", "")
|
||||
.await?;
|
||||
Some(("config_validate::process_terminated", String::new()))
|
||||
Ok(Some(("config_validate::process_terminated", String::new())))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -118,15 +142,8 @@ impl Config {
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::error", "")
|
||||
.await?;
|
||||
Some(("config_validate::error", String::new()))
|
||||
};
|
||||
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
sleep(timing::STARTUP_ERROR_DELAY).await;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
Ok(Some(("config_validate::error", String::new())))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||
@@ -151,11 +168,11 @@ impl Config {
|
||||
pub async fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||
|
||||
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
|
||||
**Config::runtime().await.draft_mut() = IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
});
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use crate::utils::{
|
||||
dirs, help,
|
||||
network::{NetworkManager, ProxyType},
|
||||
tmpl,
|
||||
use crate::{
|
||||
config::profiles,
|
||||
utils::{
|
||||
dirs, help,
|
||||
network::{NetworkManager, ProxyType},
|
||||
tmpl,
|
||||
},
|
||||
};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::{fs, time::Duration};
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct PrfItem {
|
||||
@@ -118,26 +122,29 @@ pub struct PrfOption {
|
||||
}
|
||||
|
||||
impl PrfOption {
|
||||
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
|
||||
pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option<Self> {
|
||||
match (one, other) {
|
||||
(Some(mut a), Some(b)) => {
|
||||
a.user_agent = b.user_agent.or(a.user_agent);
|
||||
a.with_proxy = b.with_proxy.or(a.with_proxy);
|
||||
a.self_proxy = b.self_proxy.or(a.self_proxy);
|
||||
a.danger_accept_invalid_certs = b
|
||||
(Some(a_ref), Some(b_ref)) => {
|
||||
let mut result = a_ref.clone();
|
||||
result.user_agent = b_ref.user_agent.clone().or(result.user_agent);
|
||||
result.with_proxy = b_ref.with_proxy.or(result.with_proxy);
|
||||
result.self_proxy = b_ref.self_proxy.or(result.self_proxy);
|
||||
result.danger_accept_invalid_certs = b_ref
|
||||
.danger_accept_invalid_certs
|
||||
.or(a.danger_accept_invalid_certs);
|
||||
a.allow_auto_update = b.allow_auto_update.or(a.allow_auto_update);
|
||||
a.update_interval = b.update_interval.or(a.update_interval);
|
||||
a.merge = b.merge.or(a.merge);
|
||||
a.script = b.script.or(a.script);
|
||||
a.rules = b.rules.or(a.rules);
|
||||
a.proxies = b.proxies.or(a.proxies);
|
||||
a.groups = b.groups.or(a.groups);
|
||||
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
|
||||
Some(a)
|
||||
.or(result.danger_accept_invalid_certs);
|
||||
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update);
|
||||
result.update_interval = b_ref.update_interval.or(result.update_interval);
|
||||
result.merge = b_ref.merge.clone().or(result.merge);
|
||||
result.script = b_ref.script.clone().or(result.script);
|
||||
result.rules = b_ref.rules.clone().or(result.rules);
|
||||
result.proxies = b_ref.proxies.clone().or(result.proxies);
|
||||
result.groups = b_ref.groups.clone().or(result.groups);
|
||||
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
|
||||
Some(result)
|
||||
}
|
||||
t => t.0.or(t.1),
|
||||
(Some(a_ref), None) => Some(a_ref.clone()),
|
||||
(None, Some(b_ref)) => Some(b_ref.clone()),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,13 +152,14 @@ impl PrfOption {
|
||||
impl PrfItem {
|
||||
/// From partial item
|
||||
/// must contain `itype`
|
||||
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||
pub async fn from(item: &PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||
if item.itype.is_none() {
|
||||
bail!("type should not be null");
|
||||
}
|
||||
|
||||
let itype = item
|
||||
.itype
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("type should not be null"))?;
|
||||
match itype.as_str() {
|
||||
"remote" => {
|
||||
@@ -159,14 +167,16 @@ impl PrfItem {
|
||||
.url
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("url should not be null"))?;
|
||||
let name = item.name;
|
||||
let desc = item.desc;
|
||||
PrfItem::from_url(url, name, desc, item.option).await
|
||||
let name = item.name.as_ref();
|
||||
let desc = item.desc.as_ref();
|
||||
let option = item.option.as_ref();
|
||||
PrfItem::from_url(url, name, desc, option).await
|
||||
}
|
||||
"local" => {
|
||||
let name = item.name.unwrap_or_else(|| "Local File".into());
|
||||
let desc = item.desc.unwrap_or_else(|| "".into());
|
||||
PrfItem::from_local(name, desc, file_data, item.option).await
|
||||
let name = item.name.clone().unwrap_or_else(|| "Local File".into());
|
||||
let desc = item.desc.clone().unwrap_or_else(|| "".into());
|
||||
let option = item.option.as_ref();
|
||||
PrfItem::from_local(name, desc, file_data, option).await
|
||||
}
|
||||
typ => bail!("invalid profile item type \"{typ}\""),
|
||||
}
|
||||
@@ -178,7 +188,7 @@ impl PrfItem {
|
||||
name: String,
|
||||
desc: String,
|
||||
file_data: Option<String>,
|
||||
option: Option<PrfOption>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
let uid = help::get_uid("L").into();
|
||||
let file = format!("{uid}.yaml").into();
|
||||
@@ -191,29 +201,29 @@ impl PrfItem {
|
||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
|
||||
merge = merge_item.uid;
|
||||
let merge_item = &mut PrfItem::from_merge(None)?;
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
|
||||
script = script_item.uid;
|
||||
let script_item = &mut PrfItem::from_script(None)?;
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
|
||||
rules = rules_item.uid;
|
||||
let rules_item = &mut PrfItem::from_rules()?;
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
|
||||
proxies = proxies_item.uid;
|
||||
let proxies_item = &mut PrfItem::from_proxies()?;
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
|
||||
groups = groups_item.uid;
|
||||
let groups_item = &mut PrfItem::from_groups()?;
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
@@ -243,24 +253,23 @@ impl PrfItem {
|
||||
/// create a new item from url
|
||||
pub async fn from_url(
|
||||
url: &str,
|
||||
name: Option<String>,
|
||||
desc: Option<String>,
|
||||
option: Option<PrfOption>,
|
||||
name: Option<&String>,
|
||||
desc: Option<&String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
let opt_ref = option.as_ref();
|
||||
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||
let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||
let accept_invalid_certs =
|
||||
opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||
let allow_auto_update = opt_ref.map(|o| o.allow_auto_update.unwrap_or(true));
|
||||
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||
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 mut merge = opt_ref.and_then(|o| o.merge.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 proxies = opt_ref.and_then(|o| o.proxies.clone());
|
||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||
option.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||
let allow_auto_update = option.map(|o| o.allow_auto_update.unwrap_or(true));
|
||||
let user_agent = option.and_then(|o| o.user_agent.clone());
|
||||
let update_interval = option.and_then(|o| o.update_interval);
|
||||
let timeout = option.and_then(|o| o.timeout_seconds).unwrap_or(20);
|
||||
let mut merge = option.and_then(|o| o.merge.clone());
|
||||
let mut script = option.and_then(|o| o.script.clone());
|
||||
let mut rules = option.and_then(|o| o.rules.clone());
|
||||
let mut proxies = option.and_then(|o| o.proxies.clone());
|
||||
let mut groups = option.and_then(|o| o.groups.clone());
|
||||
|
||||
// 选择代理类型
|
||||
let proxy_type = if self_proxy {
|
||||
@@ -297,18 +306,27 @@ impl PrfItem {
|
||||
let header = resp.headers();
|
||||
|
||||
// parse the Subscription UserInfo
|
||||
let extra = match header.get("Subscription-Userinfo") {
|
||||
Some(value) => {
|
||||
let sub_info = value.to_str().unwrap_or("");
|
||||
Some(PrfExtra {
|
||||
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
|
||||
download: help::parse_str(sub_info, "download").unwrap_or(0),
|
||||
total: help::parse_str(sub_info, "total").unwrap_or(0),
|
||||
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
|
||||
})
|
||||
let extra;
|
||||
'extra: {
|
||||
for (k, v) in header.iter() {
|
||||
let key_lower = k.as_str().to_ascii_lowercase();
|
||||
// Accept standard custom-metadata prefixes (x-amz-meta-, x-obs-meta-, x-cos-meta-, etc.).
|
||||
if key_lower
|
||||
.strip_suffix("subscription-userinfo")
|
||||
.is_some_and(|prefix| prefix.is_empty() || prefix.ends_with('-'))
|
||||
{
|
||||
let sub_info = v.to_str().unwrap_or("");
|
||||
extra = Some(PrfExtra {
|
||||
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
|
||||
download: help::parse_str(sub_info, "download").unwrap_or(0),
|
||||
total: help::parse_str(sub_info, "total").unwrap_or(0),
|
||||
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
|
||||
});
|
||||
break 'extra;
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
extra = None;
|
||||
}
|
||||
|
||||
// parse the Content-Disposition
|
||||
let filename = match header.get("Content-Disposition") {
|
||||
@@ -356,7 +374,11 @@ impl PrfItem {
|
||||
|
||||
let uid = help::get_uid("R").into();
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let name = name.unwrap_or_else(|| filename.unwrap_or_else(|| "Remote File".into()).into());
|
||||
let name = name.map(|s| s.to_owned()).unwrap_or_else(|| {
|
||||
filename
|
||||
.map(|s| s.into())
|
||||
.unwrap_or_else(|| "Remote File".into())
|
||||
});
|
||||
let data = resp.text_with_charset()?;
|
||||
|
||||
// process the charset "UTF-8 with BOM"
|
||||
@@ -371,36 +393,36 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
|
||||
merge = merge_item.uid;
|
||||
let merge_item = &mut PrfItem::from_merge(None)?;
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
|
||||
script = script_item.uid;
|
||||
let script_item = &mut PrfItem::from_script(None)?;
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
|
||||
rules = rules_item.uid;
|
||||
let rules_item = &mut PrfItem::from_rules()?;
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
|
||||
proxies = proxies_item.uid;
|
||||
let proxies_item = &mut PrfItem::from_proxies()?;
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
|
||||
groups = groups_item.uid;
|
||||
let groups_item = &mut PrfItem::from_groups()?;
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("remote".into()),
|
||||
name: Some(name),
|
||||
desc,
|
||||
desc: desc.cloned(),
|
||||
file: Some(file),
|
||||
url: Some(url.into()),
|
||||
selected: None,
|
||||
@@ -537,24 +559,28 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
/// get the file data
|
||||
pub fn read_file(&self) -> Result<String> {
|
||||
pub async fn read_file(&self) -> Result<String> {
|
||||
let file = self
|
||||
.file
|
||||
.clone()
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
|
||||
let path = dirs::app_profiles_dir()?.join(file.as_str());
|
||||
let content = fs::read_to_string(path).context("failed to read the file")?;
|
||||
let content = fs::read_to_string(path)
|
||||
.await
|
||||
.context("failed to read the file")?;
|
||||
Ok(content.into())
|
||||
}
|
||||
|
||||
/// save the file data
|
||||
pub fn save_file(&self, data: String) -> Result<()> {
|
||||
pub async fn save_file(&self, data: String) -> Result<()> {
|
||||
let file = self
|
||||
.file
|
||||
.clone()
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
|
||||
let path = dirs::app_profiles_dir()?.join(file.as_str());
|
||||
fs::write(path, data.as_bytes()).context("failed to save the file")
|
||||
fs::write(path, data.as_bytes())
|
||||
.await
|
||||
.context("failed to save the file")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::utils::{
|
||||
dirs::{self, PathBufExec},
|
||||
help,
|
||||
};
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
@@ -31,12 +32,24 @@ pub struct CleanupResult {
|
||||
macro_rules! patch {
|
||||
($lv: expr, $rv: expr, $key: tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key;
|
||||
$lv.$key = $rv.$key.clone();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl IProfiles {
|
||||
// Helper to find and remove an item by uid from the items vec, returning its file name (if any).
|
||||
fn take_item_file_by_uid(
|
||||
items: &mut Vec<PrfItem>,
|
||||
target_uid: Option<String>,
|
||||
) -> Option<String> {
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == target_uid {
|
||||
return items.remove(i).file;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub async fn new() -> Self {
|
||||
match dirs::profiles_path() {
|
||||
Ok(path) => match help::read_yaml::<Self>(&path).await {
|
||||
@@ -55,12 +68,12 @@ impl IProfiles {
|
||||
profiles
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::template()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
@@ -110,28 +123,30 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// find the item by the uid
|
||||
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
|
||||
if let Some(items) = self.items.as_ref() {
|
||||
let some_uid = Some(uid.clone());
|
||||
pub fn get_item(&self, uid: impl AsRef<str>) -> Result<&PrfItem> {
|
||||
let uid_str = uid.as_ref();
|
||||
|
||||
if let Some(items) = self.items.as_ref() {
|
||||
for each in items.iter() {
|
||||
if each.uid == some_uid {
|
||||
if let Some(uid_val) = &each.uid
|
||||
&& uid_val.as_str() == uid_str
|
||||
{
|
||||
return Ok(each);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("failed to get the profile item \"uid:{uid}\"");
|
||||
bail!("failed to get the profile item \"uid:{}\"", uid_str);
|
||||
}
|
||||
|
||||
/// append new item
|
||||
/// if the file_data is some
|
||||
/// then should save the data to file
|
||||
pub async fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
|
||||
if item.uid.is_none() {
|
||||
pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> {
|
||||
let uid = &item.uid;
|
||||
if uid.is_none() {
|
||||
bail!("the uid should not be null");
|
||||
}
|
||||
let uid = item.uid.clone();
|
||||
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
@@ -153,7 +168,7 @@ impl IProfiles {
|
||||
if self.current.is_none()
|
||||
&& (item.itype == Some("remote".into()) || item.itype == Some("local".into()))
|
||||
{
|
||||
self.current = uid;
|
||||
self.current = uid.to_owned();
|
||||
}
|
||||
|
||||
if self.items.is_none() {
|
||||
@@ -161,24 +176,23 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
if let Some(items) = self.items.as_mut() {
|
||||
items.push(item)
|
||||
items.push(item.to_owned());
|
||||
}
|
||||
|
||||
// self.save_file().await
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// reorder items
|
||||
pub async fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
|
||||
pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or_default();
|
||||
let mut old_index = None;
|
||||
let mut new_index = None;
|
||||
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == Some(active_id.clone()) {
|
||||
if items[i].uid.as_ref() == Some(active_id) {
|
||||
old_index = Some(i);
|
||||
}
|
||||
if items[i].uid == Some(over_id.clone()) {
|
||||
if items[i].uid.as_ref() == Some(over_id) {
|
||||
new_index = Some(i);
|
||||
}
|
||||
}
|
||||
@@ -194,11 +208,11 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// update the item value
|
||||
pub async fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||
pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or_default();
|
||||
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == Some(uid.clone()) {
|
||||
if each.uid.as_ref() == Some(uid) {
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
patch!(each, item, desc);
|
||||
@@ -220,13 +234,13 @@ impl IProfiles {
|
||||
|
||||
/// be used to update the remote item
|
||||
/// only patch `updated` `extra` `file_data`
|
||||
pub async fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
|
||||
pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
// find the item
|
||||
let _ = self.get_item(&uid)?;
|
||||
let _ = self.get_item(uid)?;
|
||||
|
||||
if let Some(items) = self.items.as_mut() {
|
||||
let some_uid = Some(uid.clone());
|
||||
@@ -235,8 +249,8 @@ impl IProfiles {
|
||||
if each.uid == some_uid {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
each.home = item.home;
|
||||
each.option = PrfOption::merge(each.option.clone(), item.option);
|
||||
each.home = item.home.to_owned();
|
||||
each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref());
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
if let Some(file_data) = item.file_data.take() {
|
||||
@@ -267,115 +281,58 @@ impl IProfiles {
|
||||
|
||||
/// delete item
|
||||
/// if delete the current then return true
|
||||
pub async fn delete_item(&mut self, uid: String) -> Result<bool> {
|
||||
let current = self.current.as_ref().unwrap_or(&uid);
|
||||
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> {
|
||||
let current = self.current.as_ref().unwrap_or(uid);
|
||||
let current = current.clone();
|
||||
let item = self.get_item(&uid)?;
|
||||
let item = self.get_item(uid)?;
|
||||
let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone());
|
||||
let script_uid = item.option.as_ref().and_then(|e| e.script.clone());
|
||||
let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone());
|
||||
let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone());
|
||||
let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone());
|
||||
let mut items = self.items.take().unwrap_or_default();
|
||||
let mut index = None;
|
||||
let mut merge_index = None;
|
||||
let mut script_index = None;
|
||||
let mut rules_index = None;
|
||||
let mut proxies_index = None;
|
||||
let mut groups_index = None;
|
||||
|
||||
// get the index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == Some(uid.clone()) {
|
||||
index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(index) = index
|
||||
&& let Some(file) = items.remove(index).file
|
||||
{
|
||||
// remove the main item (if exists) and delete its file
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.clone())) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
}
|
||||
// get the merge index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == merge_uid {
|
||||
merge_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(index) = merge_index
|
||||
&& let Some(file) = items.remove(index).file
|
||||
{
|
||||
|
||||
// remove related extension items (merge, script, rules, proxies, groups)
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
}
|
||||
// get the script index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == script_uid {
|
||||
script_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(index) = script_index
|
||||
&& let Some(file) = items.remove(index).file
|
||||
{
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
}
|
||||
// get the rules index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == rules_uid {
|
||||
rules_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(index) = rules_index
|
||||
&& let Some(file) = items.remove(index).file
|
||||
{
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
}
|
||||
// get the proxies index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == proxies_uid {
|
||||
proxies_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(index) = proxies_index
|
||||
&& let Some(file) = items.remove(index).file
|
||||
{
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
}
|
||||
// get the groups index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == groups_uid {
|
||||
groups_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(index) = groups_index
|
||||
&& let Some(file) = items.remove(index).file
|
||||
{
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
}
|
||||
// delete the original uid
|
||||
if current == uid {
|
||||
if current == *uid {
|
||||
self.current = None;
|
||||
for item in items.iter() {
|
||||
if item.itype == Some("remote".into()) || item.itype == Some("local".into()) {
|
||||
@@ -387,7 +344,7 @@ impl IProfiles {
|
||||
|
||||
self.items = Some(items);
|
||||
self.save_file().await?;
|
||||
Ok(current == uid)
|
||||
Ok(current == *uid)
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅内容
|
||||
@@ -478,8 +435,8 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// 判断profile是否是current指向的
|
||||
pub fn is_current_profile_index(&self, index: String) -> bool {
|
||||
self.current == Some(index)
|
||||
pub fn is_current_profile_index(&self, index: &String) -> bool {
|
||||
self.current.as_ref() == Some(index)
|
||||
}
|
||||
|
||||
/// 获取所有的profiles(uid,名称)
|
||||
@@ -498,6 +455,18 @@ impl IProfiles {
|
||||
})
|
||||
}
|
||||
|
||||
/// 通过 uid 获取名称
|
||||
pub fn get_name_by_uid(&self, uid: &String) -> Option<String> {
|
||||
if let Some(items) = &self.items {
|
||||
for item in items {
|
||||
if item.uid.as_ref() == Some(uid) {
|
||||
return item.name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 以 app 中的 profile 列表为准,删除不再需要的文件
|
||||
pub async fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
|
||||
let profiles_dir = dirs::app_profiles_dir()?;
|
||||
@@ -536,7 +505,7 @@ impl IProfiles {
|
||||
{
|
||||
// 检查是否为全局扩展文件
|
||||
if protected_files.contains(file_name) {
|
||||
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
|
||||
logging!(debug, Type::Config, "保护全局扩展配置文件: {file_name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -545,11 +514,15 @@ impl IProfiles {
|
||||
match path.to_path_buf().remove_if_exists().await {
|
||||
Ok(_) => {
|
||||
deleted_files.push(file_name.into());
|
||||
log::info!(target: "app", "已清理冗余文件: {file_name}");
|
||||
logging!(info, Type::Config, "已清理冗余文件: {file_name}");
|
||||
}
|
||||
Err(e) => {
|
||||
failed_deletions.push(format!("{file_name}: {e}").into());
|
||||
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"Warning: 清理文件失败: {file_name} - {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,8 +535,9 @@ impl IProfiles {
|
||||
failed_deletions,
|
||||
};
|
||||
|
||||
log::info!(
|
||||
target: "app",
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
|
||||
result.total_files,
|
||||
result.deleted_files.len(),
|
||||
@@ -671,14 +645,14 @@ impl IProfiles {
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn profiles_append_item_with_filedata_safe(
|
||||
item: PrfItem,
|
||||
item: &PrfItem,
|
||||
file_data: Option<String>,
|
||||
) -> Result<()> {
|
||||
let item = PrfItem::from(item, file_data).await?;
|
||||
let item = &mut PrfItem::from(item, file_data).await?;
|
||||
profiles_append_item_safe(item).await
|
||||
}
|
||||
|
||||
pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> {
|
||||
pub async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
@@ -688,7 +662,7 @@ pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()> {
|
||||
pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
@@ -698,7 +672,7 @@ pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
|
||||
pub async fn profiles_delete_item_safe(index: &String) -> Result<bool> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
@@ -708,7 +682,7 @@ pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_reorder_safe(active_id: String, over_id: String) -> Result<()> {
|
||||
pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
@@ -728,7 +702,7 @@ pub async fn profiles_save_file_safe() -> Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_draft_update_item_safe(index: String, item: PrfItem) -> Result<()> {
|
||||
pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
|
||||
logging,
|
||||
@@ -203,8 +204,7 @@ pub struct IVerge {
|
||||
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
|
||||
// pub enable_tray_icon: Option<bool>,
|
||||
/// show proxy groups directly on tray root menu
|
||||
pub tray_inline_proxy_groups: Option<bool>,
|
||||
|
||||
@@ -305,19 +305,17 @@ impl IVerge {
|
||||
|
||||
/// 配置修正后重新加载配置
|
||||
async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
|
||||
use crate::config::Config;
|
||||
|
||||
let config_draft = Config::verge().await;
|
||||
*config_draft.draft_mut() = Box::new(updated_config.clone());
|
||||
config_draft.apply();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"内存配置已强制更新,新的clash_core: {:?}",
|
||||
updated_config.clash_core
|
||||
&updated_config.clash_core
|
||||
);
|
||||
|
||||
let config_draft = Config::verge().await;
|
||||
**config_draft.draft_mut() = updated_config;
|
||||
config_draft.apply();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -355,12 +353,12 @@ impl IVerge {
|
||||
config
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::template()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
@@ -419,7 +417,7 @@ impl IVerge {
|
||||
webdav_username: None,
|
||||
webdav_password: None,
|
||||
enable_tray_speed: Some(false),
|
||||
enable_tray_icon: Some(true),
|
||||
// enable_tray_icon: Some(true),
|
||||
tray_inline_proxy_groups: Some(false),
|
||||
enable_global_hotkey: Some(true),
|
||||
enable_auto_light_weight_mode: Some(false),
|
||||
@@ -438,11 +436,12 @@ impl IVerge {
|
||||
|
||||
/// patch verge config
|
||||
/// only save to file
|
||||
pub fn patch_config(&mut self, patch: IVerge) {
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn patch_config(&mut self, patch: &IVerge) {
|
||||
macro_rules! patch {
|
||||
($key: tt) => {
|
||||
if patch.$key.is_some() {
|
||||
self.$key = patch.$key;
|
||||
self.$key = patch.$key.clone();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -514,7 +513,7 @@ impl IVerge {
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
patch!(enable_tray_speed);
|
||||
patch!(enable_tray_icon);
|
||||
// patch!(enable_tray_icon);
|
||||
patch!(tray_inline_proxy_groups);
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
@@ -608,7 +607,7 @@ pub struct IVergeResponse {
|
||||
pub webdav_username: Option<String>,
|
||||
pub webdav_password: Option<String>,
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
// pub enable_tray_icon: Option<bool>,
|
||||
pub tray_inline_proxy_groups: Option<bool>,
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
@@ -685,7 +684,7 @@ impl From<IVerge> for IVergeResponse {
|
||||
webdav_username: verge.webdav_username,
|
||||
webdav_password: verge.webdav_password,
|
||||
enable_tray_speed: verge.enable_tray_speed,
|
||||
enable_tray_icon: verge.enable_tray_icon,
|
||||
// enable_tray_icon: verge.enable_tray_icon,
|
||||
tray_inline_proxy_groups: verge.tray_inline_proxy_groups,
|
||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
@@ -697,3 +696,9 @@ impl From<IVerge> for IVergeResponse {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<IVerge>> for IVergeResponse {
|
||||
fn from(verge: Box<IVerge>) -> Self {
|
||||
IVergeResponse::from(*verge)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,13 @@ pub mod network {
|
||||
pub const DEFAULT_EXTERNAL_CONTROLLER: &str = "127.0.0.1:9097";
|
||||
|
||||
pub mod ports {
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub const DEFAULT_REDIR: u16 = 7895;
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const DEFAULT_TPROXY: u16 = 7896;
|
||||
pub const DEFAULT_MIXED: u16 = 7897;
|
||||
pub const DEFAULT_SOCKS: u16 = 7898;
|
||||
pub const DEFAULT_HTTP: u16 = 7899;
|
||||
#[allow(dead_code)]
|
||||
pub const DEFAULT_EXTERNAL_CONTROLLER: u16 = 9097;
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
pub const SINGLETON_SERVER: u16 = 33331;
|
||||
@@ -39,11 +37,8 @@ pub mod timing {
|
||||
|
||||
pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(500);
|
||||
pub const CONFIG_RELOAD_DELAY: Duration = Duration::from_millis(300);
|
||||
pub const PROCESS_VERIFY_DELAY: Duration = Duration::from_millis(100);
|
||||
#[allow(dead_code)]
|
||||
pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20);
|
||||
pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2);
|
||||
#[allow(dead_code)]
|
||||
pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -53,40 +48,16 @@ pub mod timing {
|
||||
}
|
||||
|
||||
pub mod retry {
|
||||
#[allow(dead_code)]
|
||||
pub const EVENT_EMIT_THRESHOLD: u64 = 10;
|
||||
#[allow(dead_code)]
|
||||
pub const SWR_ERROR_RETRY: usize = 2;
|
||||
}
|
||||
|
||||
pub mod files {
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
#[allow(dead_code)]
|
||||
pub const DNS_CONFIG: &str = "dns_config.yaml";
|
||||
#[allow(dead_code)]
|
||||
pub const WINDOW_STATE: &str = "window_state.json";
|
||||
}
|
||||
|
||||
pub mod process {
|
||||
pub const VERGE_MIHOMO: &str = "verge-mihomo";
|
||||
pub const VERGE_MIHOMO_ALPHA: &str = "verge-mihomo-alpha";
|
||||
|
||||
pub fn process_names() -> [&'static str; 2] {
|
||||
[VERGE_MIHOMO, VERGE_MIHOMO_ALPHA]
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn with_extension(name: &str) -> String {
|
||||
format!("{}.exe", name)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn with_extension(name: &str) -> String {
|
||||
name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod error_patterns {
|
||||
pub const CONNECTION_ERRORS: &[&str] = &[
|
||||
"Failed to create connection",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time::{Duration, timeout};
|
||||
@@ -41,15 +42,21 @@ impl AsyncProxyQuery {
|
||||
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);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"异步获取自动代理成功: enable={}, url={}",
|
||||
proxy.enable,
|
||||
proxy.url
|
||||
);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "异步获取自动代理失败: {e}");
|
||||
logging!(warn, Type::Network, "Warning: 异步获取自动代理失败: {e}");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "异步获取自动代理超时");
|
||||
logging!(warn, Type::Network, "Warning: 异步获取自动代理超时");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
}
|
||||
@@ -59,15 +66,22 @@ impl AsyncProxyQuery {
|
||||
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);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"异步获取系统代理成功: enable={}, {}:{}",
|
||||
proxy.enable,
|
||||
proxy.host,
|
||||
proxy.port
|
||||
);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "异步获取系统代理失败: {e}");
|
||||
logging!(warn, Type::Network, "Warning: 异步获取系统代理失败: {e}");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "异步获取系统代理超时");
|
||||
logging!(warn, Type::Network, "Warning: 异步获取系统代理超时");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
}
|
||||
@@ -99,7 +113,7 @@ impl AsyncProxyQuery {
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||
|
||||
if result != 0 {
|
||||
log::debug!(target: "app", "无法打开注册表项");
|
||||
logging!(debug, Type::Network, "无法打开注册表项");
|
||||
return Ok(AsyncAutoproxy::default());
|
||||
}
|
||||
|
||||
@@ -125,7 +139,7 @@ impl AsyncProxyQuery {
|
||||
.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}");
|
||||
logging!(debug, Type::Network, "从注册表读取到PAC URL: {pac_url}");
|
||||
}
|
||||
|
||||
// 2. 检查自动检测设置是否启用
|
||||
@@ -150,7 +164,11 @@ impl AsyncProxyQuery {
|
||||
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
|
||||
|
||||
if pac_enabled {
|
||||
log::debug!(target: "app", "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}");
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}"
|
||||
);
|
||||
|
||||
if pac_url.is_empty() && auto_detect != 0 {
|
||||
pac_url = "auto-detect".into();
|
||||
@@ -161,7 +179,7 @@ impl AsyncProxyQuery {
|
||||
url: pac_url,
|
||||
})
|
||||
} else {
|
||||
log::debug!(target: "app", "PAC配置未启用");
|
||||
logging!(debug, Type::Network, "PAC配置未启用");
|
||||
Ok(AsyncAutoproxy::default())
|
||||
}
|
||||
}
|
||||
@@ -177,7 +195,11 @@ impl AsyncProxyQuery {
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
log::debug!(target: "app", "scutil output: {stdout}");
|
||||
crate::logging!(
|
||||
debug,
|
||||
crate::utils::logging::Type::Network,
|
||||
"scutil output: {stdout}"
|
||||
);
|
||||
|
||||
let mut pac_enabled = false;
|
||||
let mut pac_url = String::new();
|
||||
@@ -196,7 +218,11 @@ impl AsyncProxyQuery {
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
|
||||
crate::logging!(
|
||||
debug,
|
||||
crate::utils::logging::Type::Network,
|
||||
"解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}"
|
||||
);
|
||||
|
||||
Ok(AsyncAutoproxy {
|
||||
enable: pac_enabled && !pac_url.is_empty(),
|
||||
@@ -363,7 +389,11 @@ impl AsyncProxyQuery {
|
||||
(proxy_server, 8080)
|
||||
};
|
||||
|
||||
log::debug!(target: "app", "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}");
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}"
|
||||
);
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
@@ -386,7 +416,7 @@ impl AsyncProxyQuery {
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
log::debug!(target: "app", "scutil proxy output: {stdout}");
|
||||
logging!(debug, Type::Network, "scutil proxy output: {stdout}");
|
||||
|
||||
let mut http_enabled = false;
|
||||
let mut http_host = String::new();
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use crate::constants::files::DNS_CONFIG;
|
||||
use crate::{
|
||||
config::Config,
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::{dirs, logging::Type},
|
||||
};
|
||||
use anyhow::Error;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
@@ -7,13 +13,12 @@ use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::{consts::OS, temp_dir},
|
||||
fs,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
use tokio::{fs, time::timeout};
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
// 应用版本常量,来自 tauri.conf.json
|
||||
@@ -138,9 +143,14 @@ impl WebDavClient {
|
||||
.is_err()
|
||||
{
|
||||
match client.mkcol(dirs::BACKUP_DIR).await {
|
||||
Ok(_) => log::info!("Successfully created backup directory"),
|
||||
Ok(_) => logging!(info, Type::Backup, "Successfully created backup directory"),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to create backup directory: {}", e);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Backup,
|
||||
"Warning: Failed to create backup directory: {}",
|
||||
e
|
||||
);
|
||||
// 清除缓存,强制下次重新尝试
|
||||
self.reset();
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
@@ -170,7 +180,7 @@ impl WebDavClient {
|
||||
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name).into();
|
||||
|
||||
// 读取文件并上传,如果失败尝试一次重试
|
||||
let file_content = fs::read(&file_path)?;
|
||||
let file_content = fs::read(&file_path).await?;
|
||||
|
||||
// 添加超时保护
|
||||
let upload_result = timeout(
|
||||
@@ -181,7 +191,11 @@ impl WebDavClient {
|
||||
|
||||
match upload_result {
|
||||
Err(_) => {
|
||||
log::warn!("Upload timed out, retrying once");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Backup,
|
||||
"Warning: Upload timed out, retrying once"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
timeout(
|
||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||
@@ -192,7 +206,11 @@ impl WebDavClient {
|
||||
}
|
||||
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("Upload failed, retrying once: {e}");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Backup,
|
||||
"Warning: Upload failed, retrying once: {e}"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
timeout(
|
||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||
@@ -212,7 +230,7 @@ impl WebDavClient {
|
||||
let fut = async {
|
||||
let response = client.get(path.as_str()).await?;
|
||||
let content = response.bytes().await?;
|
||||
fs::write(&storage_path, &content)?;
|
||||
fs::write(&storage_path, &content).await?;
|
||||
Ok::<(), Error>(())
|
||||
};
|
||||
|
||||
@@ -250,18 +268,19 @@ impl WebDavClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
pub async fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
|
||||
let zip_file_name: String = format!("{OS}-backup-{now}.zip").into();
|
||||
let zip_path = temp_dir().join(zip_file_name.as_str());
|
||||
|
||||
let file = fs::File::create(&zip_path)?;
|
||||
let value = zip_path.clone();
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::create(&value)).await??;
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
zip.add_directory("profiles/", SimpleFileOptions::default())?;
|
||||
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
|
||||
if let Ok(mut entries) = fs::read_dir(dirs::app_profiles_dir()?).await {
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let file_name_os = entry.file_name();
|
||||
@@ -270,16 +289,16 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
.ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?;
|
||||
let backup_path = format!("profiles/{}", file_name);
|
||||
zip.start_file(backup_path, options)?;
|
||||
let file_content = fs::read(&path)?;
|
||||
let file_content = fs::read(&path).await?;
|
||||
zip.write_all(&file_content)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
zip.start_file(dirs::CLASH_CONFIG, options)?;
|
||||
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
|
||||
zip.write_all(fs::read(dirs::clash_path()?).await?.as_slice())?;
|
||||
|
||||
let mut verge_config: serde_json::Value =
|
||||
serde_yaml_ng::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
|
||||
let verge_text = fs::read_to_string(dirs::verge_path()?).await?;
|
||||
let mut verge_config: serde_json::Value = serde_yaml_ng::from_str(&verge_text)?;
|
||||
if let Some(obj) = verge_config.as_object_mut() {
|
||||
obj.remove("webdav_username");
|
||||
obj.remove("webdav_password");
|
||||
@@ -288,14 +307,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
zip.start_file(dirs::VERGE_CONFIG, options)?;
|
||||
zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?;
|
||||
|
||||
let dns_config_path = dirs::app_home_dir()?.join(dirs::DNS_CONFIG);
|
||||
let dns_config_path = dirs::app_home_dir()?.join(DNS_CONFIG);
|
||||
if dns_config_path.exists() {
|
||||
zip.start_file(dirs::DNS_CONFIG, options)?;
|
||||
zip.write_all(fs::read(&dns_config_path)?.as_slice())?;
|
||||
zip.start_file(DNS_CONFIG, options)?;
|
||||
zip.write_all(fs::read(&dns_config_path).await?.as_slice())?;
|
||||
}
|
||||
|
||||
zip.start_file(dirs::PROFILE_YAML, options)?;
|
||||
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
|
||||
zip.write_all(fs::read(dirs::profiles_path()?).await?.as_slice())?;
|
||||
zip.finish()?;
|
||||
Ok((zip_file_name, zip_path))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
||||
use crate::config::{Config, IVerge};
|
||||
use crate::core::{async_proxy_query::AsyncProxyQuery, handle};
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use once_cell::sync::Lazy;
|
||||
use smartstring::alias::String;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
@@ -104,14 +105,14 @@ impl EventDrivenProxyManager {
|
||||
let query = QueryRequest { response_tx: tx };
|
||||
|
||||
if self.query_sender.send(query).is_err() {
|
||||
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
|
||||
logging!(error, Type::Network, "发送查询请求失败,返回缓存数据");
|
||||
return self.get_auto_proxy_cached().await;
|
||||
}
|
||||
|
||||
match timeout(Duration::from_secs(5), rx).await {
|
||||
Ok(Ok(result)) => result,
|
||||
_ => {
|
||||
log::warn!(target: "app", "查询超时,返回缓存数据");
|
||||
logging!(warn, Type::Network, "Warning: 查询超时,返回缓存数据");
|
||||
self.get_auto_proxy_cached().await
|
||||
}
|
||||
}
|
||||
@@ -134,7 +135,7 @@ impl EventDrivenProxyManager {
|
||||
|
||||
fn send_event(&self, event: ProxyEvent) {
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
log::error!(target: "app", "发送代理事件失败: {e}");
|
||||
logging!(error, Type::Network, "发送代理事件失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ impl EventDrivenProxyManager {
|
||||
event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
|
||||
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
||||
) {
|
||||
log::info!(target: "app", "事件驱动代理管理器启动");
|
||||
logging!(info, Type::Network, "事件驱动代理管理器启动");
|
||||
|
||||
// 将 mpsc 接收器包装成 Stream,避免每次循环创建 future
|
||||
let mut event_stream = UnboundedReceiverStream::new(event_rx);
|
||||
@@ -158,7 +159,7 @@ impl EventDrivenProxyManager {
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(event) = event_stream.next() => {
|
||||
log::debug!(target: "app", "处理代理事件: {event:?}");
|
||||
logging!(debug, Type::Network, "处理代理事件: {event:?}");
|
||||
let event_clone = event.clone(); // 保存一份副本用于后续检查
|
||||
Self::handle_event(&state, event).await;
|
||||
|
||||
@@ -179,13 +180,13 @@ impl EventDrivenProxyManager {
|
||||
// 定时检查代理设置
|
||||
let config = Self::get_proxy_config().await;
|
||||
if config.guard_enabled && config.sys_enabled {
|
||||
log::debug!(target: "app", "定时检查代理设置");
|
||||
logging!(debug, Type::Network, "定时检查代理设置");
|
||||
Self::check_and_restore_proxy(&state).await;
|
||||
}
|
||||
}
|
||||
else => {
|
||||
// 两个通道都关闭时退出
|
||||
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
|
||||
logging!(info, Type::Network, "事件或查询通道关闭,代理管理器停止");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -201,7 +202,7 @@ impl EventDrivenProxyManager {
|
||||
Self::initialize_proxy_state(state).await;
|
||||
}
|
||||
ProxyEvent::AppStopping => {
|
||||
log::info!(target: "app", "清理代理状态");
|
||||
logging!(info, Type::Network, "清理代理状态");
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_enabled = false;
|
||||
s.pac_enabled = false;
|
||||
@@ -224,7 +225,7 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
|
||||
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "初始化代理状态");
|
||||
logging!(info, Type::Network, "初始化代理状态");
|
||||
|
||||
let config = Self::get_proxy_config().await;
|
||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||
@@ -239,11 +240,17 @@ impl EventDrivenProxyManager {
|
||||
})
|
||||
.await;
|
||||
|
||||
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
||||
logging!(
|
||||
info,
|
||||
Type::Network,
|
||||
"代理状态初始化完成: sys={}, pac={}",
|
||||
config.sys_enabled,
|
||||
config.pac_enabled
|
||||
);
|
||||
}
|
||||
|
||||
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::debug!(target: "app", "更新代理配置");
|
||||
logging!(debug, Type::Network, "更新代理配置");
|
||||
|
||||
let config = Self::get_proxy_config().await;
|
||||
|
||||
@@ -260,7 +267,7 @@ impl EventDrivenProxyManager {
|
||||
|
||||
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过系统代理守卫检查");
|
||||
logging!(debug, Type::Network, "应用正在退出,跳过系统代理守卫检查");
|
||||
return;
|
||||
}
|
||||
let (sys_enabled, pac_enabled) = {
|
||||
@@ -272,7 +279,7 @@ impl EventDrivenProxyManager {
|
||||
return;
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "检查代理状态");
|
||||
logging!(debug, Type::Network, "检查代理状态");
|
||||
|
||||
if pac_enabled {
|
||||
Self::check_and_restore_pac_proxy(state).await;
|
||||
@@ -283,7 +290,7 @@ impl EventDrivenProxyManager {
|
||||
|
||||
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过PAC代理恢复检查");
|
||||
logging!(debug, Type::Network, "应用正在退出,跳过PAC代理恢复检查");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -296,9 +303,9 @@ impl EventDrivenProxyManager {
|
||||
.await;
|
||||
|
||||
if !current.enable || current.url != expected.url {
|
||||
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
|
||||
logging!(info, Type::Network, "PAC代理设置异常,正在恢复...");
|
||||
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
|
||||
log::error!(target: "app", "恢复PAC代理失败: {}", e);
|
||||
logging!(error, Type::Network, "恢复PAC代理失败: {}", e);
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
@@ -314,7 +321,7 @@ impl EventDrivenProxyManager {
|
||||
|
||||
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过系统代理恢复检查");
|
||||
logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复检查");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,9 +334,9 @@ impl EventDrivenProxyManager {
|
||||
.await;
|
||||
|
||||
if !current.enable || current.host != expected.host || current.port != expected.port {
|
||||
log::info!(target: "app", "系统代理设置异常,正在恢复...");
|
||||
logging!(info, Type::Network, "系统代理设置异常,正在恢复...");
|
||||
if let Err(e) = Self::restore_sys_proxy(&expected).await {
|
||||
log::error!(target: "app", "恢复系统代理失败: {}", e);
|
||||
logging!(error, Type::Network, "恢复系统代理失败: {}", e);
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
@@ -457,7 +464,7 @@ impl EventDrivenProxyManager {
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过PAC代理恢复");
|
||||
logging!(debug, Type::Network, "应用正在退出,跳过PAC代理恢复");
|
||||
return Ok(());
|
||||
}
|
||||
Self::execute_sysproxy_command(&["pac", expected_url]).await
|
||||
@@ -481,7 +488,7 @@ impl EventDrivenProxyManager {
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过系统代理恢复");
|
||||
logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复");
|
||||
return Ok(());
|
||||
}
|
||||
let address = format!("{}:{}", expected.host, expected.port);
|
||||
@@ -502,8 +509,9 @@ impl EventDrivenProxyManager {
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(
|
||||
target: "app",
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"应用正在退出,取消调用 sysproxy.exe,参数: {:?}",
|
||||
args
|
||||
);
|
||||
@@ -518,14 +526,14 @@ impl EventDrivenProxyManager {
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "获取服务路径失败: {e}");
|
||||
logging!(error, Type::Network, "获取服务路径失败: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe 不存在");
|
||||
logging!(error, Type::Network, "sysproxy.exe 不存在");
|
||||
}
|
||||
anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist");
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use anyhow::{Result, bail};
|
||||
use parking_lot::Mutex;
|
||||
use smartstring::alias::String;
|
||||
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||
|
||||
/// Enum representing all available hotkey functions
|
||||
@@ -105,66 +104,53 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
/// Execute the function associated with a hotkey function enum
|
||||
fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) {
|
||||
let app_handle = app_handle.clone();
|
||||
fn execute_function(function: HotkeyFunction) {
|
||||
match function {
|
||||
HotkeyFunction::OpenOrCloseDashboard => {
|
||||
AsyncHandler::spawn(async move || {
|
||||
crate::feat::open_or_close_dashboard().await;
|
||||
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
|
||||
notify_event(NotificationEvent::DashboardToggled).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ClashModeRule => {
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::change_clash_mode("rule".into()).await;
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
||||
)
|
||||
.await;
|
||||
notify_event(NotificationEvent::ClashModeChanged { mode: "Rule" }).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ClashModeGlobal => {
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::change_clash_mode("global".into()).await;
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Global" },
|
||||
)
|
||||
.await;
|
||||
notify_event(NotificationEvent::ClashModeChanged { mode: "Global" }).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ClashModeDirect => {
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::change_clash_mode("direct".into()).await;
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
||||
)
|
||||
.await;
|
||||
notify_event(NotificationEvent::ClashModeChanged { mode: "Direct" }).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ToggleSystemProxy => {
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::toggle_system_proxy().await;
|
||||
notify_event(app_handle, NotificationEvent::SystemProxyToggled).await;
|
||||
notify_event(NotificationEvent::SystemProxyToggled).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ToggleTunMode => {
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::toggle_tun_mode(None).await;
|
||||
notify_event(app_handle, NotificationEvent::TunModeToggled).await;
|
||||
notify_event(NotificationEvent::TunModeToggled).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::EntryLightweightMode => {
|
||||
AsyncHandler::spawn(async move || {
|
||||
entry_lightweight_mode().await;
|
||||
notify_event(app_handle, NotificationEvent::LightweightModeEntered).await;
|
||||
notify_event(NotificationEvent::LightweightModeEntered).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::Quit => {
|
||||
AsyncHandler::spawn(async move || {
|
||||
notify_event(app_handle, NotificationEvent::AppQuit).await;
|
||||
notify_event(NotificationEvent::AppQuit).await;
|
||||
feat::quit().await;
|
||||
});
|
||||
}
|
||||
@@ -172,7 +158,7 @@ impl Hotkey {
|
||||
HotkeyFunction::Hide => {
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::hide().await;
|
||||
notify_event(app_handle, NotificationEvent::AppHidden).await;
|
||||
notify_event(NotificationEvent::AppHidden).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -224,14 +210,12 @@ impl Hotkey {
|
||||
|
||||
let is_quit = matches!(function, HotkeyFunction::Quit);
|
||||
|
||||
manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
|
||||
manager.on_shortcut(hotkey, move |_app_handle, hotkey_event, event| {
|
||||
let hotkey_event_owned = *hotkey_event;
|
||||
let event_owned = event;
|
||||
let function_owned = function;
|
||||
let is_quit_owned = is_quit;
|
||||
|
||||
let app_handle_cloned = app_handle.clone();
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
if event_owned.state == ShortcutState::Pressed {
|
||||
logging!(
|
||||
@@ -242,11 +226,11 @@ impl Hotkey {
|
||||
);
|
||||
|
||||
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
|
||||
if let Some(window) = app_handle_cloned.get_webview_window("main")
|
||||
if let Some(window) = handle::Handle::get_window()
|
||||
&& window.is_focused().unwrap_or(false)
|
||||
{
|
||||
logging!(debug, Type::Hotkey, "Executing quit function");
|
||||
Self::execute_function(function_owned, &app_handle_cloned);
|
||||
Self::execute_function(function_owned);
|
||||
}
|
||||
} else {
|
||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||
@@ -258,14 +242,14 @@ impl Hotkey {
|
||||
.unwrap_or(true);
|
||||
|
||||
if is_enable_global_hotkey {
|
||||
Self::execute_function(function_owned, &app_handle_cloned);
|
||||
Self::execute_function(function_owned);
|
||||
} else {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let is_visible = WindowManager::is_main_window_visible();
|
||||
let is_focused = WindowManager::is_main_window_focused();
|
||||
|
||||
if is_focused && is_visible {
|
||||
Self::execute_function(function_owned, &app_handle_cloned);
|
||||
Self::execute_function(function_owned);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,6 @@
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use compact_str::CompactString;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::{RwLock, RwLockReadGuard};
|
||||
use clash_verge_logger::AsyncLogger;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const LOGS_QUEUE_LEN: usize = 100;
|
||||
|
||||
pub struct ClashLogger {
|
||||
logs: Arc<RwLock<VecDeque<CompactString>>>,
|
||||
}
|
||||
|
||||
impl ClashLogger {
|
||||
pub fn global() -> &'static ClashLogger {
|
||||
static LOGGER: OnceCell<ClashLogger> = OnceCell::new();
|
||||
|
||||
LOGGER.get_or_init(|| ClashLogger {
|
||||
logs: Arc::new(RwLock::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_logs(&self) -> RwLockReadGuard<'_, VecDeque<CompactString>> {
|
||||
self.logs.read()
|
||||
}
|
||||
|
||||
pub fn append_log(&self, text: CompactString) {
|
||||
let mut logs = self.logs.write();
|
||||
if logs.len() > LOGS_QUEUE_LEN {
|
||||
logs.pop_front();
|
||||
}
|
||||
logs.push_back(text);
|
||||
}
|
||||
|
||||
pub fn clear_logs(&self) {
|
||||
let mut logs = self.logs.write();
|
||||
logs.clear();
|
||||
}
|
||||
}
|
||||
pub static CLASH_LOGGER: Lazy<Arc<AsyncLogger>> = Lazy::new(|| Arc::new(AsyncLogger::new()));
|
||||
|
||||
@@ -19,11 +19,11 @@ impl CoreManager {
|
||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||
let clash_config = Config::clash().await.latest_ref().0.clone();
|
||||
|
||||
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
|
||||
**Config::runtime().await.draft_mut() = IRuntime {
|
||||
config: Some(clash_config.clone()),
|
||||
exists_keys: vec![],
|
||||
chain_logs: Default::default(),
|
||||
});
|
||||
};
|
||||
|
||||
help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?;
|
||||
handle::Handle::notice_message(error_key, error_msg);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::{CoreManager, RunningMode};
|
||||
use crate::config::{Config, ConfigType, IVerge};
|
||||
use crate::{
|
||||
core::{
|
||||
logger::ClashLogger,
|
||||
logger::CLASH_LOGGER,
|
||||
service::{SERVICE_MANAGER, ServiceStatus},
|
||||
},
|
||||
logging,
|
||||
@@ -14,16 +15,16 @@ impl CoreManager {
|
||||
pub async fn start_core(&self) -> Result<()> {
|
||||
self.prepare_startup().await?;
|
||||
|
||||
match self.get_running_mode() {
|
||||
match *self.get_running_mode() {
|
||||
RunningMode::Service => self.start_core_by_service().await,
|
||||
RunningMode::NotRunning | RunningMode::Sidecar => self.start_core_by_sidecar().await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_core(&self) -> Result<()> {
|
||||
ClashLogger::global().clear_logs();
|
||||
CLASH_LOGGER.clear_logs().await;
|
||||
|
||||
match self.get_running_mode() {
|
||||
match *self.get_running_mode() {
|
||||
RunningMode::Service => self.stop_core_by_service().await,
|
||||
RunningMode::Sidecar => self.stop_core_by_sidecar(),
|
||||
RunningMode::NotRunning => Ok(()),
|
||||
@@ -41,18 +42,12 @@ impl CoreManager {
|
||||
self.start_core().await
|
||||
}
|
||||
|
||||
pub async fn change_core(&self, clash_core: Option<String>) -> Result<(), String> {
|
||||
use crate::config::{Config, ConfigType, IVerge};
|
||||
|
||||
let core = clash_core
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Clash core cannot be None".to_string())?;
|
||||
|
||||
if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) {
|
||||
return Err(format!("Invalid clash core: {}", core).into());
|
||||
pub async fn change_core(&self, clash_core: &String) -> Result<(), String> {
|
||||
if !IVerge::VALID_CLASH_CORES.contains(&clash_core.as_str()) {
|
||||
return Err(format!("Invalid clash core: {}", clash_core).into());
|
||||
}
|
||||
|
||||
Config::verge().await.draft_mut().clash_core = clash_core;
|
||||
Config::verge().await.draft_mut().clash_core = clash_core.to_owned().into();
|
||||
Config::verge().await.apply();
|
||||
|
||||
let verge_data = Config::verge().await.latest_ref().clone();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
mod config;
|
||||
mod lifecycle;
|
||||
mod process;
|
||||
mod state;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -11,7 +10,7 @@ use tokio::sync::Semaphore;
|
||||
use crate::process::CommandChildGuard;
|
||||
use crate::singleton_lazy;
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, PartialEq, Eq)]
|
||||
#[derive(Debug, serde::Serialize, PartialEq, Eq)]
|
||||
pub enum RunningMode {
|
||||
Service,
|
||||
Sidecar,
|
||||
@@ -37,14 +36,14 @@ pub struct CoreManager {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
running_mode: RunningMode,
|
||||
running_mode: Arc<RunningMode>,
|
||||
child_sidecar: Option<CommandChildGuard>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
running_mode: RunningMode::NotRunning,
|
||||
running_mode: Arc::new(RunningMode::NotRunning),
|
||||
child_sidecar: None,
|
||||
}
|
||||
}
|
||||
@@ -61,16 +60,19 @@ impl Default for CoreManager {
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
pub fn get_running_mode(&self) -> RunningMode {
|
||||
self.state.lock().running_mode
|
||||
pub fn get_running_mode(&self) -> Arc<RunningMode> {
|
||||
Arc::clone(&self.state.lock().running_mode)
|
||||
}
|
||||
|
||||
pub fn set_running_mode(&self, mode: RunningMode) {
|
||||
self.state.lock().running_mode = mode;
|
||||
self.state.lock().running_mode = Arc::new(mode);
|
||||
}
|
||||
|
||||
pub fn set_running_child_sidecar(&self, child: CommandChildGuard) {
|
||||
self.state.lock().child_sidecar = Some(child);
|
||||
}
|
||||
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
self.cleanup_orphaned_processes().await?;
|
||||
self.start_core().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
use super::CoreManager;
|
||||
#[cfg(windows)]
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{
|
||||
constants::{process, timing},
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::Result;
|
||||
#[cfg(windows)]
|
||||
use anyhow::anyhow;
|
||||
|
||||
impl CoreManager {
|
||||
pub async fn cleanup_orphaned_processes(&self) -> Result<()> {
|
||||
logging!(info, Type::Core, "Cleaning orphaned mihomo processes");
|
||||
|
||||
let current_pid = self
|
||||
.state
|
||||
.lock()
|
||||
.child_sidecar
|
||||
.as_ref()
|
||||
.and_then(|c| c.pid());
|
||||
let target_processes = process::process_names();
|
||||
|
||||
let process_futures = target_processes.iter().map(|&name| {
|
||||
let process_name = process::with_extension(name);
|
||||
self.find_processes_by_name(process_name, name)
|
||||
});
|
||||
|
||||
let process_results = futures::future::join_all(process_futures).await;
|
||||
|
||||
let pids_to_kill: Vec<_> = process_results
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.flat_map(|(pids, name)| {
|
||||
pids.into_iter()
|
||||
.filter(move |&pid| Some(pid) != current_pid)
|
||||
.map(move |pid| (pid, name.clone()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if pids_to_kill.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let kill_futures = pids_to_kill
|
||||
.iter()
|
||||
.map(|(pid, name)| self.kill_process_verified(*pid, name.clone()));
|
||||
|
||||
let killed_count = futures::future::join_all(kill_futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|&success| success)
|
||||
.count();
|
||||
|
||||
if killed_count > 0 {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
"Cleaned {} orphaned processes",
|
||||
killed_count
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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,
|
||||
tlhelp32::{
|
||||
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
|
||||
TH32CS_SNAPPROCESS,
|
||||
},
|
||||
};
|
||||
|
||||
let process_name_clone = process_name.clone();
|
||||
let pids = AsyncHandler::spawn_blocking(move || -> Result<Vec<u32>> {
|
||||
let mut pids = Vec::new();
|
||||
|
||||
unsafe {
|
||||
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
|
||||
return Err(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 {
|
||||
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 cmd = if cfg!(target_os = "macos") {
|
||||
"pgrep"
|
||||
} else {
|
||||
"pidof"
|
||||
};
|
||||
let output = tokio::process::Command::new(cmd)
|
||||
.arg(&process_name)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok((Vec::new(), process_name));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let pids: Vec<u32> = stdout
|
||||
.split_whitespace()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
|
||||
Ok((pids, process_name))
|
||||
}
|
||||
}
|
||||
|
||||
async fn kill_process_verified(&self, pid: u32, process_name: String) -> bool {
|
||||
#[cfg(windows)]
|
||||
let success = {
|
||||
use winapi::um::{
|
||||
handleapi::CloseHandle,
|
||||
processthreadsapi::{OpenProcess, TerminateProcess},
|
||||
winnt::{HANDLE, PROCESS_TERMINATE},
|
||||
};
|
||||
|
||||
AsyncHandler::spawn_blocking(move || unsafe {
|
||||
let handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
|
||||
if handle.is_null() {
|
||||
return false;
|
||||
}
|
||||
let result = TerminateProcess(handle, 1) != 0;
|
||||
CloseHandle(handle);
|
||||
result
|
||||
})
|
||||
.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 {
|
||||
return false;
|
||||
}
|
||||
|
||||
tokio::time::sleep(timing::PROCESS_VERIFY_DELAY).await;
|
||||
|
||||
if self.is_process_running(pid).await.unwrap_or(false) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"Process {} (PID: {}) still running after termination",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
false
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
"Terminated process {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_process_running(&self, pid: u32) -> Result<bool> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use winapi::{
|
||||
shared::minwindef::DWORD,
|
||||
um::{
|
||||
handleapi::CloseHandle,
|
||||
processthreadsapi::{GetExitCodeProcess, OpenProcess},
|
||||
winnt::{HANDLE, PROCESS_QUERY_INFORMATION},
|
||||
},
|
||||
};
|
||||
|
||||
AsyncHandler::spawn_blocking(move || unsafe {
|
||||
let handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
|
||||
if handle.is_null() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut exit_code: DWORD = 0;
|
||||
let result = GetExitCodeProcess(handle, &mut exit_code);
|
||||
CloseHandle(handle);
|
||||
Ok(result != 0 && exit_code == 259)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use super::{CoreManager, RunningMode};
|
||||
use crate::{
|
||||
AsyncHandler,
|
||||
config::Config,
|
||||
core::{handle, logger::ClashLogger, service},
|
||||
core::{handle, logger::CLASH_LOGGER, service},
|
||||
logging,
|
||||
process::CommandChildGuard,
|
||||
utils::{
|
||||
@@ -15,15 +15,15 @@ use anyhow::Result;
|
||||
use compact_str::CompactString;
|
||||
use flexi_logger::DeferredNow;
|
||||
use log::Level;
|
||||
use std::collections::VecDeque;
|
||||
use scopeguard::defer;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
impl CoreManager {
|
||||
pub async fn get_clash_logs(&self) -> Result<VecDeque<CompactString>> {
|
||||
match self.get_running_mode() {
|
||||
pub async fn get_clash_logs(&self) -> Result<Vec<CompactString>> {
|
||||
match *self.get_running_mode() {
|
||||
RunningMode::Service => service::get_clash_logs_by_service().await,
|
||||
RunningMode::Sidecar => Ok(ClashLogger::global().get_logs().clone()),
|
||||
RunningMode::NotRunning => Ok(VecDeque::new()),
|
||||
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs().await),
|
||||
RunningMode::NotRunning => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,11 +49,8 @@ impl CoreManager {
|
||||
let pid = child.pid();
|
||||
logging!(trace, Type::Core, "Sidecar started with PID: {}", pid);
|
||||
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
state.child_sidecar = Some(CommandChildGuard::new(child));
|
||||
state.running_mode = RunningMode::Sidecar;
|
||||
}
|
||||
self.set_running_child_sidecar(CommandChildGuard::new(child));
|
||||
self.set_running_mode(RunningMode::Sidecar);
|
||||
|
||||
let shared_writer: SharedWriter =
|
||||
std::sync::Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?));
|
||||
@@ -67,7 +64,7 @@ impl CoreManager {
|
||||
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
|
||||
let w = shared_writer.lock().await;
|
||||
write_sidecar_log(w, &mut now, Level::Error, &message);
|
||||
ClashLogger::global().append_log(message);
|
||||
CLASH_LOGGER.append_log(message).await;
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
|
||||
let mut now = DeferredNow::default();
|
||||
@@ -80,7 +77,7 @@ impl CoreManager {
|
||||
};
|
||||
let w = shared_writer.lock().await;
|
||||
write_sidecar_log(w, &mut now, Level::Info, &message);
|
||||
ClashLogger::global().clear_logs();
|
||||
CLASH_LOGGER.clear_logs().await;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
@@ -93,14 +90,15 @@ impl CoreManager {
|
||||
|
||||
pub(super) fn stop_core_by_sidecar(&self) -> Result<()> {
|
||||
logging!(info, Type::Core, "Stopping sidecar");
|
||||
|
||||
defer! {
|
||||
self.set_running_mode(RunningMode::NotRunning);
|
||||
}
|
||||
let mut state = self.state.lock();
|
||||
if let Some(child) = state.child_sidecar.take() {
|
||||
let pid = child.pid();
|
||||
drop(child);
|
||||
logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid);
|
||||
}
|
||||
state.running_mode = RunningMode::NotRunning;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -114,8 +112,10 @@ impl CoreManager {
|
||||
|
||||
pub(super) async fn stop_core_by_service(&self) -> Result<()> {
|
||||
logging!(info, Type::Core, "Stopping service");
|
||||
defer! {
|
||||
self.set_running_mode(RunningMode::NotRunning);
|
||||
}
|
||||
service::stop_core_by_service().await?;
|
||||
self.set_running_mode(RunningMode::NotRunning);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +96,17 @@ impl NotificationSystem {
|
||||
let handle = Handle::global();
|
||||
|
||||
while !handle.is_exiting() {
|
||||
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
|
||||
match rx.recv() {
|
||||
Ok(event) => Self::process_event(handle, event),
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => break,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"receive event error, stop notification worker: {}",
|
||||
e
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::tray,
|
||||
logging, logging_error,
|
||||
utils::{dirs, init::service_writer_config, logging::Type},
|
||||
};
|
||||
@@ -8,7 +9,6 @@ use clash_verge_service_ipc::CoreConfig;
|
||||
use compact_str::CompactString;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
env::current_exe,
|
||||
path::{Path, PathBuf},
|
||||
process::Command as StdCommand,
|
||||
@@ -394,7 +394,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
start_with_existing_service(config_file).await
|
||||
}
|
||||
|
||||
pub(super) async fn get_clash_logs_by_service() -> Result<VecDeque<CompactString>> {
|
||||
pub(super) async fn get_clash_logs_by_service() -> Result<Vec<CompactString>> {
|
||||
logging!(info, Type::Service, "正在获取服务模式下的 Clash 日志");
|
||||
|
||||
let response = clash_verge_service_ipc::get_clash_logs()
|
||||
@@ -532,6 +532,7 @@ impl ServiceManager {
|
||||
return Err(anyhow::anyhow!("服务不可用: {}", reason));
|
||||
}
|
||||
}
|
||||
let _ = tray::Tray::global().update_tray_display().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
|
||||
pub struct Sysopt {
|
||||
initialed: AtomicBool,
|
||||
update_sysproxy: AtomicBool,
|
||||
reset_sysproxy: AtomicBool,
|
||||
}
|
||||
@@ -84,6 +85,7 @@ async fn execute_sysproxy_command(args: Vec<std::string::String>) -> Result<()>
|
||||
impl Default for Sysopt {
|
||||
fn default() -> Self {
|
||||
Sysopt {
|
||||
initialed: AtomicBool::new(false),
|
||||
update_sysproxy: AtomicBool::new(false),
|
||||
reset_sysproxy: AtomicBool::new(false),
|
||||
}
|
||||
@@ -94,17 +96,22 @@ impl Default for Sysopt {
|
||||
singleton_lazy!(Sysopt, SYSOPT, Sysopt::default);
|
||||
|
||||
impl Sysopt {
|
||||
pub fn is_initialed(&self) -> bool {
|
||||
self.initialed.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
||||
// 使用事件驱动代理管理器
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_app_started();
|
||||
|
||||
log::info!(target: "app", "已启用事件驱动代理守卫");
|
||||
logging!(info, Type::Core, "已启用事件驱动代理守卫");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// init the sysproxy
|
||||
pub async fn update_sysproxy(&self) -> Result<()> {
|
||||
self.initialed.store(true, Ordering::SeqCst);
|
||||
if self
|
||||
.update_sysproxy
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
@@ -224,14 +231,22 @@ impl Sysopt {
|
||||
let mut sysproxy: Sysproxy = match Sysproxy::get_system_proxy() {
|
||||
Ok(sp) => sp,
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "重置代理时获取系统代理配置失败: {e}, 使用默认配置");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"Warning: 重置代理时获取系统代理配置失败: {e}, 使用默认配置"
|
||||
);
|
||||
Sysproxy::default()
|
||||
}
|
||||
};
|
||||
let mut autoproxy = match Autoproxy::get_auto_proxy() {
|
||||
Ok(ap) => ap,
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"Warning: 重置代理时获取自动代理配置失败: {e}, 使用默认配置"
|
||||
);
|
||||
Autoproxy::default()
|
||||
}
|
||||
};
|
||||
@@ -265,14 +280,14 @@ impl Sysopt {
|
||||
{
|
||||
if is_enable {
|
||||
if let Err(e) = startup_shortcut::create_shortcut().await {
|
||||
log::error!(target: "app", "创建启动快捷方式失败: {e}");
|
||||
logging!(error, Type::Setup, "创建启动快捷方式失败: {e}");
|
||||
// 如果快捷方式创建失败,回退到原来的方法
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Err(e) = startup_shortcut::remove_shortcut().await {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {e}");
|
||||
logging!(error, Type::Setup, "删除启动快捷方式失败: {e}");
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
@@ -307,11 +322,11 @@ impl Sysopt {
|
||||
{
|
||||
match startup_shortcut::is_shortcut_enabled() {
|
||||
Ok(enabled) => {
|
||||
log::info!(target: "app", "快捷方式自启动状态: {enabled}");
|
||||
logging!(info, Type::System, "快捷方式自启动状态: {enabled}");
|
||||
return Ok(enabled);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {e}");
|
||||
logging!(error, Type::System, "检查快捷方式失败,尝试原来的方法: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,11 +337,11 @@ impl Sysopt {
|
||||
|
||||
match autostart_manager.is_enabled() {
|
||||
Ok(status) => {
|
||||
log::info!(target: "app", "Auto launch status: {status}");
|
||||
logging!(info, Type::System, "Auto launch status: {status}");
|
||||
Ok(status)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get auto launch status: {e}");
|
||||
logging!(error, Type::System, "Failed to get auto launch status: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::{config::Config, feat, logging, logging_error, singleton, utils::logging::Type};
|
||||
use crate::{
|
||||
config::Config, core::sysopt::Sysopt, feat, logging, logging_error, singleton,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||
use parking_lot::RwLock;
|
||||
@@ -10,7 +13,9 @@ use std::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
||||
type TaskID = u64;
|
||||
|
||||
@@ -99,6 +104,12 @@ impl Timer {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let allow_auto_update =
|
||||
item.option.as_ref()?.allow_auto_update.unwrap_or_default();
|
||||
if !allow_auto_update {
|
||||
return None;
|
||||
}
|
||||
|
||||
let interval = item.option.as_ref()?.update_interval? as i64;
|
||||
let updated = item.updated? as i64;
|
||||
let uid = item.uid.as_ref()?;
|
||||
@@ -149,7 +160,7 @@ impl Timer {
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_count_down_by_seconds(3, 3)
|
||||
.spawn_async_routine(|| async move {
|
||||
logging!(info, Type::Timer, "Updating tray menu");
|
||||
logging!(debug, Type::Timer, "Updating tray menu");
|
||||
crate::core::tray::Tray::global()
|
||||
.update_tray_display()
|
||||
.await
|
||||
@@ -384,7 +395,8 @@ impl Timer {
|
||||
.spawn_async_routine(move || {
|
||||
let uid = uid.clone();
|
||||
Box::pin(async move {
|
||||
Self::async_task(uid).await;
|
||||
Self::wait_until_sysopt(Duration::from_millis(1000)).await;
|
||||
Self::async_task(&uid).await;
|
||||
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
@@ -413,13 +425,15 @@ impl Timer {
|
||||
};
|
||||
|
||||
// Get the profile updated timestamp - now safe to await
|
||||
let config_profiles = Config::profiles().await;
|
||||
let profiles = config_profiles.data_ref().clone();
|
||||
let items = match profiles.get_items() {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "获取配置列表失败");
|
||||
return None;
|
||||
let items = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_guard = profiles.latest_ref();
|
||||
match profiles_guard.get_items() {
|
||||
Some(i) => i.clone(),
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "获取配置列表失败");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -468,14 +482,14 @@ impl Timer {
|
||||
}
|
||||
|
||||
/// Async task with better error handling and logging
|
||||
async fn async_task(uid: String) {
|
||||
async fn async_task(uid: &String) {
|
||||
let task_start = std::time::Instant::now();
|
||||
logging!(info, Type::Timer, "Running timer task for profile: {}", uid);
|
||||
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
|
||||
Self::emit_update_event(&uid, true);
|
||||
Self::emit_update_event(uid, true);
|
||||
|
||||
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(&uid);
|
||||
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(uid);
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
@@ -484,7 +498,7 @@ impl Timer {
|
||||
is_current
|
||||
);
|
||||
|
||||
feat::update_profile(uid.clone(), None, Some(is_current)).await
|
||||
feat::update_profile(uid, None, is_current, false).await
|
||||
})
|
||||
.await
|
||||
{
|
||||
@@ -509,7 +523,17 @@ impl Timer {
|
||||
}
|
||||
|
||||
// Emit completed event
|
||||
Self::emit_update_event(&uid, false);
|
||||
Self::emit_update_event(uid, false);
|
||||
}
|
||||
|
||||
async fn wait_until_sysopt(max_wait: Duration) {
|
||||
let _ = timeout(max_wait, async {
|
||||
while !Sysopt::global().is_initialed() {
|
||||
logging!(warn, Type::Timer, "Waiting for Sysopt to be initialized...");
|
||||
sleep(Duration::from_millis(30)).await;
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
49
src-tauri/src/core/tray/menu_def.rs
Normal file
49
src-tauri/src/core/tray/menu_def.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use crate::utils::i18n::t;
|
||||
use std::sync::Arc;
|
||||
|
||||
macro_rules! define_menu {
|
||||
($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => {
|
||||
#[derive(Debug)]
|
||||
pub struct MenuTexts {
|
||||
$(pub $field: Arc<str>,)+
|
||||
}
|
||||
|
||||
pub struct MenuIds;
|
||||
|
||||
impl MenuTexts {
|
||||
pub async fn new() -> Self {
|
||||
let ($($field,)+) = futures::join!($(t($text),)+);
|
||||
Self { $($field,)+ }
|
||||
}
|
||||
}
|
||||
|
||||
impl MenuIds {
|
||||
$(pub const $const_name: &'static str = $id;)+
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_menu! {
|
||||
dashboard => DASHBOARD, "tray_dashboard", "Dashboard",
|
||||
rule_mode => RULE_MODE, "tray_rule_mode", "Rule Mode",
|
||||
global_mode => GLOBAL_MODE, "tray_global_mode", "Global Mode",
|
||||
direct_mode => DIRECT_MODE, "tray_direct_mode", "Direct Mode",
|
||||
profiles => PROFILES, "tray_profiles", "Profiles",
|
||||
proxies => PROXIES, "tray_proxies", "Proxies",
|
||||
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "System Proxy",
|
||||
tun_mode => TUN_MODE, "tray_tun_mode", "TUN Mode",
|
||||
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "Close All Connections",
|
||||
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "LightWeight Mode",
|
||||
copy_env => COPY_ENV, "tray_copy_env", "Copy Env",
|
||||
conf_dir => CONF_DIR, "tray_conf_dir", "Conf Dir",
|
||||
core_dir => CORE_DIR, "tray_core_dir", "Core Dir",
|
||||
logs_dir => LOGS_DIR, "tray_logs_dir", "Logs Dir",
|
||||
open_dir => OPEN_DIR, "tray_open_dir", "Open Dir",
|
||||
app_log => APP_LOG, "tray_app_log", "Open App Log",
|
||||
core_log => CORE_LOG, "tray_core_log", "Open Core Log",
|
||||
restart_clash => RESTART_CLASH, "tray_restart_clash", "Restart Clash Core",
|
||||
restart_app => RESTART_APP, "tray_restart_app", "Restart App",
|
||||
verge_version => VERGE_VERSION, "tray_verge_version", "Verge Version",
|
||||
more => MORE, "tray_more", "More",
|
||||
exit => EXIT, "tray_exit", "Exit",
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use tauri::Emitter;
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri_plugin_mihomo::models::Proxies;
|
||||
use tokio::fs;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::config::PrfSelected;
|
||||
use crate::core::service;
|
||||
use crate::module::lightweight;
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
@@ -21,8 +25,8 @@ use futures::future::join_all;
|
||||
use parking_lot::Mutex;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fs,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -31,9 +35,13 @@ use tauri::{
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
||||
};
|
||||
mod menu_def;
|
||||
use menu_def::{MenuIds, MenuTexts};
|
||||
|
||||
// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑)
|
||||
|
||||
type ProxyMenuItem = (Option<Submenu<Wry>>, Vec<Box<dyn IsMenuItem<Wry>>>);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TrayState {}
|
||||
|
||||
@@ -54,8 +62,12 @@ fn should_handle_tray_click() -> bool {
|
||||
*last_click = now;
|
||||
true
|
||||
} else {
|
||||
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
|
||||
now.duration_since(*last_click).as_millis());
|
||||
logging!(
|
||||
debug,
|
||||
Type::Tray,
|
||||
"托盘点击被防抖机制忽略,距离上次点击 {}ms",
|
||||
now.duration_since(*last_click).as_millis()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -78,7 +90,7 @@ impl TrayState {
|
||||
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
|
||||
if is_common_tray_icon
|
||||
&& let Ok(Some(common_icon_path)) = find_target_icons("common")
|
||||
&& let Ok(icon_data) = fs::read(common_icon_path)
|
||||
&& let Ok(icon_data) = fs::read(common_icon_path).await
|
||||
{
|
||||
return (true, icon_data);
|
||||
}
|
||||
@@ -115,7 +127,7 @@ impl TrayState {
|
||||
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
|
||||
if is_sysproxy_tray_icon
|
||||
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
|
||||
&& let Ok(icon_data) = fs::read(sysproxy_icon_path)
|
||||
&& let Ok(icon_data) = fs::read(sysproxy_icon_path).await
|
||||
{
|
||||
return (true, icon_data);
|
||||
}
|
||||
@@ -152,7 +164,7 @@ impl TrayState {
|
||||
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
|
||||
if is_tun_tray_icon
|
||||
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun")
|
||||
&& let Ok(icon_data) = fs::read(tun_icon_path)
|
||||
&& let Ok(icon_data) = fs::read(tun_icon_path).await
|
||||
{
|
||||
return (true, icon_data);
|
||||
}
|
||||
@@ -199,7 +211,7 @@ singleton_lazy!(Tray, TRAY, Tray::default);
|
||||
impl Tray {
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘初始化");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘初始化");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -207,11 +219,15 @@ impl Tray {
|
||||
|
||||
match self.create_tray_from_handle(app_handle).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "System tray created successfully");
|
||||
logging!(info, Type::Tray, "System tray created successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
// Don't return error, let application continue running without tray
|
||||
log::warn!(target: "app", "System tray creation failed: {}, Application will continue running without tray icon", e);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Tray,
|
||||
"System tray creation failed: {e}, Application will continue running without tray icon",
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO: 初始化时,暂时使用此方法更新系统托盘菜单,有效避免代理节点菜单空白
|
||||
@@ -222,7 +238,7 @@ impl Tray {
|
||||
/// 更新托盘点击行为
|
||||
pub async fn update_click_behavior(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘点击行为更新");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘点击行为更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -242,7 +258,7 @@ impl Tray {
|
||||
/// 更新托盘菜单
|
||||
pub async fn update_menu(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘菜单更新");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘菜单更新");
|
||||
return Ok(());
|
||||
}
|
||||
// 调整最小更新间隔,确保状态及时刷新
|
||||
@@ -290,6 +306,8 @@ impl Tray {
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
let tun_mode_available = cmd::system::is_admin().unwrap_or_default()
|
||||
|| service::is_service_available().await.is_ok();
|
||||
let mode = {
|
||||
Config::clash()
|
||||
.await
|
||||
@@ -315,16 +333,21 @@ impl Tray {
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
tun_mode_available,
|
||||
profile_uid_and_name,
|
||||
is_lightweight_mode,
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
log::debug!(target: "app", "托盘菜单更新成功");
|
||||
logging!(debug, Type::Tray, "托盘菜单更新成功");
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Tray,
|
||||
"Failed to update tray menu: tray not found"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -334,7 +357,7 @@ impl Tray {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn update_icon(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -343,7 +366,11 @@ impl Tray {
|
||||
let tray = match app_handle.tray_by_id("main") {
|
||||
Some(tray) => tray,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Tray,
|
||||
"Failed to update tray icon: tray not found"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -373,7 +400,7 @@ impl Tray {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub async fn update_icon(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -382,7 +409,11 @@ impl Tray {
|
||||
let tray = match app_handle.tray_by_id("main") {
|
||||
Some(tray) => tray,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Tray,
|
||||
"Failed to update tray icon: tray not found"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -405,7 +436,7 @@ impl Tray {
|
||||
/// 更新托盘显示状态的函数
|
||||
pub async fn update_tray_display(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘显示状态更新");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘显示状态更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -423,7 +454,7 @@ impl Tray {
|
||||
/// 更新托盘提示
|
||||
pub async fn update_tooltip(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘提示更新");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘提示更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -479,7 +510,11 @@ impl Tray {
|
||||
if let Some(tray) = app_handle.tray_by_id("main") {
|
||||
let _ = tray.set_tooltip(Some(&tooltip));
|
||||
} else {
|
||||
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Tray,
|
||||
"Failed to update tray tooltip: tray not found"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -487,7 +522,7 @@ impl Tray {
|
||||
|
||||
pub async fn update_part(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘局部更新");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新");
|
||||
return Ok(());
|
||||
}
|
||||
// self.update_menu().await?;
|
||||
@@ -500,11 +535,11 @@ impl Tray {
|
||||
|
||||
pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘创建");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘创建");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!(target: "app", "正在从AppHandle创建系统托盘");
|
||||
logging!(info, Type::Tray, "正在从AppHandle创建系统托盘");
|
||||
|
||||
// 获取图标
|
||||
let icon_bytes = TrayState::get_common_tray_icon().await.1;
|
||||
@@ -550,7 +585,7 @@ impl Tray {
|
||||
AsyncHandler::spawn(|| async move {
|
||||
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
|
||||
log::debug!(target: "app", "tray event: {tray_event:?}");
|
||||
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
|
||||
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
@@ -585,14 +620,13 @@ impl Tray {
|
||||
});
|
||||
});
|
||||
tray.on_menu_event(on_menu_event);
|
||||
log::info!(target: "app", "系统托盘创建成功");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 托盘统一的状态更新函数
|
||||
pub async fn update_all_states(&self) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
log::debug!(target: "app", "应用正在退出,跳过托盘状态更新");
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘状态更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -606,70 +640,8 @@ impl Tray {
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_tray_menu(
|
||||
app_handle: &AppHandle,
|
||||
mode: Option<&str>,
|
||||
system_proxy_enabled: bool,
|
||||
tun_mode_enabled: bool,
|
||||
profile_uid_and_name: Vec<(String, String)>,
|
||||
is_lightweight_mode: bool,
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let mode = mode.unwrap_or("");
|
||||
|
||||
// 获取当前配置文件的选中代理组信息
|
||||
let current_profile_selected = {
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_ref = profiles_config.latest_ref();
|
||||
profiles_ref
|
||||
.get_current()
|
||||
.and_then(|uid| profiles_ref.get_item(&uid).ok())
|
||||
.and_then(|profile| profile.selected.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await;
|
||||
|
||||
let runtime_proxy_groups_order = cmd::get_runtime_config()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"Failed to fetch runtime proxy groups for tray menu: {e}"
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|config| {
|
||||
config
|
||||
.get("proxy-groups")
|
||||
.and_then(|groups| groups.as_sequence())
|
||||
.map(|groups| {
|
||||
groups
|
||||
.iter()
|
||||
.filter_map(|group| group.get("name"))
|
||||
.filter_map(|name| name.as_str())
|
||||
.map(|name| name.into())
|
||||
.collect::<Vec<String>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
let proxy_group_order_map = runtime_proxy_groups_order.as_ref().map(|group_names| {
|
||||
group_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, name)| (name.clone(), index))
|
||||
.collect::<HashMap<String, usize>>()
|
||||
});
|
||||
|
||||
let verge_settings = Config::verge().await.latest_ref().clone();
|
||||
let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false);
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let hotkeys = verge_settings
|
||||
.hotkeys
|
||||
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
|
||||
hotkeys
|
||||
.as_ref()
|
||||
.map(|h| {
|
||||
h.iter()
|
||||
@@ -689,35 +661,43 @@ async fn create_tray_menu(
|
||||
})
|
||||
.collect::<std::collections::HashMap<String, String>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = {
|
||||
let futures = profile_uid_and_name
|
||||
.iter()
|
||||
.map(|(profile_uid, profile_name)| {
|
||||
let app_handle = app_handle.clone();
|
||||
let profile_uid = profile_uid.clone();
|
||||
let profile_name = profile_name.clone();
|
||||
async move {
|
||||
let is_current_profile = Config::profiles()
|
||||
.await
|
||||
.data_mut()
|
||||
.is_current_profile_index(profile_uid.clone());
|
||||
CheckMenuItem::with_id(
|
||||
&app_handle,
|
||||
format!("profiles_{profile_uid}"),
|
||||
t(&profile_name).await,
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
)
|
||||
}
|
||||
});
|
||||
let results = join_all(futures).await;
|
||||
results.into_iter().collect::<Result<Vec<_>, _>>()?
|
||||
};
|
||||
async fn create_profile_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
profile_uid_and_name: Vec<(String, String)>,
|
||||
) -> Result<Vec<CheckMenuItem<Wry>>> {
|
||||
let futures = profile_uid_and_name
|
||||
.iter()
|
||||
.map(|(profile_uid, profile_name)| {
|
||||
let app_handle = app_handle.clone();
|
||||
async move {
|
||||
let is_current_profile = Config::profiles()
|
||||
.await
|
||||
.latest_ref()
|
||||
.is_current_profile_index(profile_uid);
|
||||
CheckMenuItem::with_id(
|
||||
&app_handle,
|
||||
format!("profiles_{profile_uid}"),
|
||||
t(profile_name).await,
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
)
|
||||
}
|
||||
});
|
||||
let results = join_all(futures).await;
|
||||
Ok(results.into_iter().collect::<Result<Vec<_>, _>>()?)
|
||||
}
|
||||
|
||||
// 代理组子菜单
|
||||
fn create_subcreate_proxy_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
proxy_mode: &str,
|
||||
current_profile_selected: &[PrfSelected],
|
||||
proxy_group_order_map: Option<HashMap<String, usize>>,
|
||||
proxy_nodes_data: Result<Proxies>,
|
||||
) -> Result<Vec<Submenu<Wry>>> {
|
||||
let proxy_submenus: Vec<Submenu<Wry>> = {
|
||||
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
|
||||
|
||||
@@ -725,7 +705,7 @@ async fn create_tray_menu(
|
||||
if let Ok(proxy_nodes_data) = proxy_nodes_data {
|
||||
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
|
||||
// Filter groups based on mode
|
||||
let should_show = match mode {
|
||||
let should_show = match proxy_mode {
|
||||
"global" => group_name == "GLOBAL",
|
||||
_ => group_name != "GLOBAL",
|
||||
} &&
|
||||
@@ -771,7 +751,9 @@ async fn create_tray_menu(
|
||||
is_selected,
|
||||
None::<&str>,
|
||||
)
|
||||
.map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e))
|
||||
.map_err(|e| {
|
||||
logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
@@ -781,7 +763,7 @@ async fn create_tray_menu(
|
||||
}
|
||||
|
||||
// Determine if group is active
|
||||
let is_group_active = match mode {
|
||||
let is_group_active = match proxy_mode {
|
||||
"global" => group_name == "GLOBAL" && !now_proxy.is_empty(),
|
||||
"direct" => false,
|
||||
_ => {
|
||||
@@ -813,7 +795,12 @@ async fn create_tray_menu(
|
||||
let insertion_index = submenus.len();
|
||||
submenus.push((group_name.into(), insertion_index, submenu));
|
||||
} else {
|
||||
log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Tray,
|
||||
"Failed to create proxy group submenu: {}",
|
||||
group_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -837,29 +824,118 @@ async fn create_tray_menu(
|
||||
.map(|(_, _, submenu)| submenu)
|
||||
.collect()
|
||||
};
|
||||
Ok(proxy_submenus)
|
||||
}
|
||||
|
||||
fn create_proxy_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
show_proxy_groups_inline: bool,
|
||||
proxy_submenus: Vec<Submenu<Wry>>,
|
||||
proxies_text: &Arc<str>,
|
||||
) -> Result<ProxyMenuItem> {
|
||||
// 创建代理主菜单
|
||||
let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline {
|
||||
(
|
||||
None,
|
||||
proxy_submenus
|
||||
.into_iter()
|
||||
.map(|submenu| Box::new(submenu) as Box<dyn IsMenuItem<Wry>>)
|
||||
.collect(),
|
||||
)
|
||||
} else if !proxy_submenus.is_empty() {
|
||||
let proxy_submenu_refs: Vec<&dyn IsMenuItem<Wry>> = proxy_submenus
|
||||
.iter()
|
||||
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
|
||||
(
|
||||
Some(Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
MenuIds::PROXIES,
|
||||
proxies_text,
|
||||
true,
|
||||
&proxy_submenu_refs,
|
||||
)?),
|
||||
Vec::new(),
|
||||
)
|
||||
} else {
|
||||
(None, Vec::new())
|
||||
};
|
||||
Ok((proxies_submenu, inline_proxy_items))
|
||||
}
|
||||
|
||||
async fn create_tray_menu(
|
||||
app_handle: &AppHandle,
|
||||
mode: Option<&str>,
|
||||
system_proxy_enabled: bool,
|
||||
tun_mode_enabled: bool,
|
||||
tun_mode_available: bool,
|
||||
profile_uid_and_name: Vec<(String, String)>,
|
||||
is_lightweight_mode: bool,
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
|
||||
// 获取当前配置文件的选中代理组信息
|
||||
let current_profile_selected = {
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_ref = profiles_config.latest_ref();
|
||||
profiles_ref
|
||||
.get_current()
|
||||
.and_then(|uid| profiles_ref.get_item(&uid).ok())
|
||||
.and_then(|profile| profile.selected.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await;
|
||||
|
||||
let runtime_proxy_groups_order = cmd::get_runtime_config()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"Failed to fetch runtime proxy groups for tray menu: {e}"
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|config| {
|
||||
config
|
||||
.get("proxy-groups")
|
||||
.and_then(|groups| groups.as_sequence())
|
||||
.map(|groups| {
|
||||
groups
|
||||
.iter()
|
||||
.filter_map(|group| group.get("name"))
|
||||
.filter_map(|name| name.as_str())
|
||||
.map(|name| name.into())
|
||||
.collect::<Vec<String>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
let proxy_group_order_map: Option<
|
||||
HashMap<smartstring::SmartString<smartstring::LazyCompact>, usize>,
|
||||
> = runtime_proxy_groups_order.as_ref().map(|group_names| {
|
||||
group_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, name)| (name.clone(), index))
|
||||
.collect::<HashMap<String, usize>>()
|
||||
});
|
||||
|
||||
let verge_settings = Config::verge().await.latest_ref().clone();
|
||||
let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false);
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let hotkeys = create_hotkeys(&verge_settings.hotkeys);
|
||||
|
||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> =
|
||||
create_profile_menu_item(app_handle, profile_uid_and_name).await?;
|
||||
|
||||
// Pre-fetch all localized strings
|
||||
let dashboard_text = t("Dashboard").await;
|
||||
let rule_mode_text = t("Rule Mode").await;
|
||||
let global_mode_text = t("Global Mode").await;
|
||||
let direct_mode_text = t("Direct Mode").await;
|
||||
let profiles_text = t("Profiles").await;
|
||||
let proxies_text = t("Proxies").await;
|
||||
let system_proxy_text = t("System Proxy").await;
|
||||
let tun_mode_text = t("TUN Mode").await;
|
||||
let close_all_connections_text = t("Close All Connections").await;
|
||||
let lightweight_mode_text = t("LightWeight Mode").await;
|
||||
let copy_env_text = t("Copy Env").await;
|
||||
let conf_dir_text = t("Conf Dir").await;
|
||||
let core_dir_text = t("Core Dir").await;
|
||||
let logs_dir_text = t("Logs Dir").await;
|
||||
let open_dir_text = t("Open Dir").await;
|
||||
let restart_clash_text = t("Restart Clash Core").await;
|
||||
let restart_app_text = t("Restart App").await;
|
||||
let verge_version_text = t("Verge Version").await;
|
||||
let more_text = t("More").await;
|
||||
let exit_text = t("Exit").await;
|
||||
|
||||
let texts = &MenuTexts::new().await;
|
||||
// Convert to references only when needed
|
||||
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
.iter()
|
||||
@@ -868,81 +944,66 @@ async fn create_tray_menu(
|
||||
|
||||
let open_window = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_window",
|
||||
dashboard_text,
|
||||
MenuIds::DASHBOARD,
|
||||
&texts.dashboard,
|
||||
true,
|
||||
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
|
||||
)?;
|
||||
|
||||
let rule_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"rule_mode",
|
||||
rule_mode_text,
|
||||
MenuIds::RULE_MODE,
|
||||
&texts.rule_mode,
|
||||
true,
|
||||
mode == "rule",
|
||||
current_proxy_mode == "rule",
|
||||
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
|
||||
)?;
|
||||
|
||||
let global_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"global_mode",
|
||||
global_mode_text,
|
||||
MenuIds::GLOBAL_MODE,
|
||||
&texts.global_mode,
|
||||
true,
|
||||
mode == "global",
|
||||
current_proxy_mode == "global",
|
||||
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
|
||||
)?;
|
||||
|
||||
let direct_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"direct_mode",
|
||||
direct_mode_text,
|
||||
MenuIds::DIRECT_MODE,
|
||||
&texts.direct_mode,
|
||||
true,
|
||||
mode == "direct",
|
||||
current_proxy_mode == "direct",
|
||||
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
|
||||
)?;
|
||||
|
||||
let profiles = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"profiles",
|
||||
profiles_text,
|
||||
MenuIds::PROFILES,
|
||||
&texts.profiles,
|
||||
true,
|
||||
&profile_menu_items_refs,
|
||||
)?;
|
||||
|
||||
// 创建代理主菜单
|
||||
let (proxies_submenu, inline_proxy_items): (Option<Submenu<Wry>>, Vec<&dyn IsMenuItem<Wry>>) =
|
||||
if show_proxy_groups_inline {
|
||||
(
|
||||
None,
|
||||
proxy_submenus
|
||||
.iter()
|
||||
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
|
||||
.collect(),
|
||||
)
|
||||
} else if !proxy_submenus.is_empty() {
|
||||
let proxy_submenu_refs: Vec<&dyn IsMenuItem<Wry>> = proxy_submenus
|
||||
.iter()
|
||||
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
let proxy_sub_menus = create_subcreate_proxy_menu_item(
|
||||
app_handle,
|
||||
current_proxy_mode,
|
||||
¤t_profile_selected,
|
||||
proxy_group_order_map,
|
||||
proxy_nodes_data.map_err(anyhow::Error::from),
|
||||
)?;
|
||||
|
||||
(
|
||||
Some(Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"proxies",
|
||||
proxies_text,
|
||||
true,
|
||||
&proxy_submenu_refs,
|
||||
)?),
|
||||
Vec::new(),
|
||||
)
|
||||
} else {
|
||||
(None, Vec::new())
|
||||
};
|
||||
let (proxies_menu, inline_proxy_items) = create_proxy_menu_item(
|
||||
app_handle,
|
||||
show_proxy_groups_inline,
|
||||
proxy_sub_menus,
|
||||
&texts.proxies,
|
||||
)?;
|
||||
|
||||
let system_proxy = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"system_proxy",
|
||||
system_proxy_text,
|
||||
MenuIds::SYSTEM_PROXY,
|
||||
&texts.system_proxy,
|
||||
true,
|
||||
system_proxy_enabled,
|
||||
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
|
||||
@@ -950,92 +1011,120 @@ async fn create_tray_menu(
|
||||
|
||||
let tun_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"tun_mode",
|
||||
tun_mode_text,
|
||||
true,
|
||||
MenuIds::TUN_MODE,
|
||||
&texts.tun_mode,
|
||||
tun_mode_available,
|
||||
tun_mode_enabled,
|
||||
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
|
||||
)?;
|
||||
|
||||
let close_all_connections = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"close_all_connections",
|
||||
close_all_connections_text,
|
||||
MenuIds::CLOSE_ALL_CONNECTIONS,
|
||||
&texts.close_all_connections,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let lighteweight_mode = &CheckMenuItem::with_id(
|
||||
let lightweight_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"entry_lightweight_mode",
|
||||
lightweight_mode_text,
|
||||
MenuIds::LIGHTWEIGHT_MODE,
|
||||
&texts.lightweight_mode,
|
||||
true,
|
||||
is_lightweight_mode,
|
||||
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
|
||||
)?;
|
||||
|
||||
let copy_env = &MenuItem::with_id(app_handle, "copy_env", copy_env_text, true, None::<&str>)?;
|
||||
let copy_env = &MenuItem::with_id(
|
||||
app_handle,
|
||||
MenuIds::COPY_ENV,
|
||||
&texts.copy_env,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let open_app_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_app_dir",
|
||||
conf_dir_text,
|
||||
MenuIds::CONF_DIR,
|
||||
&texts.conf_dir,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let open_core_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_core_dir",
|
||||
core_dir_text,
|
||||
MenuIds::CORE_DIR,
|
||||
&texts.core_dir,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let open_logs_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_logs_dir",
|
||||
logs_dir_text,
|
||||
MenuIds::LOGS_DIR,
|
||||
&texts.logs_dir,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let open_app_log = &MenuItem::with_id(
|
||||
app_handle,
|
||||
MenuIds::APP_LOG,
|
||||
&texts.app_log,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let open_core_log = &MenuItem::with_id(
|
||||
app_handle,
|
||||
MenuIds::CORE_LOG,
|
||||
&texts.core_log,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let open_dir = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"open_dir",
|
||||
open_dir_text,
|
||||
MenuIds::OPEN_DIR,
|
||||
&texts.open_dir,
|
||||
true,
|
||||
&[open_app_dir, open_core_dir, open_logs_dir],
|
||||
&[
|
||||
open_app_dir,
|
||||
open_core_dir,
|
||||
open_logs_dir,
|
||||
open_app_log,
|
||||
open_core_log,
|
||||
],
|
||||
)?;
|
||||
|
||||
let restart_clash = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_clash",
|
||||
restart_clash_text,
|
||||
MenuIds::RESTART_CLASH,
|
||||
&texts.restart_clash,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let restart_app = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_app",
|
||||
restart_app_text,
|
||||
MenuIds::RESTART_APP,
|
||||
&texts.restart_app,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let app_version = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"app_version",
|
||||
format!("{} {version}", verge_version_text),
|
||||
MenuIds::VERGE_VERSION,
|
||||
format!("{} {version}", &texts.verge_version),
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let more = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"more",
|
||||
more_text,
|
||||
MenuIds::MORE,
|
||||
&texts.more,
|
||||
true,
|
||||
&[
|
||||
close_all_connections,
|
||||
@@ -1045,7 +1134,13 @@ async fn create_tray_menu(
|
||||
],
|
||||
)?;
|
||||
|
||||
let quit = &MenuItem::with_id(app_handle, "quit", exit_text, true, Some("CmdOrControl+Q"))?;
|
||||
let quit = &MenuItem::with_id(
|
||||
app_handle,
|
||||
MenuIds::EXIT,
|
||||
&texts.exit,
|
||||
true,
|
||||
Some("CmdOrControl+Q"),
|
||||
)?;
|
||||
|
||||
let separator = &PredefinedMenuItem::separator(app_handle)?;
|
||||
|
||||
@@ -1063,9 +1158,9 @@ async fn create_tray_menu(
|
||||
// 如果有代理节点,添加代理节点菜单
|
||||
if show_proxy_groups_inline {
|
||||
if !inline_proxy_items.is_empty() {
|
||||
menu_items.extend_from_slice(&inline_proxy_items);
|
||||
menu_items.extend(inline_proxy_items.iter().map(|item| item.as_ref()));
|
||||
}
|
||||
} else if let Some(ref proxies_menu) = proxies_submenu {
|
||||
} else if let Some(ref proxies_menu) = proxies_menu {
|
||||
menu_items.push(proxies_menu);
|
||||
}
|
||||
|
||||
@@ -1074,7 +1169,7 @@ async fn create_tray_menu(
|
||||
system_proxy as &dyn IsMenuItem<Wry>,
|
||||
tun_mode as &dyn IsMenuItem<Wry>,
|
||||
separator,
|
||||
lighteweight_mode as &dyn IsMenuItem<Wry>,
|
||||
lightweight_mode as &dyn IsMenuItem<Wry>,
|
||||
copy_env as &dyn IsMenuItem<Wry>,
|
||||
open_dir as &dyn IsMenuItem<Wry>,
|
||||
more as &dyn IsMenuItem<Wry>,
|
||||
@@ -1091,13 +1186,14 @@ async fn create_tray_menu(
|
||||
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
AsyncHandler::spawn(|| async move {
|
||||
match event.id.as_ref() {
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5]; // Removing the "_mode" suffix
|
||||
mode @ (MenuIds::RULE_MODE | MenuIds::GLOBAL_MODE | MenuIds::DIRECT_MODE) => {
|
||||
// Removing the the "tray_" preffix and "_mode" suffix
|
||||
let mode = &mode[5..mode.len() - 5];
|
||||
logging!(info, Type::ProxyMode, "Switch Proxy Mode To: {}", mode);
|
||||
feat::change_clash_mode(mode.into()).await;
|
||||
}
|
||||
"open_window" => {
|
||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
||||
MenuIds::DASHBOARD => {
|
||||
logging!(info, Type::Tray, "托盘菜单点击: 打开窗口");
|
||||
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
@@ -1106,40 +1202,51 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
WindowManager::show_main_window().await;
|
||||
};
|
||||
}
|
||||
"system_proxy" => {
|
||||
MenuIds::SYSTEM_PROXY => {
|
||||
feat::toggle_system_proxy().await;
|
||||
}
|
||||
"tun_mode" => {
|
||||
MenuIds::TUN_MODE => {
|
||||
feat::toggle_tun_mode(None).await;
|
||||
}
|
||||
"close_all_connections" => {
|
||||
MenuIds::CLOSE_ALL_CONNECTIONS => {
|
||||
if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await {
|
||||
log::error!(target: "app", "Failed to close all connections from tray: {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Tray,
|
||||
"Failed to close all connections from tray: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
"copy_env" => feat::copy_clash_env().await,
|
||||
"open_app_dir" => {
|
||||
MenuIds::COPY_ENV => feat::copy_clash_env().await,
|
||||
MenuIds::CONF_DIR => {
|
||||
println!("Open directory submenu clicked");
|
||||
let _ = cmd::open_app_dir().await;
|
||||
}
|
||||
"open_core_dir" => {
|
||||
MenuIds::CORE_DIR => {
|
||||
let _ = cmd::open_core_dir().await;
|
||||
}
|
||||
"open_logs_dir" => {
|
||||
MenuIds::LOGS_DIR => {
|
||||
let _ = cmd::open_logs_dir().await;
|
||||
}
|
||||
"restart_clash" => feat::restart_clash_core().await,
|
||||
"restart_app" => feat::restart_app().await,
|
||||
"entry_lightweight_mode" => {
|
||||
MenuIds::APP_LOG => {
|
||||
let _ = cmd::open_app_log().await;
|
||||
}
|
||||
MenuIds::CORE_LOG => {
|
||||
let _ = cmd::open_core_log().await;
|
||||
}
|
||||
MenuIds::RESTART_CLASH => feat::restart_clash_core().await,
|
||||
MenuIds::RESTART_APP => feat::restart_app().await,
|
||||
MenuIds::LIGHTWEIGHT_MODE => {
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
if !is_in_lightweight_mode() {
|
||||
lightweight::entry_lightweight_mode().await; // Await async function
|
||||
lightweight::entry_lightweight_mode().await;
|
||||
} else {
|
||||
lightweight::exit_lightweight_mode().await; // Await async function
|
||||
lightweight::exit_lightweight_mode().await;
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
MenuIds::EXIT => {
|
||||
feat::quit().await;
|
||||
}
|
||||
id if id.starts_with("profiles_") => {
|
||||
@@ -1160,12 +1267,25 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name);
|
||||
logging!(
|
||||
info,
|
||||
Type::Tray,
|
||||
"切换代理成功: {} -> {}",
|
||||
group_name,
|
||||
proxy_name
|
||||
);
|
||||
let _ = handle::Handle::app_handle()
|
||||
.emit("verge://refresh-proxy-config", ());
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Tray,
|
||||
"切换代理失败: {} -> {}, 错误: {:?}",
|
||||
group_name,
|
||||
proxy_name,
|
||||
e
|
||||
);
|
||||
|
||||
// Fallback to IPC update
|
||||
if (handle::Handle::mihomo()
|
||||
@@ -1174,7 +1294,13 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
.await)
|
||||
.is_ok()
|
||||
{
|
||||
log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name);
|
||||
logging!(
|
||||
info,
|
||||
Type::Tray,
|
||||
"代理切换回退成功: {} -> {}",
|
||||
group_name,
|
||||
proxy_name
|
||||
);
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let _ = app_handle.emit("verge://force-refresh-proxies", ());
|
||||
@@ -1188,7 +1314,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
|
||||
// Ensure tray state update is awaited and properly handled
|
||||
if let Err(e) = Tray::global().update_all_states().await {
|
||||
log::warn!(target: "app", "更新托盘状态失败: {e}");
|
||||
logging!(warn, Type::Tray, "Failed to update tray state: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::config::{Config, ConfigType};
|
||||
use crate::core::handle;
|
||||
@@ -11,44 +11,38 @@ use crate::singleton_lazy;
|
||||
use crate::utils::dirs;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
// pub enum ValidationResult {
|
||||
// Valid,
|
||||
// Invalid(String),
|
||||
// }
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
pub enum ValidationProcessStatus {
|
||||
Ongoing,
|
||||
Completed,
|
||||
}
|
||||
|
||||
pub struct CoreConfigValidator {
|
||||
// inner: Vec<String>,
|
||||
// result: ValidationResult,
|
||||
process_status: Arc<Mutex<ValidationProcessStatus>>,
|
||||
is_processing: AtomicBool,
|
||||
}
|
||||
|
||||
impl CoreConfigValidator {
|
||||
pub fn new() -> Self {
|
||||
CoreConfigValidator {
|
||||
process_status: Arc::new(Mutex::new(ValidationProcessStatus::Completed)),
|
||||
Self {
|
||||
is_processing: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_start(&self) -> bool {
|
||||
!self.is_processing.swap(true, Ordering::AcqRel)
|
||||
}
|
||||
|
||||
pub fn finish(&self) {
|
||||
self.is_processing.store(false, Ordering::Release)
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreConfigValidator {
|
||||
/// 检查文件是否为脚本文件
|
||||
fn is_script_file<P>(path: P) -> Result<bool>
|
||||
where
|
||||
P: AsRef<Path> + std::fmt::Display,
|
||||
{
|
||||
async fn is_script_file(path: &str) -> Result<bool> {
|
||||
// 1. 先通过扩展名快速判断
|
||||
if has_ext(&path, "yaml") || has_ext(&path, "yml") {
|
||||
if has_ext(path, "yaml") || has_ext(path, "yml") {
|
||||
return Ok(false); // YAML文件不是脚本文件
|
||||
} else if has_ext(&path, "js") {
|
||||
} else if has_ext(path, "js") {
|
||||
return Ok(true); // JS文件是脚本文件
|
||||
}
|
||||
|
||||
// 2. 读取文件内容
|
||||
let content = match std::fs::read_to_string(&path) {
|
||||
let content = match fs::read_to_string(path).await {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
logging!(
|
||||
@@ -118,11 +112,11 @@ impl CoreConfigValidator {
|
||||
}
|
||||
|
||||
/// 只进行文件语法检查,不进行完整验证
|
||||
fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> {
|
||||
async fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> {
|
||||
logging!(info, Type::Validate, "开始检查文件: {}", config_path);
|
||||
|
||||
// 读取文件内容
|
||||
let content = match std::fs::read_to_string(config_path) {
|
||||
let content = match fs::read_to_string(config_path).await {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read file: {err}").into();
|
||||
@@ -147,9 +141,9 @@ impl CoreConfigValidator {
|
||||
}
|
||||
|
||||
/// 验证脚本文件语法
|
||||
fn validate_script_file(path: &str) -> Result<(bool, String)> {
|
||||
async fn validate_script_file(path: &str) -> Result<(bool, String)> {
|
||||
// 读取脚本内容
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
let content = match fs::read_to_string(path).await {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read script file: {err}").into();
|
||||
@@ -219,14 +213,14 @@ impl CoreConfigValidator {
|
||||
"检测到Merge文件,仅进行语法检查: {}",
|
||||
config_path
|
||||
);
|
||||
return Self::validate_file_syntax(config_path);
|
||||
return Self::validate_file_syntax(config_path).await;
|
||||
}
|
||||
|
||||
// 检查是否为脚本文件
|
||||
let is_script = if config_path.ends_with(".js") {
|
||||
true
|
||||
} else {
|
||||
match Self::is_script_file(config_path) {
|
||||
match Self::is_script_file(config_path).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
// 如果无法确定文件类型,尝试使用Clash内核验证
|
||||
@@ -249,7 +243,7 @@ impl CoreConfigValidator {
|
||||
"检测到脚本文件,使用JavaScript验证: {}",
|
||||
config_path
|
||||
);
|
||||
return Self::validate_script_file(config_path);
|
||||
return Self::validate_script_file(config_path).await;
|
||||
}
|
||||
|
||||
// 对YAML配置文件使用Clash内核验证
|
||||
@@ -325,22 +319,18 @@ impl CoreConfigValidator {
|
||||
|
||||
/// 验证运行时配置
|
||||
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||
if *self.process_status.lock().await == ValidationProcessStatus::Ongoing {
|
||||
if !self.try_start() {
|
||||
logging!(info, Type::Validate, "验证已在进行中,跳过新的验证请求");
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
*self.process_status.lock().await = ValidationProcessStatus::Ongoing;
|
||||
defer! {
|
||||
self.finish();
|
||||
}
|
||||
logging!(info, Type::Validate, "生成临时配置文件用于验证");
|
||||
|
||||
let result = async {
|
||||
let config_path = Config::generate_file(ConfigType::Check).await?;
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
Self::validate_config_internal(config_path).await
|
||||
}
|
||||
.await;
|
||||
|
||||
*self.process_status.lock().await = ValidationProcessStatus::Completed;
|
||||
result
|
||||
let config_path = Config::generate_file(ConfigType::Check).await?;
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
Self::validate_config_internal(config_path).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::fs;
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainItem {
|
||||
@@ -83,7 +83,7 @@ impl AsyncChainItemFrom for Option<ChainItem> {
|
||||
match itype {
|
||||
"script" => Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Script(fs::read_to_string(path).ok()?.into()),
|
||||
data: ChainType::Script(fs::read_to_string(path).await.ok()?.into()),
|
||||
}),
|
||||
"merge" => Some(ChainItem {
|
||||
uid,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::use_lowercase;
|
||||
use serde_yaml_ng::{self, Mapping, Value};
|
||||
|
||||
@@ -19,7 +21,11 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
|
||||
deep_merge(&mut config, &Value::from(merge));
|
||||
|
||||
config.as_mapping().cloned().unwrap_or_else(|| {
|
||||
log::error!("Failed to convert merged config to mapping, using empty mapping");
|
||||
logging!(
|
||||
error,
|
||||
Type::Core,
|
||||
"Failed to convert merged config to mapping, using empty mapping"
|
||||
);
|
||||
Mapping::new()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,17 +6,45 @@ pub mod seq;
|
||||
mod tun;
|
||||
|
||||
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
|
||||
use crate::constants;
|
||||
use crate::utils::dirs;
|
||||
use crate::{config::Config, utils::tmpl};
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tokio::fs;
|
||||
|
||||
type ResultLog = Vec<(String, String)>;
|
||||
#[derive(Debug)]
|
||||
struct ConfigValues {
|
||||
clash_config: Mapping,
|
||||
clash_core: Option<String>,
|
||||
enable_tun: bool,
|
||||
enable_builtin: bool,
|
||||
socks_enabled: bool,
|
||||
http_enabled: bool,
|
||||
enable_dns_settings: bool,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
redir_enabled: bool,
|
||||
#[cfg(target_os = "linux")]
|
||||
tproxy_enabled: bool,
|
||||
}
|
||||
|
||||
/// Enhance mode
|
||||
/// 返回最终订阅、该订阅包含的键、和script执行的结果
|
||||
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// config.yaml 的订阅
|
||||
#[derive(Debug)]
|
||||
struct ProfileItems {
|
||||
config: Mapping,
|
||||
merge_item: ChainItem,
|
||||
script_item: ChainItem,
|
||||
rules_item: ChainItem,
|
||||
proxies_item: ChainItem,
|
||||
groups_item: ChainItem,
|
||||
global_merge: ChainItem,
|
||||
global_script: ChainItem,
|
||||
profile_name: String,
|
||||
}
|
||||
|
||||
async fn get_config_values() -> ConfigValues {
|
||||
let clash_config = { Config::clash().await.latest_ref().0.clone() };
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
|
||||
@@ -31,12 +59,14 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
verge.enable_dns_settings.unwrap_or(false),
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_enabled = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
verge.verge_redir_enabled.unwrap_or(false)
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_enabled = {
|
||||
let verge = Config::verge().await;
|
||||
@@ -44,9 +74,189 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
verge.verge_tproxy_enabled.unwrap_or(false)
|
||||
};
|
||||
|
||||
ConfigValues {
|
||||
clash_config,
|
||||
clash_core,
|
||||
enable_tun,
|
||||
enable_builtin,
|
||||
socks_enabled,
|
||||
http_enabled,
|
||||
enable_dns_settings,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
redir_enabled,
|
||||
#[cfg(target_os = "linux")]
|
||||
tproxy_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_profile_items() -> ProfileItems {
|
||||
// 从profiles里拿东西 - 先收集需要的数据,然后释放锁
|
||||
let (
|
||||
mut config,
|
||||
current,
|
||||
merge_uid,
|
||||
script_uid,
|
||||
rules_uid,
|
||||
proxies_uid,
|
||||
groups_uid,
|
||||
_current_profile_uid,
|
||||
name,
|
||||
) = {
|
||||
let current = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_clone = profiles.latest_ref().clone();
|
||||
profiles_clone.current_mapping().await.unwrap_or_default()
|
||||
};
|
||||
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_ref();
|
||||
|
||||
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
|
||||
let script_uid = profiles_ref.current_script().unwrap_or_default();
|
||||
let rules_uid = profiles_ref.current_rules().unwrap_or_default();
|
||||
let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
|
||||
let groups_uid = profiles_ref.current_groups().unwrap_or_default();
|
||||
let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
|
||||
|
||||
let name = profiles_ref
|
||||
.get_item(¤t_profile_uid)
|
||||
.ok()
|
||||
.and_then(|item| item.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
(
|
||||
current,
|
||||
merge_uid,
|
||||
script_uid,
|
||||
rules_uid,
|
||||
proxies_uid,
|
||||
groups_uid,
|
||||
current_profile_uid,
|
||||
name,
|
||||
)
|
||||
};
|
||||
|
||||
// 现在获取具体的items,此时profiles锁已经释放
|
||||
let merge_item = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(merge_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
});
|
||||
|
||||
let script_item = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(script_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
});
|
||||
|
||||
let rules_item = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(rules_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Rules(SeqMap::default()),
|
||||
});
|
||||
|
||||
let proxies_item = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(proxies_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Proxies(SeqMap::default()),
|
||||
});
|
||||
|
||||
let groups_item = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(groups_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Groups(SeqMap::default()),
|
||||
});
|
||||
|
||||
let global_merge = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item("Merge").ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "Merge".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
});
|
||||
|
||||
let global_script = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item("Script").ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "Script".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
});
|
||||
|
||||
ProfileItems {
|
||||
config: current,
|
||||
merge_item,
|
||||
script_item,
|
||||
rules_item,
|
||||
@@ -54,192 +264,19 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
groups_item,
|
||||
global_merge,
|
||||
global_script,
|
||||
profile_name,
|
||||
) = {
|
||||
// 收集所有需要的数据,然后释放profiles锁
|
||||
let (
|
||||
current,
|
||||
merge_uid,
|
||||
script_uid,
|
||||
rules_uid,
|
||||
proxies_uid,
|
||||
groups_uid,
|
||||
_current_profile_uid,
|
||||
name,
|
||||
) = {
|
||||
// 分离async调用和数据获取,避免借用检查问题
|
||||
let current = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_clone = profiles.latest_ref().clone();
|
||||
profiles_clone.current_mapping().await.unwrap_or_default()
|
||||
};
|
||||
profile_name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// 重新获取锁进行其他操作
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_ref();
|
||||
fn process_global_items(
|
||||
mut config: Mapping,
|
||||
global_merge: ChainItem,
|
||||
global_script: ChainItem,
|
||||
profile_name: String,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
let mut result_map = HashMap::new();
|
||||
let mut exists_keys = use_keys(&config);
|
||||
|
||||
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
|
||||
let script_uid = profiles_ref.current_script().unwrap_or_default();
|
||||
let rules_uid = profiles_ref.current_rules().unwrap_or_default();
|
||||
let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
|
||||
let groups_uid = profiles_ref.current_groups().unwrap_or_default();
|
||||
let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
|
||||
|
||||
let name = profiles_ref
|
||||
.get_item(¤t_profile_uid)
|
||||
.ok()
|
||||
.and_then(|item| item.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
(
|
||||
current,
|
||||
merge_uid,
|
||||
script_uid,
|
||||
rules_uid,
|
||||
proxies_uid,
|
||||
groups_uid,
|
||||
current_profile_uid,
|
||||
name,
|
||||
)
|
||||
};
|
||||
|
||||
// 现在获取具体的items,此时profiles锁已经释放
|
||||
let merge = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&merge_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
});
|
||||
|
||||
let script = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&script_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
});
|
||||
|
||||
let rules = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&rules_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Rules(SeqMap::default()),
|
||||
});
|
||||
|
||||
let proxies = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&proxies_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Proxies(SeqMap::default()),
|
||||
});
|
||||
|
||||
let groups = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&groups_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Groups(SeqMap::default()),
|
||||
});
|
||||
|
||||
let global_merge = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&"Merge".into()).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "Merge".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
});
|
||||
|
||||
let global_script = {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&"Script".into()).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "Script".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
});
|
||||
|
||||
(
|
||||
current,
|
||||
merge,
|
||||
script,
|
||||
rules,
|
||||
proxies,
|
||||
groups,
|
||||
global_merge,
|
||||
global_script,
|
||||
name,
|
||||
)
|
||||
};
|
||||
|
||||
let mut result_map = HashMap::new(); // 保存脚本日志
|
||||
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
||||
|
||||
// 全局Merge和Script
|
||||
if let ChainType::Merge(merge) = global_merge.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
config = use_merge(merge, config.to_owned());
|
||||
@@ -247,7 +284,6 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
|
||||
if let ChainType::Script(script) = global_script.data {
|
||||
let mut logs = vec![];
|
||||
|
||||
match use_script(script, config.to_owned(), profile_name.to_owned()) {
|
||||
Ok((res_config, res_logs)) => {
|
||||
exists_keys.extend(use_keys(&res_config));
|
||||
@@ -256,11 +292,24 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
}
|
||||
Err(err) => logs.push(("exception".into(), err.to_string().into())),
|
||||
}
|
||||
|
||||
result_map.insert(global_script.uid, logs);
|
||||
}
|
||||
|
||||
// 订阅关联的Merge、Script、Rules、Proxies、Groups
|
||||
(config, exists_keys, result_map)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn process_profile_items(
|
||||
mut config: Mapping,
|
||||
mut exists_keys: Vec<String>,
|
||||
mut result_map: HashMap<String, ResultLog>,
|
||||
rules_item: ChainItem,
|
||||
proxies_item: ChainItem,
|
||||
groups_item: ChainItem,
|
||||
merge_item: ChainItem,
|
||||
script_item: ChainItem,
|
||||
profile_name: String,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Rules(rules) = rules_item.data {
|
||||
config = use_seq(rules, config.to_owned(), "rules");
|
||||
}
|
||||
@@ -280,7 +329,6 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
|
||||
if let ChainType::Script(script) = script_item.data {
|
||||
let mut logs = vec![];
|
||||
|
||||
match use_script(script, config.to_owned(), profile_name) {
|
||||
Ok((res_config, res_logs)) => {
|
||||
exists_keys.extend(use_keys(&res_config));
|
||||
@@ -289,11 +337,20 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
}
|
||||
Err(err) => logs.push(("exception".into(), err.to_string().into())),
|
||||
}
|
||||
|
||||
result_map.insert(script_item.uid, logs);
|
||||
}
|
||||
|
||||
// 合并默认的config
|
||||
(config, exists_keys, result_map)
|
||||
}
|
||||
|
||||
async fn merge_default_config(
|
||||
mut config: Mapping,
|
||||
clash_config: Mapping,
|
||||
socks_enabled: bool,
|
||||
http_enabled: bool,
|
||||
#[cfg(not(target_os = "windows"))] redir_enabled: bool,
|
||||
#[cfg(target_os = "linux")] tproxy_enabled: bool,
|
||||
) -> Mapping {
|
||||
for (key, value) in clash_config.into_iter() {
|
||||
if key.as_str() == Some("tun") {
|
||||
let mut tun = config.get_mut("tun").map_or_else(Mapping::new, |val| {
|
||||
@@ -353,66 +410,140 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 内建脚本最后跑
|
||||
config
|
||||
}
|
||||
|
||||
fn apply_builtin_scripts(
|
||||
mut config: Mapping,
|
||||
clash_core: Option<String>,
|
||||
enable_builtin: bool,
|
||||
) -> Mapping {
|
||||
if enable_builtin {
|
||||
ChainItem::builtin()
|
||||
.into_iter()
|
||||
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||
.map(|(_, c)| c)
|
||||
.for_each(|item| {
|
||||
log::debug!(target: "app", "run builtin script {}", item.uid);
|
||||
logging!(debug, Type::Core, "run builtin script {}", item.uid);
|
||||
if let ChainType::Script(script) = item.data {
|
||||
match use_script(script, config.to_owned(), "".into()) {
|
||||
Ok((res_config, _)) => {
|
||||
config = res_config;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "builtin script error `{err}`");
|
||||
logging!(error, Type::Core, "builtin script error `{err}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
config = use_tun(config, enable_tun);
|
||||
config = use_sort(config);
|
||||
config
|
||||
}
|
||||
|
||||
// 应用独立的DNS配置(如果启用)
|
||||
if enable_dns_settings {
|
||||
use crate::utils::dirs;
|
||||
use std::fs;
|
||||
async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping {
|
||||
if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() {
|
||||
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
||||
|
||||
if let Ok(app_dir) = dirs::app_home_dir() {
|
||||
let dns_path = app_dir.join("dns_config.yaml");
|
||||
|
||||
if dns_path.exists()
|
||||
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path)
|
||||
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
|
||||
if dns_path.exists()
|
||||
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path).await
|
||||
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
|
||||
{
|
||||
if let Some(hosts_value) = dns_config.get("hosts")
|
||||
&& hosts_value.is_mapping()
|
||||
{
|
||||
// 处理hosts配置
|
||||
if let Some(hosts_value) = dns_config.get("hosts")
|
||||
&& hosts_value.is_mapping()
|
||||
{
|
||||
config.insert("hosts".into(), hosts_value.clone());
|
||||
log::info!(target: "app", "apply hosts configuration");
|
||||
}
|
||||
config.insert("hosts".into(), hosts_value.clone());
|
||||
logging!(info, Type::Core, "apply hosts configuration");
|
||||
}
|
||||
|
||||
if let Some(dns_value) = dns_config.get("dns") {
|
||||
if let Some(dns_mapping) = dns_value.as_mapping() {
|
||||
config.insert("dns".into(), dns_mapping.clone().into());
|
||||
log::info!(target: "app", "apply dns_config.yaml (dns section)");
|
||||
}
|
||||
} else {
|
||||
config.insert("dns".into(), dns_config.into());
|
||||
log::info!(target: "app", "apply dns_config.yaml");
|
||||
if let Some(dns_value) = dns_config.get("dns") {
|
||||
if let Some(dns_mapping) = dns_value.as_mapping() {
|
||||
config.insert("dns".into(), dns_mapping.clone().into());
|
||||
logging!(info, Type::Core, "apply dns_config.yaml (dns section)");
|
||||
}
|
||||
} else {
|
||||
config.insert("dns".into(), dns_config.into());
|
||||
logging!(info, Type::Core, "apply dns_config.yaml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Enhance mode
|
||||
/// 返回最终订阅、该订阅包含的键、和script执行的结果
|
||||
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// gather config values
|
||||
let cfg_vals = get_config_values().await;
|
||||
let ConfigValues {
|
||||
clash_config,
|
||||
clash_core,
|
||||
enable_tun,
|
||||
enable_builtin,
|
||||
socks_enabled,
|
||||
http_enabled,
|
||||
enable_dns_settings,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
redir_enabled,
|
||||
#[cfg(target_os = "linux")]
|
||||
tproxy_enabled,
|
||||
} = cfg_vals;
|
||||
|
||||
// collect profile items
|
||||
let profile = collect_profile_items().await;
|
||||
let config = profile.config;
|
||||
let merge_item = profile.merge_item;
|
||||
let script_item = profile.script_item;
|
||||
let rules_item = profile.rules_item;
|
||||
let proxies_item = profile.proxies_item;
|
||||
let groups_item = profile.groups_item;
|
||||
let global_merge = profile.global_merge;
|
||||
let global_script = profile.global_script;
|
||||
let profile_name = profile.profile_name;
|
||||
|
||||
// process globals
|
||||
let (config, exists_keys, result_map) =
|
||||
process_global_items(config, global_merge, global_script, profile_name.clone());
|
||||
|
||||
// process profile-specific items
|
||||
let (config, exists_keys, result_map) = process_profile_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
rules_item,
|
||||
proxies_item,
|
||||
groups_item,
|
||||
merge_item,
|
||||
script_item,
|
||||
profile_name,
|
||||
);
|
||||
|
||||
// merge default clash config
|
||||
let config = merge_default_config(
|
||||
config,
|
||||
clash_config,
|
||||
socks_enabled,
|
||||
http_enabled,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
redir_enabled,
|
||||
#[cfg(target_os = "linux")]
|
||||
tproxy_enabled,
|
||||
)
|
||||
.await;
|
||||
|
||||
// builtin scripts
|
||||
let mut config = apply_builtin_scripts(config, clash_core, enable_builtin);
|
||||
|
||||
config = use_tun(config, enable_tun);
|
||||
config = use_sort(config);
|
||||
|
||||
// dns settings
|
||||
config = apply_dns_settings(config, enable_dns_settings).await;
|
||||
|
||||
let mut exists_set = HashSet::new();
|
||||
exists_set.extend(exists_keys);
|
||||
exists_keys = exists_set.into_iter().collect();
|
||||
let exists_keys: Vec<String> = exists_set.into_iter().collect();
|
||||
|
||||
(config, exists_keys, result_map)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use serde_yaml_ng::{Mapping, Value};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::process::AsyncHandler;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
macro_rules! revise {
|
||||
($map: expr, $key: expr, $val: expr) => {
|
||||
@@ -42,9 +44,10 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
|
||||
if should_override {
|
||||
revise!(tun_val, "stack", "mixed");
|
||||
log::warn!(
|
||||
target: "app",
|
||||
"gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility"
|
||||
logging!(
|
||||
warn,
|
||||
Type::Network,
|
||||
"Warning: gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::backup,
|
||||
logging, logging_error,
|
||||
process::AsyncHandler,
|
||||
utils::{
|
||||
dirs::{PathBufExec, app_home_dir, local_backup_dir},
|
||||
logging::Type,
|
||||
@@ -12,7 +13,8 @@ use chrono::Utc;
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
use std::{fs, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LocalBackupFile {
|
||||
@@ -24,7 +26,7 @@ pub struct LocalBackupFile {
|
||||
|
||||
/// Create a backup and upload to WebDAV
|
||||
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
||||
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
|
||||
logging!(error, Type::Backup, "Failed to create backup: {err:#?}");
|
||||
err
|
||||
})?;
|
||||
@@ -97,12 +99,14 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
})?;
|
||||
|
||||
// extract zip file
|
||||
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
|
||||
let value = backup_storage_path.clone();
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
logging_error!(
|
||||
Type::Backup,
|
||||
super::patch_verge(
|
||||
IVerge {
|
||||
&IVerge {
|
||||
webdav_url,
|
||||
webdav_username,
|
||||
webdav_password,
|
||||
@@ -119,7 +123,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
|
||||
/// Create a backup and save to local storage
|
||||
pub async fn create_local_backup() -> Result<()> {
|
||||
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
||||
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Backup,
|
||||
@@ -131,7 +135,7 @@ pub async fn create_local_backup() -> Result<()> {
|
||||
let backup_dir = local_backup_dir()?;
|
||||
let target_path = backup_dir.join(file_name.as_str());
|
||||
|
||||
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()) {
|
||||
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await {
|
||||
logging!(
|
||||
error,
|
||||
Type::Backup,
|
||||
@@ -151,12 +155,12 @@ pub async fn create_local_backup() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
|
||||
async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
|
||||
if let Some(parent) = to.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
match fs::rename(&from, &to) {
|
||||
match fs::rename(&from, &to).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(rename_err) => {
|
||||
// Attempt copy + remove as fallback, covering cross-device moves
|
||||
@@ -165,8 +169,11 @@ fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
|
||||
Type::Backup,
|
||||
"Failed to rename backup file directly, fallback to copy/remove: {rename_err:#?}"
|
||||
);
|
||||
fs::copy(&from, &to).map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?;
|
||||
fs::copy(&from, &to)
|
||||
.await
|
||||
.map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?;
|
||||
fs::remove_file(&from)
|
||||
.await
|
||||
.map_err(|err| anyhow!("Failed to remove temp backup file: {err:#?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -174,24 +181,25 @@ fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
|
||||
}
|
||||
|
||||
/// List local backups
|
||||
pub fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
|
||||
pub async fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
|
||||
let backup_dir = local_backup_dir()?;
|
||||
if !backup_dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut backups = Vec::new();
|
||||
for entry in fs::read_dir(&backup_dir)? {
|
||||
let entry = entry?;
|
||||
let mut dir = fs::read_dir(&backup_dir).await?;
|
||||
while let Some(entry) = dir.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
let metadata = entry.metadata().await?;
|
||||
if !metadata.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||
continue;
|
||||
let file_name = match path.file_name().and_then(|name| name.to_str()) {
|
||||
Some(name) => name,
|
||||
None => continue,
|
||||
};
|
||||
let metadata = entry.metadata()?;
|
||||
let last_modified = metadata
|
||||
.modified()
|
||||
.map(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339())
|
||||
@@ -239,12 +247,13 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
|
||||
let mut zip = zip::ZipArchive::new(fs::File::open(&target_path)?)?;
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
logging_error!(
|
||||
Type::Backup,
|
||||
super::patch_verge(
|
||||
IVerge {
|
||||
&IVerge {
|
||||
webdav_url,
|
||||
webdav_username,
|
||||
webdav_password,
|
||||
@@ -258,7 +267,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Export local backup file to user selected destination
|
||||
pub fn export_local_backup(filename: String, destination: String) -> Result<()> {
|
||||
pub async fn export_local_backup(filename: String, destination: String) -> Result<()> {
|
||||
let backup_dir = local_backup_dir()?;
|
||||
let source_path = backup_dir.join(filename.as_str());
|
||||
if !source_path.exists() {
|
||||
@@ -267,10 +276,11 @@ pub fn export_local_backup(filename: String, destination: String) -> Result<()>
|
||||
|
||||
let dest_path = PathBuf::from(destination.as_str());
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
fs::copy(&source_path, &dest_path)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| anyhow!("Failed to export backup file: {err:#?}"))?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{CoreManager, handle, tray},
|
||||
logging_error,
|
||||
logging, logging_error,
|
||||
process::AsyncHandler,
|
||||
utils::{self, logging::Type, resolve},
|
||||
};
|
||||
@@ -17,7 +17,7 @@ pub async fn restart_clash_core() {
|
||||
}
|
||||
Err(err) => {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target:"app", "{err}");
|
||||
logging!(error, Type::Core, "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ pub async fn restart_app() {
|
||||
"restart_app::error",
|
||||
format!("Failed to cleanup resources: {err}"),
|
||||
);
|
||||
log::error!(target:"app", "Restart failed during cleanup: {err}");
|
||||
logging!(error, Type::Core, "Restart failed during cleanup: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fn after_change_clash_mode() {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to get connections: {err}");
|
||||
logging!(error, Type::Core, "Failed to get connections: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -64,7 +64,7 @@ pub async fn change_clash_mode(mode: String) {
|
||||
let json_value = serde_json::json!({
|
||||
"mode": mode
|
||||
});
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
logging!(debug, Type::Core, "change clash mode to {mode}");
|
||||
match handle::Handle::mihomo()
|
||||
.await
|
||||
.patch_base_config(&json_value)
|
||||
@@ -91,7 +91,7 @@ pub async fn change_clash_mode(mode: String) {
|
||||
after_change_clash_mode();
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
Err(err) => logging!(error, Type::Core, "{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
log::trace!(target: "app", "test_delay response: {response:#?}");
|
||||
logging!(trace, Type::Network, "test_delay response: {response:#?}");
|
||||
if response.status().is_success() {
|
||||
Ok(start.elapsed().as_millis() as u32)
|
||||
} else {
|
||||
@@ -131,7 +131,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!(target: "app", "test_delay error: {err:#?}");
|
||||
logging!(trace, Type::Network, "test_delay error: {err:#?}");
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,23 +63,19 @@ enum UpdateFlags {
|
||||
LighteWeight = 1 << 10,
|
||||
}
|
||||
|
||||
/// Patch Verge configuration
|
||||
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
Config::verge()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(patch.clone());
|
||||
fn determine_update_flags(patch: &IVerge) -> i32 {
|
||||
let mut update_flags: i32 = UpdateFlags::None as i32;
|
||||
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let pac = patch.proxy_auto_config;
|
||||
let pac_content = patch.pac_file_content;
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let language = patch.language;
|
||||
let pac_content = &patch.pac_file_content;
|
||||
let proxy_bypass = &patch.system_proxy_bypass;
|
||||
let language = &patch.language;
|
||||
let mixed_port = patch.verge_mixed_port;
|
||||
#[cfg(target_os = "macos")]
|
||||
let tray_icon = patch.tray_icon;
|
||||
let tray_icon = &patch.tray_icon;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let tray_icon: Option<String> = None;
|
||||
let common_tray_icon = patch.common_tray_icon;
|
||||
@@ -98,147 +94,156 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
let http_enabled = patch.verge_http_enabled;
|
||||
let http_port = patch.verge_port;
|
||||
let enable_tray_speed = patch.enable_tray_speed;
|
||||
let enable_tray_icon = patch.enable_tray_icon;
|
||||
// let enable_tray_icon = patch.enable_tray_icon;
|
||||
let enable_global_hotkey = patch.enable_global_hotkey;
|
||||
let tray_event = patch.tray_event;
|
||||
let tray_event = &patch.tray_event;
|
||||
let home_cards = patch.home_cards.clone();
|
||||
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
|
||||
let enable_external_controller = patch.enable_external_controller;
|
||||
let res: std::result::Result<(), anyhow::Error> = {
|
||||
// Initialize with no flags set
|
||||
let mut update_flags: i32 = UpdateFlags::None as i32;
|
||||
let tray_inline_proxy_groups = patch.tray_inline_proxy_groups;
|
||||
|
||||
if tun_mode.is_some() {
|
||||
update_flags |= UpdateFlags::ClashConfig as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
if enable_global_hotkey.is_some() || home_cards.is_some() {
|
||||
update_flags |= UpdateFlags::VergeConfig as i32;
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if redir_enabled.is_some() || redir_port.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
if socks_enabled.is_some()
|
||||
|| http_enabled.is_some()
|
||||
|| socks_port.is_some()
|
||||
|| http_port.is_some()
|
||||
|| mixed_port.is_some()
|
||||
{
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
if auto_launch.is_some() {
|
||||
update_flags |= UpdateFlags::Launch as i32;
|
||||
}
|
||||
if tun_mode.is_some() {
|
||||
update_flags |= UpdateFlags::ClashConfig as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
if enable_global_hotkey.is_some() || home_cards.is_some() {
|
||||
update_flags |= UpdateFlags::VergeConfig as i32;
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if redir_enabled.is_some() || redir_port.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
if socks_enabled.is_some()
|
||||
|| http_enabled.is_some()
|
||||
|| socks_port.is_some()
|
||||
|| http_port.is_some()
|
||||
|| mixed_port.is_some()
|
||||
{
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
if auto_launch.is_some() {
|
||||
update_flags |= UpdateFlags::Launch as i32;
|
||||
}
|
||||
|
||||
if system_proxy.is_some() {
|
||||
update_flags |= UpdateFlags::SysProxy as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
if system_proxy.is_some() {
|
||||
update_flags |= UpdateFlags::SysProxy as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
|
||||
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
|
||||
update_flags |= UpdateFlags::SysProxy as i32;
|
||||
}
|
||||
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
|
||||
update_flags |= UpdateFlags::SysProxy as i32;
|
||||
}
|
||||
|
||||
if language.is_some() {
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
}
|
||||
if common_tray_icon.is_some()
|
||||
|| sysproxy_tray_icon.is_some()
|
||||
|| tun_tray_icon.is_some()
|
||||
|| tray_icon.is_some()
|
||||
|| enable_tray_speed.is_some()
|
||||
|| enable_tray_icon.is_some()
|
||||
{
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
if language.is_some() {
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
}
|
||||
if common_tray_icon.is_some()
|
||||
|| sysproxy_tray_icon.is_some()
|
||||
|| tun_tray_icon.is_some()
|
||||
|| tray_icon.is_some()
|
||||
|| enable_tray_speed.is_some()
|
||||
// || enable_tray_icon.is_some()
|
||||
{
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
|
||||
if patch.hotkeys.is_some() {
|
||||
update_flags |= UpdateFlags::Hotkey as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
}
|
||||
if patch.hotkeys.is_some() {
|
||||
update_flags |= UpdateFlags::Hotkey as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
}
|
||||
|
||||
if tray_event.is_some() {
|
||||
update_flags |= UpdateFlags::SystrayClickBehavior as i32;
|
||||
}
|
||||
if tray_event.is_some() {
|
||||
update_flags |= UpdateFlags::SystrayClickBehavior as i32;
|
||||
}
|
||||
|
||||
if enable_auto_light_weight.is_some() {
|
||||
update_flags |= UpdateFlags::LighteWeight as i32;
|
||||
}
|
||||
if enable_auto_light_weight.is_some() {
|
||||
update_flags |= UpdateFlags::LighteWeight as i32;
|
||||
}
|
||||
|
||||
// 处理 external-controller 的开关
|
||||
if enable_external_controller.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
if enable_external_controller.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
|
||||
// Process updates based on flags
|
||||
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
||||
CoreManager::global().update_config().await?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
|
||||
Config::verge().await.draft_mut().enable_global_hotkey = enable_global_hotkey;
|
||||
handle::Handle::refresh_verge();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
|
||||
sysopt::Sysopt::global().update_launch().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
|
||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0
|
||||
&& let Some(hotkeys) = patch.hotkeys
|
||||
{
|
||||
hotkey::Hotkey::global().update(hotkeys).await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
|
||||
tray::Tray::global().update_menu().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
|
||||
tray::Tray::global().update_icon().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
|
||||
tray::Tray::global().update_tooltip().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
|
||||
tray::Tray::global().update_click_behavior().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
|
||||
if enable_auto_light_weight.unwrap_or(false) {
|
||||
lightweight::enable_auto_light_weight_mode().await;
|
||||
} else {
|
||||
lightweight::disable_auto_light_weight_mode();
|
||||
}
|
||||
}
|
||||
if tray_inline_proxy_groups.is_some() {
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
}
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::verge().await.apply();
|
||||
if !not_save_file {
|
||||
// 分离数据获取和异步调用
|
||||
let verge_data = Config::verge().await.data_mut().clone();
|
||||
verge_data.save_file().await?;
|
||||
}
|
||||
update_flags
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::verge().await.discard();
|
||||
Err(err)
|
||||
async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<()> {
|
||||
// Process updates based on flags
|
||||
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
||||
CoreManager::global().update_config().await?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
|
||||
Config::verge().await.draft_mut().enable_global_hotkey = patch.enable_global_hotkey;
|
||||
handle::Handle::refresh_verge();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
|
||||
sysopt::Sysopt::global().update_launch().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
|
||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0
|
||||
&& let Some(hotkeys) = &patch.hotkeys
|
||||
{
|
||||
hotkey::Hotkey::global().update(hotkeys.to_owned()).await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
|
||||
tray::Tray::global().update_menu().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
|
||||
tray::Tray::global().update_icon().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
|
||||
tray::Tray::global().update_tooltip().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
|
||||
tray::Tray::global().update_click_behavior().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
|
||||
if patch.enable_auto_light_weight_mode.unwrap_or(false) {
|
||||
lightweight::enable_auto_light_weight_mode().await;
|
||||
} else {
|
||||
lightweight::disable_auto_light_weight_mode();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
|
||||
Config::verge().await.draft_mut().patch_config(patch);
|
||||
|
||||
let update_flags = determine_update_flags(patch);
|
||||
let process_flag_result: std::result::Result<(), anyhow::Error> = {
|
||||
process_terminated_flags(update_flags, patch).await?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(err) = process_flag_result {
|
||||
Config::verge().await.discard();
|
||||
return Err(err);
|
||||
}
|
||||
Config::verge().await.apply();
|
||||
if !not_save_file {
|
||||
// 分离数据获取和异步调用
|
||||
let verge_data = Config::verge().await.data_ref().clone();
|
||||
verge_data.save_file().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,154 +18,188 @@ pub async fn toggle_proxy_profile(profile_index: String) {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
logging!(error, Type::Tray, "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a profile
|
||||
/// If updating current profile, activate it
|
||||
/// auto_refresh: 是否自动更新配置和刷新前端
|
||||
pub async fn update_profile(
|
||||
uid: String,
|
||||
option: Option<PrfOption>,
|
||||
auto_refresh: Option<bool>,
|
||||
) -> Result<()> {
|
||||
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
|
||||
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
|
||||
async fn should_update_profile(
|
||||
uid: &String,
|
||||
ignore_auto_update: bool,
|
||||
) -> Result<Option<(String, Option<PrfOption>)>> {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
let item = profiles.get_item(uid)?;
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL,无法更新");
|
||||
bail!("failed to get the profile item url");
|
||||
} else if !item
|
||||
if !is_remote {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[订阅更新] {uid} 不是远程订阅,跳过更新"
|
||||
);
|
||||
Ok(None)
|
||||
} else if item.url.is_none() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"Warning: [订阅更新] {uid} 缺少URL,无法更新"
|
||||
);
|
||||
bail!("failed to get the profile item url");
|
||||
} else if !ignore_auto_update
|
||||
&& !item
|
||||
.option
|
||||
.as_ref()
|
||||
.and_then(|o| o.allow_auto_update)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
log::info!(target: "app", "[订阅更新] {} 禁止自动更新,跳过更新", uid);
|
||||
None
|
||||
} else {
|
||||
log::info!(target: "app",
|
||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||
uid,
|
||||
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
|
||||
);
|
||||
Some((
|
||||
item.url
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
|
||||
item.option.clone(),
|
||||
))
|
||||
}
|
||||
{
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[订阅更新] {} 禁止自动更新,跳过更新",
|
||||
uid
|
||||
);
|
||||
Ok(None)
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||
uid,
|
||||
item.url
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
|
||||
);
|
||||
Ok(Some((
|
||||
item.url
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
|
||||
item.option.clone(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform_profile_update(
|
||||
uid: &String,
|
||||
url: &String,
|
||||
opt: Option<&PrfOption>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<bool> {
|
||||
logging!(info, Type::Config, "[订阅更新] 开始下载新的订阅内容");
|
||||
let mut merged_opt = PrfOption::merge(opt, option);
|
||||
let is_current = {
|
||||
let profiles = Config::profiles().await;
|
||||
profiles.latest_ref().is_current_profile_index(uid)
|
||||
};
|
||||
let profile_name = {
|
||||
let profiles = Config::profiles().await;
|
||||
profiles
|
||||
.latest_ref()
|
||||
.get_name_by_uid(uid)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let mut last_err;
|
||||
|
||||
let should_update = match url_opt {
|
||||
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
|
||||
Ok(mut item) => {
|
||||
logging!(info, Type::Config, "[订阅更新] 更新订阅配置成功");
|
||||
profiles_draft_update_item_safe(uid, &mut item).await?;
|
||||
return Ok(is_current);
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"Warning: [订阅更新] 正常更新失败: {err},尝试使用Clash代理更新"
|
||||
);
|
||||
last_err = err;
|
||||
}
|
||||
}
|
||||
|
||||
merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(true);
|
||||
merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(false);
|
||||
|
||||
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
|
||||
Ok(mut item) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[订阅更新] 使用 Clash代理 更新订阅配置成功"
|
||||
);
|
||||
profiles_draft_update_item_safe(uid, &mut item).await?;
|
||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||
drop(last_err);
|
||||
return Ok(is_current);
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"Warning: [订阅更新] 正常更新失败: {err},尝试使用Clash代理更新"
|
||||
);
|
||||
last_err = err;
|
||||
}
|
||||
}
|
||||
|
||||
merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(false);
|
||||
merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(true);
|
||||
|
||||
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
|
||||
Ok(mut item) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[订阅更新] 使用 系统代理 更新订阅配置成功"
|
||||
);
|
||||
profiles_draft_update_item_safe(uid, &mut item).await?;
|
||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||
drop(last_err);
|
||||
return Ok(is_current);
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"Warning: [订阅更新] 正常更新失败: {err},尝试使用系统代理更新"
|
||||
);
|
||||
last_err = err;
|
||||
}
|
||||
}
|
||||
|
||||
handle::Handle::notice_message(
|
||||
"update_failed_even_with_clash",
|
||||
format!("{profile_name} - {last_err}"),
|
||||
);
|
||||
Ok(is_current)
|
||||
}
|
||||
|
||||
pub async fn update_profile(
|
||||
uid: &String,
|
||||
option: Option<&PrfOption>,
|
||||
auto_refresh: bool,
|
||||
ignore_auto_update: bool,
|
||||
) -> Result<()> {
|
||||
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
|
||||
let url_opt = should_update_profile(uid, ignore_auto_update).await?;
|
||||
|
||||
let should_refresh = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
|
||||
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
|
||||
|
||||
// 尝试使用正常设置更新
|
||||
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||
Ok(item) => {
|
||||
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||
let profiles = Config::profiles().await;
|
||||
|
||||
// 使用Send-safe helper函数
|
||||
let result = profiles_draft_update_item_safe(uid.clone(), item).await;
|
||||
result?;
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.latest_ref().get_current();
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(err) => {
|
||||
// 首次更新失败,尝试使用Clash代理
|
||||
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err},尝试使用Clash代理更新");
|
||||
|
||||
// 发送通知
|
||||
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
||||
|
||||
// 保存原始代理设置
|
||||
let original_with_proxy = merged_opt.as_ref().and_then(|o| o.with_proxy);
|
||||
let original_self_proxy = merged_opt.as_ref().and_then(|o| o.self_proxy);
|
||||
|
||||
// 创建使用Clash代理的选项
|
||||
let mut fallback_opt = merged_opt.unwrap_or_default();
|
||||
fallback_opt.with_proxy = Some(false);
|
||||
fallback_opt.self_proxy = Some(true);
|
||||
|
||||
// 使用Clash代理重试
|
||||
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
|
||||
Ok(mut item) => {
|
||||
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
|
||||
|
||||
// 恢复原始代理设置到item
|
||||
if let Some(option) = item.option.as_mut() {
|
||||
option.with_proxy = original_with_proxy;
|
||||
option.self_proxy = original_self_proxy;
|
||||
}
|
||||
|
||||
// 更新到配置
|
||||
let profiles = Config::profiles().await;
|
||||
|
||||
// 使用 Send-safe 方法进行数据操作
|
||||
profiles_draft_update_item_safe(uid.clone(), item.clone()).await?;
|
||||
|
||||
// 获取配置名称用于通知
|
||||
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
|
||||
|
||||
// 发送通知告知用户自动更新使用了回退机制
|
||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.data_ref().get_current();
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(retry_err) => {
|
||||
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
|
||||
handle::Handle::notice_message(
|
||||
"update_failed_even_with_clash",
|
||||
format!("{retry_err}"),
|
||||
);
|
||||
return Err(retry_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
perform_profile_update(uid, &url, opt.as_ref(), option).await? && auto_refresh
|
||||
}
|
||||
None => auto_refresh,
|
||||
};
|
||||
|
||||
if should_update {
|
||||
if should_refresh {
|
||||
logging!(info, Type::Config, "[订阅更新] 更新内核配置");
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Config, "[订阅更新] 更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
// if let Err(err) = cmd::proxy::force_refresh_proxies().await {
|
||||
// logging!(
|
||||
// error,
|
||||
// Type::Config,
|
||||
// true,
|
||||
// "[订阅更新] 代理组刷新失败: {}",
|
||||
// err
|
||||
// );
|
||||
// }
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "[订阅更新] 更新失败: {}", err);
|
||||
handle::Handle::notice_message("update_failed", format!("{err}"));
|
||||
log::error!(target: "app", "{err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle,
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use std::env;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
/// Toggle system proxy on/off
|
||||
pub async fn toggle_system_proxy() {
|
||||
// 获取当前系统代理状态
|
||||
let enable = {
|
||||
let verge = Config::verge().await;
|
||||
|
||||
verge.latest_ref().enable_system_proxy.unwrap_or(false)
|
||||
};
|
||||
// 获取自动关闭连接设置
|
||||
let auto_close_connection = {
|
||||
let verge = Config::verge().await;
|
||||
|
||||
verge.latest_ref().auto_close_connection.unwrap_or(false)
|
||||
};
|
||||
let verge = Config::verge().await;
|
||||
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false);
|
||||
let auto_close_connection = verge.latest_ref().auto_close_connection.unwrap_or(false);
|
||||
|
||||
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
|
||||
if enable
|
||||
&& auto_close_connection
|
||||
&& let Err(err) = handle::Handle::mihomo().await.close_all_connections().await
|
||||
{
|
||||
log::error!(target: "app", "Failed to close all connections: {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::ProxyMode,
|
||||
"Failed to close all connections: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
let patch_result = super::patch_verge(
|
||||
IVerge {
|
||||
&IVerge {
|
||||
enable_system_proxy: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
@@ -39,7 +36,7 @@ pub async fn toggle_system_proxy() {
|
||||
|
||||
match patch_result {
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
Err(err) => logging!(error, Type::ProxyMode, "{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +46,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
match super::patch_verge(
|
||||
IVerge {
|
||||
&IVerge {
|
||||
enable_tun_mode: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
@@ -58,7 +55,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
Err(err) => logging!(error, Type::ProxyMode, "{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,12 +110,16 @@ pub async fn copy_clash_env() {
|
||||
}
|
||||
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
|
||||
_ => {
|
||||
log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}");
|
||||
logging!(
|
||||
error,
|
||||
Type::ProxyMode,
|
||||
"copy_clash_env: Invalid env type! {env_type}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if cliboard.write_text(export_text).is_err() {
|
||||
log::error!(target: "app", "Failed to write to clipboard");
|
||||
logging!(error, Type::ProxyMode, "Failed to write to clipboard");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub async fn open_or_close_dashboard() {
|
||||
async fn open_or_close_dashboard_internal() {
|
||||
let _ = lightweight::exit_lightweight_mode().await;
|
||||
let result = WindowManager::toggle_main_window().await;
|
||||
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||
logging!(info, Type::Window, "Window toggle result: {result:?}");
|
||||
}
|
||||
|
||||
pub async fn quit() {
|
||||
@@ -47,7 +47,7 @@ pub async fn clean_async() -> bool {
|
||||
let tun_task = async {
|
||||
let tun_enabled = Config::verge()
|
||||
.await
|
||||
.data_ref()
|
||||
.latest_ref()
|
||||
.enable_tun_mode
|
||||
.unwrap_or(false);
|
||||
|
||||
@@ -71,16 +71,20 @@ pub async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
log::info!(target: "app", "TUN模式已禁用");
|
||||
logging!(info, Type::Window, "TUN模式已禁用");
|
||||
true
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "禁用TUN模式失败: {e}");
|
||||
logging!(warn, Type::Window, "Warning: 禁用TUN模式失败: {e}");
|
||||
// 超时不阻塞退出
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "禁用TUN模式超时(可能系统正在关机),继续退出流程");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
"Warning: 禁用TUN模式超时(可能系统正在关机),继续退出流程"
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -101,7 +105,7 @@ pub async fn clean_async() -> bool {
|
||||
.unwrap_or(false);
|
||||
|
||||
if !sys_proxy_enabled {
|
||||
log::info!(target: "app", "系统代理未启用,跳过重置");
|
||||
logging!(info, Type::Window, "系统代理未启用,跳过重置");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -110,19 +114,23 @@ pub async fn clean_async() -> bool {
|
||||
|
||||
if is_shutting_down {
|
||||
// sysproxy-rs 操作注册表(避免.exe的dll错误)
|
||||
log::info!(target: "app", "检测到正在关机,syspro-rs操作注册表关闭系统代理");
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
"检测到正在关机,syspro-rs操作注册表关闭系统代理"
|
||||
);
|
||||
|
||||
match Sysproxy::get_system_proxy() {
|
||||
Ok(mut sysproxy) => {
|
||||
sysproxy.enable = false;
|
||||
if let Err(e) = sysproxy.set_system_proxy() {
|
||||
log::warn!(target: "app", "关机时关闭系统代理失败: {e}");
|
||||
logging!(warn, Type::Window, "Warning: 关机时关闭系统代理失败: {e}");
|
||||
} else {
|
||||
log::info!(target: "app", "系统代理已关闭(通过注册表)");
|
||||
logging!(info, Type::Window, "系统代理已关闭(通过注册表)");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "关机时获取代理设置失败: {e}");
|
||||
logging!(warn, Type::Window, "Warning: 关机时获取代理设置失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +144,7 @@ pub async fn clean_async() -> bool {
|
||||
}
|
||||
|
||||
// 正常退出:使用 sysproxy.exe 重置代理
|
||||
log::info!(target: "app", "sysproxy.exe重置系统代理");
|
||||
logging!(info, Type::Window, "sysproxy.exe重置系统代理");
|
||||
|
||||
match timeout(
|
||||
Duration::from_secs(2),
|
||||
@@ -145,15 +153,19 @@ pub async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
log::info!(target: "app", "系统代理已重置");
|
||||
logging!(info, Type::Window, "系统代理已重置");
|
||||
true
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "重置系统代理失败: {e}");
|
||||
logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "重置系统代理超时,继续退出流程");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
"Warning: 重置系统代理超时,继续退出流程"
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -169,11 +181,11 @@ pub async fn clean_async() -> bool {
|
||||
.unwrap_or(false);
|
||||
|
||||
if !sys_proxy_enabled {
|
||||
log::info!(target: "app", "系统代理未启用,跳过重置");
|
||||
logging!(info, Type::Window, "系统代理未启用,跳过重置");
|
||||
return true;
|
||||
}
|
||||
|
||||
log::info!(target: "app", "开始重置系统代理...");
|
||||
logging!(info, Type::Window, "开始重置系统代理...");
|
||||
|
||||
match timeout(
|
||||
Duration::from_millis(1500),
|
||||
@@ -182,15 +194,15 @@ pub async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
log::info!(target: "app", "系统代理已重置");
|
||||
logging!(info, Type::Window, "系统代理已重置");
|
||||
true
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "重置系统代理失败: {e}");
|
||||
logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "重置系统代理超时,继续退出");
|
||||
logging!(warn, Type::Window, "Warning: 重置系统代理超时,继续退出");
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -206,11 +218,15 @@ pub async fn clean_async() -> bool {
|
||||
|
||||
match timeout(stop_timeout, CoreManager::global().stop_core()).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "core已停止");
|
||||
logging!(info, Type::Window, "core已停止");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "停止core超时(可能系统正在关机),继续退出");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
"Warning: 停止core超时(可能系统正在关机),继续退出"
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -226,11 +242,11 @@ pub async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "DNS设置已恢复");
|
||||
logging!(info, Type::Window, "DNS设置已恢复");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "恢复DNS设置超时");
|
||||
logging!(warn, Type::Window, "Warning: 恢复DNS设置超时");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ mod feat;
|
||||
mod module;
|
||||
mod process;
|
||||
pub mod utils;
|
||||
use crate::constants::files;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::module::lightweight;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::utils::linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -19,6 +22,7 @@ use crate::{
|
||||
process::AsyncHandler,
|
||||
utils::{resolve, server},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use config::Config;
|
||||
use once_cell::sync::OnceCell;
|
||||
use tauri::{AppHandle, Manager};
|
||||
@@ -28,11 +32,8 @@ use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use utils::logging::Type;
|
||||
|
||||
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
|
||||
|
||||
/// Application initialization helper functions
|
||||
mod app_init {
|
||||
use anyhow::Result;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Initialize singleton monitoring for other instances
|
||||
@@ -91,14 +92,14 @@ mod app_init {
|
||||
}
|
||||
|
||||
app.deep_link().on_open_url(|event| {
|
||||
let url = event.urls().first().map(|u| u.to_string());
|
||||
if let Some(url) = url {
|
||||
AsyncHandler::spawn(|| async {
|
||||
if let Err(e) = resolve::resolve_scheme(url.into()).await {
|
||||
logging!(error, Type::Setup, "Failed to resolve scheme: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
let urls = event.urls();
|
||||
AsyncHandler::spawn(move || async move {
|
||||
if let Some(url) = urls.first()
|
||||
&& let Err(e) = resolve::resolve_scheme(url.as_ref()).await
|
||||
{
|
||||
logging!(error, Type::Setup, "Failed to resolve scheme: {}", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -115,7 +116,7 @@ mod app_init {
|
||||
{
|
||||
auto_start_plugin_builder = auto_start_plugin_builder
|
||||
.macos_launcher(MacosLauncher::LaunchAgent)
|
||||
.app_name(app.config().identifier.clone());
|
||||
.app_name(&app.config().identifier);
|
||||
}
|
||||
app.handle().plugin(auto_start_plugin_builder.build())?;
|
||||
Ok(())
|
||||
@@ -125,7 +126,7 @@ mod app_init {
|
||||
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
logging!(info, Type::Setup, "初始化窗口状态管理...");
|
||||
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
||||
.with_filename("window_state.json")
|
||||
.with_filename(files::WINDOW_STATE)
|
||||
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
|
||||
.build();
|
||||
app.handle().plugin(window_state_plugin)?;
|
||||
@@ -141,6 +142,8 @@ mod app_init {
|
||||
cmd::open_logs_dir,
|
||||
cmd::open_web_url,
|
||||
cmd::open_core_dir,
|
||||
cmd::open_app_log,
|
||||
cmd::open_core_log,
|
||||
cmd::get_portable_flag,
|
||||
cmd::get_network_interfaces,
|
||||
cmd::get_system_hostname,
|
||||
@@ -285,6 +288,11 @@ pub fn run() {
|
||||
pub async fn handle_reopen(has_visible_windows: bool) {
|
||||
handle::Handle::global().init();
|
||||
|
||||
if lightweight::is_in_lightweight_mode() {
|
||||
lightweight::exit_lightweight_mode().await;
|
||||
return;
|
||||
}
|
||||
|
||||
if !has_visible_windows {
|
||||
handle::Handle::global().set_activation_policy_regular();
|
||||
let _ = WindowManager::show_main_window().await;
|
||||
|
||||
@@ -10,6 +10,5 @@ fn main() {
|
||||
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
|
||||
}
|
||||
}
|
||||
|
||||
app_lib::run();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, timer::Timer, tray::Tray},
|
||||
core::{handle, timer::Timer},
|
||||
log_err, logging,
|
||||
process::AsyncHandler,
|
||||
utils::logging::Type,
|
||||
@@ -43,49 +43,42 @@ impl LightweightState {
|
||||
|
||||
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
|
||||
|
||||
static WINDOW_CLOSE_HANDLER: AtomicU32 = AtomicU32::new(0);
|
||||
static WEBVIEW_FOCUS_HANDLER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
fn set_state(new: LightweightState) {
|
||||
LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release);
|
||||
match new {
|
||||
LightweightState::Normal => {
|
||||
logging!(info, Type::Lightweight, "轻量模式已关闭");
|
||||
}
|
||||
LightweightState::In => {
|
||||
logging!(info, Type::Lightweight, "轻量模式已开启");
|
||||
}
|
||||
LightweightState::Exiting => {
|
||||
logging!(info, Type::Lightweight, "正在退出轻量模式");
|
||||
}
|
||||
}
|
||||
}
|
||||
static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
#[inline]
|
||||
fn get_state() -> LightweightState {
|
||||
LIGHTWEIGHT_STATE.load(Ordering::Acquire).into()
|
||||
}
|
||||
|
||||
// 检查是否处于轻量模式
|
||||
#[inline]
|
||||
fn try_transition(from: LightweightState, to: LightweightState) -> bool {
|
||||
LIGHTWEIGHT_STATE
|
||||
.compare_exchange(
|
||||
from.as_u8(),
|
||||
to.as_u8(),
|
||||
Ordering::AcqRel,
|
||||
Ordering::Relaxed,
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn record_state_and_log(state: LightweightState) {
|
||||
LIGHTWEIGHT_STATE.store(state.as_u8(), Ordering::Release);
|
||||
match state {
|
||||
LightweightState::Normal => logging!(info, Type::Lightweight, "轻量模式已关闭"),
|
||||
LightweightState::In => logging!(info, Type::Lightweight, "轻量模式已开启"),
|
||||
LightweightState::Exiting => logging!(info, Type::Lightweight, "正在退出轻量模式"),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_in_lightweight_mode() -> bool {
|
||||
get_state() == LightweightState::In
|
||||
}
|
||||
|
||||
// 设置轻量模式状态(仅 Normal <-> In)
|
||||
async fn set_lightweight_mode(value: bool) {
|
||||
let current = get_state();
|
||||
if value && current != LightweightState::In {
|
||||
set_state(LightweightState::In);
|
||||
} else if !value && current != LightweightState::Normal {
|
||||
set_state(LightweightState::Normal);
|
||||
}
|
||||
|
||||
// 只有在状态可用时才触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part().await {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_once_auto_lightweight() {
|
||||
pub async fn auto_lightweight_boot() -> Result<()> {
|
||||
let verge_config = Config::verge().await;
|
||||
let enable_auto = verge_config
|
||||
.data_mut()
|
||||
@@ -96,39 +89,23 @@ pub async fn run_once_auto_lightweight() {
|
||||
.enable_silent_start
|
||||
.unwrap_or(false);
|
||||
|
||||
if !(enable_auto && is_silent_start) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
"不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式"
|
||||
);
|
||||
return;
|
||||
if is_silent_start {
|
||||
logging!(info, Type::Lightweight, "静默启动:直接进入轻量模式");
|
||||
let _ = entry_lightweight_mode().await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
set_lightweight_mode(true).await;
|
||||
if !enable_auto {
|
||||
logging!(info, Type::Lightweight, "未开启自动轻量模式,跳过初始化");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
"非静默启动:注册自动轻量模式监听器"
|
||||
);
|
||||
enable_auto_light_weight_mode().await;
|
||||
}
|
||||
|
||||
pub async fn auto_lightweight_mode_init() -> Result<()> {
|
||||
let is_silent_start =
|
||||
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
|
||||
let enable_auto = {
|
||||
Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.enable_auto_light_weight_mode
|
||||
}
|
||||
.unwrap_or(false);
|
||||
|
||||
if enable_auto && !is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
"非静默启动直接挂载自动进入轻量模式监听器!"
|
||||
);
|
||||
set_state(LightweightState::Normal);
|
||||
enable_auto_light_weight_mode().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -151,43 +128,18 @@ pub fn disable_auto_light_weight_mode() {
|
||||
}
|
||||
|
||||
pub async fn entry_lightweight_mode() -> bool {
|
||||
// 尝试从 Normal -> In
|
||||
if LIGHTWEIGHT_STATE
|
||||
.compare_exchange(
|
||||
LightweightState::Normal as u8,
|
||||
LightweightState::In as u8,
|
||||
Ordering::Acquire,
|
||||
Ordering::Relaxed,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
if !try_transition(LightweightState::Normal, LightweightState::In) {
|
||||
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用");
|
||||
return false;
|
||||
}
|
||||
|
||||
record_state_and_log(LightweightState::In);
|
||||
WindowManager::destroy_main_window();
|
||||
|
||||
set_lightweight_mode(true).await;
|
||||
let _ = cancel_light_weight_timer();
|
||||
|
||||
// 回到 In
|
||||
set_state(LightweightState::In);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
// 添加从轻量模式恢复的函数
|
||||
pub async fn exit_lightweight_mode() -> bool {
|
||||
// 尝试从 In -> Exiting
|
||||
if LIGHTWEIGHT_STATE
|
||||
.compare_exchange(
|
||||
LightweightState::In as u8,
|
||||
LightweightState::Exiting as u8,
|
||||
Ordering::Acquire,
|
||||
Ordering::Relaxed,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
if !try_transition(LightweightState::In, LightweightState::Exiting) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
@@ -195,16 +147,10 @@ pub async fn exit_lightweight_mode() -> bool {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
record_state_and_log(LightweightState::Exiting);
|
||||
WindowManager::show_main_window().await;
|
||||
|
||||
set_lightweight_mode(false).await;
|
||||
let _ = cancel_light_weight_timer();
|
||||
|
||||
// 回到 Normal
|
||||
set_state(LightweightState::Normal);
|
||||
|
||||
logging!(info, Type::Lightweight, "轻量模式退出完成");
|
||||
record_state_and_log(LightweightState::Normal);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -215,24 +161,31 @@ pub async fn add_light_weight_timer() {
|
||||
|
||||
fn setup_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let handler = window.listen("tauri://close-requested", move |_event| {
|
||||
let old_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if old_id != 0 {
|
||||
window.unlisten(old_id);
|
||||
}
|
||||
let handler_id = window.listen("tauri://close-requested", move |_event| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
if let Err(e) = setup_light_weight_timer().await {
|
||||
log::warn!("Failed to setup light weight timer: {e}");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Lightweight,
|
||||
"Warning: Failed to setup light weight timer: {e}"
|
||||
);
|
||||
}
|
||||
}));
|
||||
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
|
||||
});
|
||||
|
||||
WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release);
|
||||
WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
|
||||
if handler != 0 {
|
||||
window.unlisten(handler);
|
||||
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
window.unlisten(id);
|
||||
logging!(info, Type::Lightweight, "取消了窗口关闭监听");
|
||||
}
|
||||
}
|
||||
@@ -240,7 +193,11 @@ fn cancel_window_close_listener() {
|
||||
|
||||
fn setup_webview_focus_listener() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let handler = window.listen("tauri://focus", move |_event| {
|
||||
let old_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if old_id != 0 {
|
||||
window.unlisten(old_id);
|
||||
}
|
||||
let handler_id = window.listen("tauri://focus", move |_event| {
|
||||
log_err!(cancel_light_weight_timer());
|
||||
logging!(
|
||||
info,
|
||||
@@ -248,37 +205,45 @@ fn setup_webview_focus_listener() {
|
||||
"监听到窗口获得焦点,取消轻量模式计时"
|
||||
);
|
||||
});
|
||||
|
||||
WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release);
|
||||
WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_webview_focus_listener() {
|
||||
if let Some(window) = handle::Handle::get_window() {
|
||||
let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
|
||||
if handler != 0 {
|
||||
window.unlisten(handler);
|
||||
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
window.unlisten(id);
|
||||
logging!(info, Type::Lightweight, "取消了窗口焦点监听");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup_light_weight_timer() -> Result<()> {
|
||||
Timer::global().init().await?;
|
||||
if let Err(e) = Timer::global().init().await {
|
||||
return Err(e).context("failed to initialize timer");
|
||||
}
|
||||
|
||||
let once_by_minutes = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.auto_light_weight_minutes
|
||||
.unwrap_or(10);
|
||||
|
||||
// 获取task_id
|
||||
{
|
||||
let timer_map = Timer::global().timer_map.read();
|
||||
if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) {
|
||||
logging!(warn, Type::Timer, "轻量模式计时器已存在,跳过创建");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let task_id = {
|
||||
Timer::global()
|
||||
.timer_count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
};
|
||||
|
||||
// 创建任务
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(task_id)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
@@ -289,7 +254,6 @@ async fn setup_light_weight_timer() -> Result<()> {
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
// 添加任务到定时器
|
||||
{
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
delay_timer
|
||||
@@ -297,7 +261,6 @@ async fn setup_light_weight_timer() -> Result<()> {
|
||||
.context("failed to add timer task")?;
|
||||
}
|
||||
|
||||
// 更新任务映射
|
||||
{
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let timer_task = crate::core::timer::TimerTask {
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
use anyhow::Result;
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandChildGuard(Option<CommandChild>);
|
||||
|
||||
impl Drop for CommandChildGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = self.kill() {
|
||||
log::error!(target: "app", "Failed to kill child process: {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
"Failed to kill child process: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::{logging, utils::logging::Type};
|
||||
#[cfg(target_os = "windows")]
|
||||
use anyhow::{Result, anyhow};
|
||||
#[cfg(target_os = "windows")]
|
||||
use log::info;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::{os::windows::process::CommandExt, path::Path, path::PathBuf};
|
||||
@@ -49,15 +49,15 @@ pub async fn create_shortcut() -> Result<()> {
|
||||
.remove_if_exists()
|
||||
.await
|
||||
.inspect(|_| {
|
||||
info!(target: "app", "成功移除旧启动快捷方式");
|
||||
logging!(info, Type::Setup, "成功移除旧启动快捷方式");
|
||||
})
|
||||
.inspect_err(|err| {
|
||||
log::error!(target: "app", "移除旧启动快捷方式失败: {err}");
|
||||
logging!(error, Type::Setup, "移除旧启动快捷方式失败: {err}");
|
||||
});
|
||||
|
||||
// 如果新快捷方式已存在,直接返回成功
|
||||
if new_shortcut_path.exists() {
|
||||
info!(target: "app", "启动快捷方式已存在");
|
||||
logging!(info, Type::Setup, "启动快捷方式已存在");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ pub async fn create_shortcut() -> Result<()> {
|
||||
return Err(anyhow!("创建快捷方式失败: {}", error_msg));
|
||||
}
|
||||
|
||||
info!(target: "app", "成功创建启动快捷方式");
|
||||
logging!(info, Type::Setup, "成功创建启动快捷方式");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -102,22 +102,22 @@ pub async fn remove_shortcut() -> Result<()> {
|
||||
.remove_if_exists()
|
||||
.await
|
||||
.inspect(|_| {
|
||||
info!(target: "app", "成功删除旧启动快捷方式");
|
||||
logging!(info, Type::Setup, "成功删除旧启动快捷方式");
|
||||
removed_any = true;
|
||||
})
|
||||
.inspect_err(|err| {
|
||||
log::error!(target: "app", "删除旧启动快捷方式失败: {err}");
|
||||
logging!(error, Type::Setup, "删除旧启动快捷方式失败: {err}");
|
||||
});
|
||||
|
||||
let _ = new_shortcut_path
|
||||
.remove_if_exists()
|
||||
.await
|
||||
.inspect(|_| {
|
||||
info!(target: "app", "成功删除启动快捷方式");
|
||||
logging!(info, Type::Setup, "成功删除启动快捷方式");
|
||||
removed_any = true;
|
||||
})
|
||||
.inspect_err(|err| {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {err}");
|
||||
logging!(error, Type::Setup, "删除启动快捷方式失败: {err}");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::{core::handle, logging, utils::logging::Type};
|
||||
use crate::{
|
||||
core::{CoreManager, handle, manager::RunningMode},
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use once_cell::sync::OnceCell;
|
||||
@@ -20,7 +24,6 @@ pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
|
||||
pub static CLASH_CONFIG: &str = "config.yaml";
|
||||
pub static VERGE_CONFIG: &str = "verge.yaml";
|
||||
pub static PROFILE_YAML: &str = "profiles.yaml";
|
||||
pub static DNS_CONFIG: &str = "dns_config.yaml";
|
||||
|
||||
/// init portable flag
|
||||
pub fn init_portable_flag() -> Result<()> {
|
||||
@@ -58,7 +61,11 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
match app_handle.path().data_dir() {
|
||||
Ok(dir) => Ok(dir.join(APP_ID)),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get the app home directory: {e}");
|
||||
logging!(
|
||||
error,
|
||||
Type::File,
|
||||
"Failed to get the app home directory: {e}"
|
||||
);
|
||||
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
|
||||
}
|
||||
}
|
||||
@@ -72,7 +79,11 @@ pub fn app_resources_dir() -> Result<PathBuf> {
|
||||
match app_handle.path().resource_dir() {
|
||||
Ok(dir) => Ok(dir.join("resources")),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get the resource directory: {e}");
|
||||
logging!(
|
||||
error,
|
||||
Type::File,
|
||||
"Failed to get the resource directory: {e}"
|
||||
);
|
||||
Err(anyhow::anyhow!("Failed to get the resource directory"))
|
||||
}
|
||||
}
|
||||
@@ -122,6 +133,11 @@ pub fn app_logs_dir() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join("logs"))
|
||||
}
|
||||
|
||||
// latest verge log
|
||||
pub fn app_latest_log() -> Result<PathBuf> {
|
||||
Ok(app_logs_dir()?.join("latest.log"))
|
||||
}
|
||||
|
||||
/// local backups dir
|
||||
pub fn local_backup_dir() -> Result<PathBuf> {
|
||||
let dir = app_home_dir()?.join(BACKUP_DIR);
|
||||
@@ -167,6 +183,15 @@ pub fn service_log_dir() -> Result<PathBuf> {
|
||||
Ok(log_dir)
|
||||
}
|
||||
|
||||
pub fn clash_latest_log() -> Result<PathBuf> {
|
||||
match *CoreManager::global().get_running_mode() {
|
||||
RunningMode::Service => Ok(service_log_dir()?.join("service_latest.log")),
|
||||
RunningMode::Sidecar | RunningMode::NotRunning => {
|
||||
Ok(sidecar_log_dir()?.join("sidecar_latest.log"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
|
||||
let path_str = path
|
||||
.as_os_str()
|
||||
@@ -211,7 +236,11 @@ pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
|
||||
if home_config.exists() || fs::create_dir_all(&home_config).is_ok() {
|
||||
Some(home_config)
|
||||
} else {
|
||||
log::error!(target: "app", "Failed to create safe directory: {home_config:?}");
|
||||
logging!(
|
||||
error,
|
||||
Type::File,
|
||||
"Failed to create safe directory: {home_config:?}"
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
|
||||
@@ -24,22 +24,21 @@ impl<T: Clone + ToOwned> From<T> for Draft<T> {
|
||||
///
|
||||
/// # Methods
|
||||
/// - `data_mut`: Returns a mutable reference to the committed data.
|
||||
/// - `data_ref`: Returns an immutable reference to the committed data.
|
||||
/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists.
|
||||
/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data.
|
||||
/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed.
|
||||
/// - `discard`: Discards the draft data and returns it if it existed.
|
||||
impl<T: Clone + ToOwned> Draft<Box<T>> {
|
||||
/// 正式数据视图
|
||||
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
|
||||
}
|
||||
|
||||
/// 可写正式数据
|
||||
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
|
||||
}
|
||||
|
||||
/// 返回正式数据的只读视图(不包含草稿)
|
||||
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
|
||||
}
|
||||
|
||||
/// 创建或获取草稿并返回可写引用
|
||||
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
let guard = self.inner.upgradable_read();
|
||||
@@ -69,17 +68,21 @@ impl<T: Clone + ToOwned> Draft<Box<T>> {
|
||||
}
|
||||
|
||||
/// 提交草稿,返回旧正式数据
|
||||
pub fn apply(&self) -> Option<Box<T>> {
|
||||
let mut inner = self.inner.write();
|
||||
inner
|
||||
.1
|
||||
.take()
|
||||
.map(|draft| std::mem::replace(&mut inner.0, draft))
|
||||
pub fn apply(&self) {
|
||||
let guard = self.inner.upgradable_read();
|
||||
if guard.1.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
|
||||
if let Some(draft) = guard.1.take() {
|
||||
guard.0 = draft;
|
||||
}
|
||||
}
|
||||
|
||||
/// 丢弃草稿,返回被丢弃的草稿
|
||||
pub fn discard(&self) -> Option<Box<T>> {
|
||||
self.inner.write().1.take()
|
||||
pub fn discard(&self) {
|
||||
self.inner.write().1.take();
|
||||
}
|
||||
|
||||
/// 异步修改正式数据,闭包直接获得 Box<T> 所有权
|
||||
@@ -152,8 +155,7 @@ fn test_draft_box() {
|
||||
}
|
||||
|
||||
// 5. 提交草稿
|
||||
assert!(draft.apply().is_some()); // 第一次提交应有返回
|
||||
assert!(draft.apply().is_none()); // 第二次提交返回 None
|
||||
draft.apply();
|
||||
|
||||
// 正式数据已更新
|
||||
{
|
||||
@@ -170,8 +172,7 @@ fn test_draft_box() {
|
||||
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
|
||||
|
||||
// 7. 丢弃草稿
|
||||
assert!(draft.discard().is_some()); // 第一次丢弃返回 Some
|
||||
assert!(draft.discard().is_none()); // 再次丢弃返回 None
|
||||
draft.discard();
|
||||
|
||||
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone
|
||||
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false));
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde_json::Value;
|
||||
use std::{fs, path::PathBuf, sync::RwLock};
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use sys_locale;
|
||||
|
||||
const DEFAULT_LANGUAGE: &str = "zh";
|
||||
|
||||
type TranslationMap = (String, HashMap<String, Arc<str>>);
|
||||
|
||||
fn get_locales_dir() -> Option<PathBuf> {
|
||||
dirs::app_resources_dir()
|
||||
.map(|resource_path| resource_path.join("locales"))
|
||||
@@ -33,18 +40,33 @@ pub fn get_supported_languages() -> Vec<String> {
|
||||
languages
|
||||
}
|
||||
|
||||
static TRANSLATIONS: Lazy<RwLock<(String, Value)>> = Lazy::new(|| {
|
||||
pub async fn current_language() -> String {
|
||||
Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.language
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.unwrap_or_else(get_system_language)
|
||||
}
|
||||
|
||||
static TRANSLATIONS: Lazy<RwLock<TranslationMap>> = Lazy::new(|| {
|
||||
let lang = get_system_language();
|
||||
let json = load_lang_file(&lang).unwrap_or_else(|| Value::Object(Default::default()));
|
||||
RwLock::new((lang, json))
|
||||
let map = load_lang_file(&lang).unwrap_or_default();
|
||||
RwLock::new((lang, map))
|
||||
});
|
||||
|
||||
fn load_lang_file(lang: &str) -> Option<Value> {
|
||||
fn load_lang_file(lang: &str) -> Option<HashMap<String, Arc<str>>> {
|
||||
let locales_dir = get_locales_dir()?;
|
||||
let file_path = locales_dir.join(format!("{lang}.json"));
|
||||
fs::read_to_string(file_path)
|
||||
.ok()
|
||||
.and_then(|content| serde_json::from_str(&content).ok())
|
||||
.and_then(|content| serde_json::from_str::<HashMap<String, String>>(&content).ok())
|
||||
.map(|map| {
|
||||
map.into_iter()
|
||||
.map(|(k, v)| (k, Arc::from(v.as_str())))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn get_system_language() -> String {
|
||||
@@ -55,44 +77,38 @@ fn get_system_language() -> String {
|
||||
.unwrap_or_else(|| DEFAULT_LANGUAGE.into())
|
||||
}
|
||||
|
||||
pub async fn t(key: &str) -> String {
|
||||
let current_lang = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.language
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.unwrap_or_else(get_system_language);
|
||||
pub async fn t(key: &str) -> Arc<str> {
|
||||
let current_lang = current_language().await;
|
||||
|
||||
{
|
||||
if let Ok(cache) = TRANSLATIONS.read()
|
||||
&& cache.0 == current_lang
|
||||
&& let Some(text) = cache.1.get(key).and_then(|val| val.as_str())
|
||||
&& let Some(text) = cache.1.get(key)
|
||||
{
|
||||
return text.into();
|
||||
return Arc::clone(text);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_json) = load_lang_file(¤t_lang)
|
||||
if let Some(new_map) = load_lang_file(¤t_lang)
|
||||
&& let Ok(mut cache) = TRANSLATIONS.write()
|
||||
{
|
||||
*cache = (current_lang.clone(), new_json);
|
||||
*cache = (current_lang.clone(), new_map);
|
||||
|
||||
if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) {
|
||||
return text.into();
|
||||
if let Some(text) = cache.1.get(key) {
|
||||
return Arc::clone(text);
|
||||
}
|
||||
}
|
||||
|
||||
if current_lang != DEFAULT_LANGUAGE
|
||||
&& let Some(default_json) = load_lang_file(DEFAULT_LANGUAGE)
|
||||
&& let Some(default_map) = load_lang_file(DEFAULT_LANGUAGE)
|
||||
&& let Ok(mut cache) = TRANSLATIONS.write()
|
||||
{
|
||||
*cache = (DEFAULT_LANGUAGE.into(), default_json);
|
||||
*cache = (DEFAULT_LANGUAGE.into(), default_map);
|
||||
|
||||
if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) {
|
||||
return text.into();
|
||||
if let Some(text) = cache.1.get(key) {
|
||||
return Arc::clone(text);
|
||||
}
|
||||
}
|
||||
|
||||
key.into()
|
||||
Arc::from(key)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use crate::utils::logging::NoModuleFilter;
|
||||
use crate::{
|
||||
config::*,
|
||||
constants,
|
||||
core::handle,
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
@@ -304,7 +305,7 @@ async fn init_dns_config() -> Result<()> {
|
||||
|
||||
// 检查DNS配置文件是否存在
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let dns_path = app_dir.join("dns_config.yaml");
|
||||
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
||||
|
||||
if !dns_path.exists() {
|
||||
logging!(info, Type::Setup, "Creating default DNS config file");
|
||||
@@ -429,26 +430,8 @@ pub async fn init_resources() -> Result<()> {
|
||||
let src_path = res_dir.join(file);
|
||||
let dest_path = app_dir.join(file);
|
||||
|
||||
let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
|
||||
match fs::copy(&src, &dest).await {
|
||||
Ok(_) => {
|
||||
logging!(debug, Type::Setup, "resources copied '{}'", file);
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
"failed to copy resources '{}' to '{:?}', {}",
|
||||
file,
|
||||
dest,
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if src_path.exists() && !dest_path.exists() {
|
||||
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
|
||||
handle_copy(&src_path, &dest_path, file).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -458,12 +441,12 @@ pub async fn init_resources() -> Result<()> {
|
||||
match (src_modified, dest_modified) {
|
||||
(Ok(src_modified), Ok(dest_modified)) => {
|
||||
if src_modified > dest_modified {
|
||||
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
|
||||
handle_copy(&src_path, &dest_path, file).await;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
logging!(debug, Type::Setup, "failed to get modified '{}'", file);
|
||||
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
|
||||
handle_copy(&src_path, &dest_path, file).await;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -563,3 +546,21 @@ pub async fn startup_script() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_copy(src: &PathBuf, dest: &PathBuf, file: &str) {
|
||||
match fs::copy(src, dest).await {
|
||||
Ok(_) => {
|
||||
logging!(debug, Type::Setup, "resources copied '{}'", file);
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
"failed to copy resources '{}' to '{:?}', {}",
|
||||
file,
|
||||
dest,
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use tokio::sync::{Mutex, MutexGuard};
|
||||
|
||||
pub type SharedWriter = Arc<Mutex<FileLogWriter>>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Cmd,
|
||||
Core,
|
||||
@@ -29,7 +29,6 @@ pub enum Type {
|
||||
Lightweight,
|
||||
Network,
|
||||
ProxyMode,
|
||||
// Cache,
|
||||
Validate,
|
||||
ClashVergeRev,
|
||||
}
|
||||
@@ -53,7 +52,6 @@ impl fmt::Display for Type {
|
||||
Type::Lightweight => write!(f, "[Lightweight]"),
|
||||
Type::Network => write!(f, "[Network]"),
|
||||
Type::ProxyMode => write!(f, "[ProxMode]"),
|
||||
// Type::Cache => write!(f, "[Cache]"),
|
||||
Type::Validate => write!(f, "[Validate]"),
|
||||
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"),
|
||||
}
|
||||
@@ -82,15 +80,6 @@ macro_rules! log_err {
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace_err {
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// wrap the anyhow error
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::config::Config;
|
||||
use anyhow::Result;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use isahc::config::DnsCache;
|
||||
use isahc::prelude::*;
|
||||
use isahc::{HttpClient, config::SslOption};
|
||||
use isahc::{
|
||||
@@ -143,6 +144,12 @@ impl NetworkManager {
|
||||
|
||||
builder = builder.redirect_policy(RedirectPolicy::Follow);
|
||||
|
||||
// 禁用缓存,不关心连接复用
|
||||
builder = builder.connection_cache_size(0);
|
||||
|
||||
// 禁用 DNS 缓存,避免因 DNS 变化导致的问题
|
||||
builder = builder.dns_cache(DnsCache::Disable);
|
||||
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::utils::i18n::t;
|
||||
use crate::{core::handle, utils::i18n::t};
|
||||
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
|
||||
pub enum NotificationEvent<'a> {
|
||||
@@ -16,8 +15,10 @@ pub enum NotificationEvent<'a> {
|
||||
AppHidden,
|
||||
}
|
||||
|
||||
fn notify(app: &AppHandle, title: &str, body: &str) {
|
||||
app.notification()
|
||||
fn notify(title: &str, body: &str) {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle
|
||||
.notification()
|
||||
.builder()
|
||||
.title(title)
|
||||
.body(body)
|
||||
@@ -25,49 +26,44 @@ fn notify(app: &AppHandle, title: &str, body: &str) {
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub async fn notify_event<'a>(app: AppHandle, event: NotificationEvent<'a>) {
|
||||
pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
|
||||
match event {
|
||||
NotificationEvent::DashboardToggled => {
|
||||
notify(
|
||||
&app,
|
||||
&t("DashboardToggledTitle").await,
|
||||
&t("DashboardToggledBody").await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::ClashModeChanged { mode } => {
|
||||
notify(
|
||||
&app,
|
||||
&t("ClashModeChangedTitle").await,
|
||||
&t_with_args("ClashModeChangedBody", mode).await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::SystemProxyToggled => {
|
||||
notify(
|
||||
&app,
|
||||
&t("SystemProxyToggledTitle").await,
|
||||
&t("SystemProxyToggledBody").await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::TunModeToggled => {
|
||||
notify(
|
||||
&app,
|
||||
&t("TunModeToggledTitle").await,
|
||||
&t("TunModeToggledBody").await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::LightweightModeEntered => {
|
||||
notify(
|
||||
&app,
|
||||
&t("LightweightModeEnteredTitle").await,
|
||||
&t("LightweightModeEnteredBody").await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::AppQuit => {
|
||||
notify(&app, &t("AppQuitTitle").await, &t("AppQuitBody").await);
|
||||
notify(&t("AppQuitTitle").await, &t("AppQuitBody").await);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
NotificationEvent::AppHidden => {
|
||||
notify(&app, &t("AppHiddenTitle").await, &t("AppHiddenBody").await);
|
||||
notify(&t("AppHiddenTitle").await, &t("AppHiddenBody").await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::{logging, utils::logging::Type};
|
||||
pub async fn set_public_dns(dns_server: String) {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use crate::utils::logging::Type;
|
||||
use crate::{core::handle, logging, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
log::info!(target: "app", "try to set system dns");
|
||||
logging!(info, Type::Config, "try to set system dns");
|
||||
let resource_dir = match dirs::app_resources_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get resource directory: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to get resource directory: {}",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let script = resource_dir.join("set_dns.sh");
|
||||
if !script.exists() {
|
||||
log::error!(target: "app", "set_dns.sh not found");
|
||||
logging!(error, Type::Config, "set_dns.sh not found");
|
||||
return;
|
||||
}
|
||||
let script = script.to_string_lossy().into_owned();
|
||||
@@ -28,14 +35,14 @@ pub async fn set_public_dns(dns_server: String) {
|
||||
{
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
log::info!(target: "app", "set system dns successfully");
|
||||
logging!(info, Type::Config, "set system dns successfully");
|
||||
} else {
|
||||
let code = status.code().unwrap_or(-1);
|
||||
log::error!(target: "app", "set system dns failed: {code}");
|
||||
logging!(error, Type::Config, "set system dns failed: {code}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "set system dns failed: {err}");
|
||||
logging!(error, Type::Config, "set system dns failed: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,17 +52,22 @@ pub async fn restore_public_dns() {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
log::info!(target: "app", "try to unset system dns");
|
||||
logging!(info, Type::Config, "try to unset system dns");
|
||||
let resource_dir = match dirs::app_resources_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get resource directory: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to get resource directory: {}",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let script = resource_dir.join("unset_dns.sh");
|
||||
if !script.exists() {
|
||||
log::error!(target: "app", "unset_dns.sh not found");
|
||||
logging!(error, Type::Config, "unset_dns.sh not found");
|
||||
return;
|
||||
}
|
||||
let script = script.to_string_lossy().into_owned();
|
||||
@@ -69,14 +81,14 @@ pub async fn restore_public_dns() {
|
||||
{
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
log::info!(target: "app", "unset system dns successfully");
|
||||
logging!(info, Type::Config, "unset system dns successfully");
|
||||
} else {
|
||||
let code = status.code().unwrap_or(-1);
|
||||
log::error!(target: "app", "unset system dns failed: {code}");
|
||||
logging!(error, Type::Config, "unset system dns failed: {code}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "unset system dns failed: {err}");
|
||||
logging!(error, Type::Config, "unset system dns failed: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use smartstring::alias::String;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
@@ -11,10 +10,7 @@ use crate::{
|
||||
tray::Tray,
|
||||
},
|
||||
logging, logging_error,
|
||||
module::{
|
||||
lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight},
|
||||
signal,
|
||||
},
|
||||
module::{lightweight::auto_lightweight_boot, signal},
|
||||
process::AsyncHandler,
|
||||
utils::{init, logging::Type, server, window_manager::WindowManager},
|
||||
};
|
||||
@@ -71,8 +67,7 @@ pub fn resolve_setup_async() {
|
||||
tray_init,
|
||||
init_timer(),
|
||||
init_hotkey(),
|
||||
init_auto_lightweight_mode(),
|
||||
init_once_auto_lightweight(),
|
||||
init_auto_lightweight_boot(),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -103,7 +98,7 @@ pub(super) async fn resolve_setup_logger() {
|
||||
logging_error!(Type::Setup, init::init_logger().await);
|
||||
}
|
||||
|
||||
pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
pub async fn resolve_scheme(param: &str) -> Result<()> {
|
||||
logging_error!(Type::Setup, scheme::resolve_scheme(param).await);
|
||||
Ok(())
|
||||
}
|
||||
@@ -128,12 +123,8 @@ pub(super) async fn init_hotkey() {
|
||||
logging_error!(Type::Setup, Hotkey::global().init().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_once_auto_lightweight() {
|
||||
run_once_auto_lightweight().await;
|
||||
}
|
||||
|
||||
pub(super) async fn init_auto_lightweight_mode() {
|
||||
logging_error!(Type::Setup, auto_lightweight_mode_init().await);
|
||||
pub(super) async fn init_auto_lightweight_boot() {
|
||||
logging_error!(Type::Setup, auto_lightweight_boot().await);
|
||||
}
|
||||
|
||||
pub(super) fn init_signal() {
|
||||
|
||||
@@ -3,17 +3,22 @@ use percent_encoding::percent_decode_str;
|
||||
use smartstring::alias::String;
|
||||
use tauri::Url;
|
||||
|
||||
use crate::{config::PrfItem, core::handle, logging, logging_error, utils::logging::Type};
|
||||
use crate::{
|
||||
config::{PrfItem, profiles},
|
||||
core::handle,
|
||||
logging, logging_error,
|
||||
utils::logging::Type,
|
||||
};
|
||||
|
||||
pub(super) async fn resolve_scheme(param: String) -> Result<()> {
|
||||
log::info!(target:"app", "received deep link: {param}");
|
||||
pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
|
||||
logging!(info, Type::Config, "received deep link: {param}");
|
||||
|
||||
let param_str = if param.starts_with("[") && param.len() > 4 {
|
||||
param
|
||||
.get(2..param.len() - 2)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))?
|
||||
} else {
|
||||
param.as_str()
|
||||
param
|
||||
};
|
||||
|
||||
// 解析 URL
|
||||
@@ -25,10 +30,11 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
|
||||
};
|
||||
|
||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
||||
let name = link_parsed
|
||||
let name_owned: Option<String> = link_parsed
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.into());
|
||||
.map(|(_, value)| value.into_owned().into());
|
||||
let name = name_owned.as_ref();
|
||||
|
||||
let url_param = if let Some(query) = link_parsed.query() {
|
||||
let prefix = "url=";
|
||||
@@ -43,10 +49,10 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
|
||||
};
|
||||
|
||||
match url_param {
|
||||
Some(url) => {
|
||||
log::info!(target:"app", "decoded subscription url: {url}");
|
||||
Some(ref url) => {
|
||||
logging!(info, Type::Config, "decoded subscription url: {url}");
|
||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
||||
Ok(item) => {
|
||||
Ok(mut item) => {
|
||||
let uid = match item.uid.clone() {
|
||||
Some(uid) => uid,
|
||||
None => {
|
||||
@@ -58,7 +64,7 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let result = crate::config::profiles::profiles_append_item_safe(item).await;
|
||||
let result = profiles::profiles_append_item_safe(&mut item).await;
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
"failed to import subscription url: {:?}",
|
||||
|
||||
@@ -21,16 +21,14 @@ const MINIMAL_HEIGHT: f64 = 520.0;
|
||||
pub async fn build_new_window() -> Result<WebviewWindow, String> {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
|
||||
let start_page = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.start_page
|
||||
.clone()
|
||||
.unwrap_or_else(|| "/".into());
|
||||
let config = Config::verge().await;
|
||||
let latest = config.latest_ref();
|
||||
let start_page = latest.start_page.as_deref().unwrap_or("/");
|
||||
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
app_handle,
|
||||
"main", /* the unique window label */
|
||||
tauri::WebviewUrl::App(start_page.as_str().into()),
|
||||
tauri::WebviewUrl::App(start_page.into()),
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.center()
|
||||
|
||||
@@ -51,7 +51,11 @@ pub async fn check_singleton() -> Result<()> {
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
log::error!("failed to setup singleton listen server");
|
||||
logging!(
|
||||
error,
|
||||
Type::Window,
|
||||
"failed to setup singleton listen server"
|
||||
);
|
||||
bail!("app exists");
|
||||
}
|
||||
Ok(())
|
||||
@@ -107,9 +111,8 @@ pub fn embed_server() {
|
||||
let scheme = warp::path!("commands" / "scheme")
|
||||
.and(warp::query::<QueryParam>())
|
||||
.map(|query: QueryParam| {
|
||||
let param = query.param.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
logging_error!(Type::Setup, resolve::resolve_scheme(param).await);
|
||||
logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await);
|
||||
});
|
||||
warp::reply::with_status::<std::string::String>(
|
||||
"ok".to_string(),
|
||||
@@ -130,7 +133,7 @@ pub fn embed_server() {
|
||||
}
|
||||
|
||||
pub fn shutdown_embedded_server() {
|
||||
log::info!("shutting down embedded server");
|
||||
logging!(info, Type::Window, "shutting down embedded server");
|
||||
if let Some(sender) = SHUTDOWN_SENDER.get()
|
||||
&& let Some(sender) = sender.lock().take()
|
||||
{
|
||||
|
||||
@@ -58,7 +58,11 @@ fn get_window_operation_debounce() -> &'static Mutex<Instant> {
|
||||
|
||||
fn should_handle_window_operation() -> bool {
|
||||
if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) {
|
||||
log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
"Warning: [防抖] 窗口操作已在进行中,跳过重复调用"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -67,17 +71,27 @@ fn should_handle_window_operation() -> bool {
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(*last_operation);
|
||||
|
||||
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
|
||||
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
"[防抖] 检查窗口操作间隔: {}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", "[防抖] 窗口操作被允许执行");
|
||||
logging!(info, Type::Window, "[防抖] 窗口操作被允许执行");
|
||||
true
|
||||
} else {
|
||||
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
|
||||
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
"Warning: [防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
|
||||
elapsed.as_millis(),
|
||||
WINDOW_OPERATION_DEBOUNCE_MS
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -359,7 +373,6 @@ impl WindowManager {
|
||||
}
|
||||
return WindowOperationResult::Destroyed;
|
||||
}
|
||||
logging!(warn, Type::Window, "窗口摧毁失败");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
|
||||
|
||||
@@ -506,7 +506,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
// 导航到代理页面
|
||||
const goToProxies = useCallback(() => {
|
||||
navigate("/");
|
||||
navigate("/proxies");
|
||||
}, [navigate]);
|
||||
|
||||
// 获取要显示的代理节点
|
||||
|
||||
@@ -53,10 +53,13 @@ export const useCustomTheme = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
const preferBrowserMatchMedia =
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.matchMedia === "function"
|
||||
) {
|
||||
typeof window.matchMedia === "function" &&
|
||||
// Skip Tauri flow when running purely in browser.
|
||||
!("__TAURI__" in window);
|
||||
|
||||
if (preferBrowserMatchMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -341,7 +341,8 @@ export const ProfileItem = (props: Props) => {
|
||||
|
||||
try {
|
||||
// 调用后端更新(后端会自动处理回退逻辑)
|
||||
await updateProfile(itemData.uid, option);
|
||||
const payload = Object.keys(option).length > 0 ? option : undefined;
|
||||
await updateProfile(itemData.uid, payload);
|
||||
|
||||
// 更新成功,刷新列表
|
||||
mutate("getProfiles");
|
||||
|
||||
@@ -307,7 +307,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
</GuardState>
|
||||
</Item>
|
||||
)} */}
|
||||
{OS === "macos" && (
|
||||
{/* {OS === "macos" && (
|
||||
<Item>
|
||||
<ListItemText primary={t("Enable Tray Icon")} />
|
||||
<GuardState
|
||||
@@ -326,7 +326,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</Item>
|
||||
)}
|
||||
)} */}
|
||||
<Item>
|
||||
<ListItemText primary={t("Show Proxy Groups Inline")} />
|
||||
<GuardState
|
||||
|
||||
@@ -117,13 +117,8 @@ const ProxyControlSwitches = ({
|
||||
const { uninstallServiceAndRestartCore } = useServiceUninstaller();
|
||||
const { actualState: systemProxyActualState, toggleSystemProxy } =
|
||||
useSystemProxyState();
|
||||
const {
|
||||
isServiceMode,
|
||||
isTunModeAvailable,
|
||||
mutateRunningMode,
|
||||
mutateServiceOk,
|
||||
mutateTunModeAvailable,
|
||||
} = useSystemState();
|
||||
const { isServiceOk, isTunModeAvailable, mutateSystemState } =
|
||||
useSystemState();
|
||||
|
||||
const sysproxyRef = useRef<DialogRef>(null);
|
||||
const tunRef = useRef<DialogRef>(null);
|
||||
@@ -148,9 +143,7 @@ const ProxyControlSwitches = ({
|
||||
const onInstallService = useLockFn(async () => {
|
||||
try {
|
||||
await installServiceAndRestartCore();
|
||||
await mutateRunningMode();
|
||||
await mutateServiceOk();
|
||||
await mutateTunModeAvailable();
|
||||
await mutateSystemState();
|
||||
} catch (err) {
|
||||
showNotice("error", (err as Error).message || String(err));
|
||||
}
|
||||
@@ -158,11 +151,11 @@ const ProxyControlSwitches = ({
|
||||
|
||||
const onUninstallService = useLockFn(async () => {
|
||||
try {
|
||||
await handleTunToggle(false);
|
||||
if (verge?.enable_tun_mode) {
|
||||
await handleTunToggle(false);
|
||||
}
|
||||
await uninstallServiceAndRestartCore();
|
||||
await mutateRunningMode();
|
||||
await mutateServiceOk();
|
||||
await mutateTunModeAvailable();
|
||||
await mutateSystemState();
|
||||
} catch (err) {
|
||||
showNotice("error", (err as Error).message || String(err));
|
||||
}
|
||||
@@ -198,22 +191,22 @@ const ProxyControlSwitches = ({
|
||||
extraIcons={
|
||||
<>
|
||||
{!isTunModeAvailable && (
|
||||
<TooltipIcon
|
||||
title={t("TUN requires Service Mode or Admin Mode")}
|
||||
icon={WarningRounded}
|
||||
sx={{ color: "warning.main", ml: 1 }}
|
||||
/>
|
||||
<>
|
||||
<TooltipIcon
|
||||
title={t("TUN requires Service Mode or Admin Mode")}
|
||||
icon={WarningRounded}
|
||||
sx={{ color: "warning.main", ml: 1 }}
|
||||
/>
|
||||
<TooltipIcon
|
||||
title={t("Install Service")}
|
||||
icon={BuildRounded}
|
||||
color="primary"
|
||||
onClick={onInstallService}
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!isTunModeAvailable && (
|
||||
<TooltipIcon
|
||||
title={t("Install Service")}
|
||||
icon={BuildRounded}
|
||||
color="primary"
|
||||
onClick={onInstallService}
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
)}
|
||||
{isServiceMode && (
|
||||
{isServiceOk && (
|
||||
<TooltipIcon
|
||||
title={t("Uninstall Service")}
|
||||
icon={DeleteForeverRounded}
|
||||
|
||||
@@ -62,7 +62,9 @@ export const useProfiles = () => {
|
||||
const patchCurrent = async (value: Partial<IProfileItem>) => {
|
||||
if (profiles?.current) {
|
||||
await patchProfile(profiles.current, value);
|
||||
mutateProfiles();
|
||||
if (!value.selected) {
|
||||
mutateProfiles();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,73 +1,99 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
import { useVerge } from "./use-verge";
|
||||
|
||||
export interface SystemState {
|
||||
runningMode: "Sidecar" | "Service";
|
||||
isAdminMode: boolean;
|
||||
isServiceOk: boolean;
|
||||
}
|
||||
|
||||
const defaultSystemState = {
|
||||
runningMode: "Sidecar",
|
||||
isAdminMode: false,
|
||||
isServiceOk: false,
|
||||
} as SystemState;
|
||||
|
||||
let disablingTunMode = false;
|
||||
|
||||
/**
|
||||
* 自定义 hook 用于获取系统运行状态
|
||||
* 包括运行模式、管理员状态、系统服务是否可用
|
||||
*/
|
||||
export function useSystemState() {
|
||||
// 获取运行模式
|
||||
const {
|
||||
data: runningMode = "Sidecar",
|
||||
mutate: mutateRunningMode,
|
||||
isLoading: runningModeLoading,
|
||||
} = useSWR("getRunningMode", getRunningMode, {
|
||||
suspense: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const isSidecarMode = runningMode === "Sidecar";
|
||||
const isServiceMode = runningMode === "Service";
|
||||
const { t } = useTranslation();
|
||||
const { verge, patchVerge } = useVerge();
|
||||
|
||||
// 获取管理员状态
|
||||
const { data: isAdminMode = false, isLoading: isAdminLoading } = useSWR(
|
||||
"isAdmin",
|
||||
isAdmin,
|
||||
const {
|
||||
data: systemState,
|
||||
mutate: mutateSystemState,
|
||||
isLoading,
|
||||
} = useSWR(
|
||||
"getSystemState",
|
||||
async () => {
|
||||
const [runningMode, isAdminMode, isServiceOk] = await Promise.all([
|
||||
getRunningMode(),
|
||||
isAdmin(),
|
||||
isServiceAvailable(),
|
||||
]);
|
||||
return { runningMode, isAdminMode, isServiceOk } as SystemState;
|
||||
},
|
||||
{
|
||||
suspense: false,
|
||||
revalidateOnFocus: false,
|
||||
suspense: true,
|
||||
refreshInterval: 30000,
|
||||
fallback: defaultSystemState,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: isServiceOk = false,
|
||||
mutate: mutateServiceOk,
|
||||
isLoading: isServiceLoading,
|
||||
} = useSWR(isServiceMode ? "isServiceAvailable" : null, isServiceAvailable, {
|
||||
suspense: false,
|
||||
revalidateOnFocus: false,
|
||||
onSuccess: (data) => {
|
||||
console.log("[useSystemState] 服务状态更新:", data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("[useSystemState] 服务状态检查失败:", error);
|
||||
},
|
||||
// isPaused: () => !isServiceMode, // 仅在非 Service 模式下暂停请求
|
||||
});
|
||||
const isSidecarMode = systemState.runningMode === "Sidecar";
|
||||
const isServiceMode = systemState.runningMode === "Service";
|
||||
const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk;
|
||||
|
||||
const isLoading =
|
||||
runningModeLoading || isAdminLoading || (isServiceMode && isServiceLoading);
|
||||
const enable_tun_mode = verge?.enable_tun_mode;
|
||||
useEffect(() => {
|
||||
if (enable_tun_mode === undefined) return;
|
||||
|
||||
const { data: isTunModeAvailable = false, mutate: mutateTunModeAvailable } =
|
||||
useSWR(
|
||||
["isTunModeAvailable", isAdminMode, isServiceOk],
|
||||
() => isAdminMode || isServiceOk,
|
||||
{
|
||||
suspense: false,
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
if (
|
||||
!disablingTunMode &&
|
||||
enable_tun_mode &&
|
||||
!isTunModeAvailable &&
|
||||
!isLoading
|
||||
) {
|
||||
disablingTunMode = true;
|
||||
patchVerge({ enable_tun_mode: false })
|
||||
.then(() => {
|
||||
showNotice(
|
||||
"info",
|
||||
t("TUN Mode automatically disabled due to service unavailable"),
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("[useVerge] 自动关闭TUN模式失败:", err);
|
||||
showNotice("error", t("Failed to disable TUN Mode automatically"));
|
||||
})
|
||||
.finally(() => {
|
||||
const tid = setTimeout(() => {
|
||||
// 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式
|
||||
disablingTunMode = false;
|
||||
clearTimeout(tid);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
}, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading, t]);
|
||||
|
||||
return {
|
||||
runningMode,
|
||||
isAdminMode,
|
||||
runningMode: systemState.runningMode,
|
||||
isAdminMode: systemState.isAdminMode,
|
||||
isServiceOk: systemState.isServiceOk,
|
||||
isSidecarMode,
|
||||
isServiceMode,
|
||||
isServiceOk,
|
||||
isTunModeAvailable,
|
||||
mutateRunningMode,
|
||||
mutateServiceOk,
|
||||
mutateTunModeAvailable,
|
||||
mutateSystemState,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useSystemState } from "@/hooks/use-system-state";
|
||||
import { getVergeConfig, patchVergeConfig } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
export const useVerge = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isTunModeAvailable, isServiceMode, isLoading } = useSystemState();
|
||||
const disablingRef = useRef(false);
|
||||
|
||||
const { data: verge, mutate: mutateVerge } = useSWR(
|
||||
"getVergeConfig",
|
||||
async () => {
|
||||
@@ -24,53 +16,6 @@ export const useVerge = () => {
|
||||
mutateVerge();
|
||||
};
|
||||
|
||||
const { enable_tun_mode } = verge ?? {};
|
||||
|
||||
const mutateVergeRef = useRef(mutateVerge);
|
||||
const tRef = useRef(t);
|
||||
const enableTunRef = useRef(enable_tun_mode);
|
||||
const isLoadingRef = useRef(isLoading);
|
||||
const isServiceModeRef = useRef(isServiceMode);
|
||||
|
||||
mutateVergeRef.current = mutateVerge;
|
||||
tRef.current = t;
|
||||
enableTunRef.current = enable_tun_mode;
|
||||
isLoadingRef.current = isLoading;
|
||||
isServiceModeRef.current = isServiceMode;
|
||||
|
||||
const doDisable = useCallback(async () => {
|
||||
try {
|
||||
if (isServiceModeRef.current === true) return;
|
||||
await patchVergeConfig({ enable_tun_mode: false });
|
||||
await mutateVergeRef.current?.();
|
||||
showNotice(
|
||||
"info",
|
||||
tRef.current(
|
||||
"TUN Mode automatically disabled due to service unavailable",
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[useVerge] 自动关闭TUN模式失败:", err);
|
||||
showNotice(
|
||||
"error",
|
||||
tRef.current("Failed to disable TUN Mode automatically"),
|
||||
);
|
||||
} finally {
|
||||
disablingRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTunModeAvailable === true) return;
|
||||
if (isLoadingRef.current === true) return;
|
||||
if (enableTunRef.current !== true) return;
|
||||
if (isServiceModeRef.current === true) return;
|
||||
if (disablingRef.current) return;
|
||||
|
||||
disablingRef.current = true;
|
||||
void doDisable();
|
||||
}, [isTunModeAvailable, doDisable]);
|
||||
|
||||
return {
|
||||
verge,
|
||||
mutateVerge,
|
||||
|
||||
@@ -25,7 +25,7 @@ const executeWithErrorHandling = async (
|
||||
};
|
||||
|
||||
export const useServiceInstaller = () => {
|
||||
const { mutateRunningMode, mutateServiceOk } = useSystemState();
|
||||
const { mutateSystemState } = useSystemState();
|
||||
|
||||
const installServiceAndRestartCore = useCallback(async () => {
|
||||
await executeWithErrorHandling(
|
||||
@@ -34,9 +34,13 @@ export const useServiceInstaller = () => {
|
||||
"Service Installed Successfully",
|
||||
);
|
||||
|
||||
await executeWithErrorHandling(() => restartCore(), "Restarting Core...");
|
||||
await mutateRunningMode();
|
||||
await mutateServiceOk();
|
||||
}, [mutateRunningMode, mutateServiceOk]);
|
||||
await executeWithErrorHandling(
|
||||
() => restartCore(),
|
||||
"Restarting Core...",
|
||||
"Clash Core Restarted",
|
||||
);
|
||||
|
||||
await mutateSystemState();
|
||||
}, [mutateSystemState]);
|
||||
return { installServiceAndRestartCore };
|
||||
};
|
||||
|
||||
@@ -25,21 +25,26 @@ const executeWithErrorHandling = async (
|
||||
};
|
||||
|
||||
export const useServiceUninstaller = () => {
|
||||
const { mutateRunningMode, mutateServiceOk } = useSystemState();
|
||||
const { mutateSystemState } = useSystemState();
|
||||
|
||||
const uninstallServiceAndRestartCore = useCallback(async () => {
|
||||
await executeWithErrorHandling(() => stopCore(), "Stopping Core...");
|
||||
|
||||
await executeWithErrorHandling(
|
||||
() => uninstallService(),
|
||||
"Uninstalling Service...",
|
||||
"Service Uninstalled Successfully",
|
||||
);
|
||||
|
||||
await executeWithErrorHandling(() => restartCore(), "Restarting Core...");
|
||||
await mutateRunningMode();
|
||||
await mutateServiceOk();
|
||||
}, [mutateRunningMode, mutateServiceOk]);
|
||||
try {
|
||||
await executeWithErrorHandling(() => stopCore(), "Stopping Core...");
|
||||
await executeWithErrorHandling(
|
||||
() => uninstallService(),
|
||||
"Uninstalling Service...",
|
||||
"Service Uninstalled Successfully",
|
||||
);
|
||||
} catch (ignore) {
|
||||
} finally {
|
||||
await executeWithErrorHandling(
|
||||
() => restartCore(),
|
||||
"Restarting Core...",
|
||||
"Clash Core Restarted",
|
||||
);
|
||||
await mutateSystemState();
|
||||
}
|
||||
}, [mutateSystemState]);
|
||||
|
||||
return { uninstallServiceAndRestartCore };
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"Label-Connections": "الاتصالات",
|
||||
"Label-Rules": "القواعد",
|
||||
"Label-Logs": "السجلات",
|
||||
"Label-Test": "اختبار",
|
||||
"Label-Settings": "الإعدادات",
|
||||
"Proxies": "الوكلاء",
|
||||
"Proxy Groups": "مجموعات الوكلاء",
|
||||
@@ -166,7 +165,6 @@
|
||||
"Table View": "عرض الجدول",
|
||||
"List View": "عرض القائمة",
|
||||
"Close All": "إغلاق الكل",
|
||||
"Default": "افتراضي",
|
||||
"Download Speed": "سرعة التنزيل",
|
||||
"Upload Speed": "سرعة الرفع",
|
||||
"Host": "المضيف",
|
||||
@@ -197,6 +195,8 @@
|
||||
"Settings": "الإعدادات",
|
||||
"System Setting": "إعدادات النظام",
|
||||
"Tun Mode": "وضع TUN",
|
||||
"TUN requires Service Mode": "يتطلب وضع TUN خدمة",
|
||||
"Install Service": "تثبيت الخدمة ",
|
||||
"Reset to Default": "إعادة تعيين إلى الافتراضي",
|
||||
"Tun Mode Info": "وضع TUN (بطاقة شبكة افتراضية): يلتقط كل حركة المرور في النظام. عند تمكينه، لا حاجة لتفعيل وكيل النظام.",
|
||||
"Stack": "مكدس TUN",
|
||||
@@ -279,7 +279,8 @@
|
||||
"Open UWP tool": "فتح أداة UWP",
|
||||
"Open UWP tool Info": "منذ نظام ويندوز 8، يتم تقييد تطبيقات UWP من الوصول المباشر إلى المضيف المحلي. هذه الأداة تتيح تجاوز هذا التقييد",
|
||||
"Update GeoData": "تحديث البيانات الجغرافية",
|
||||
"Verge Setting": "إعدادات Verge",
|
||||
"Verge Basic Setting": "الإعدادات الأساسية Verge",
|
||||
"Verge Advanced Setting": "الإعدادات الأساسية Verge",
|
||||
"Language": "اللغة",
|
||||
"Theme Mode": "وضع السمة",
|
||||
"theme.light": "سمة فاتحة",
|
||||
@@ -365,6 +366,7 @@
|
||||
"Profile Reactivated": "تم إعادة تنشيط الملف الشخصي",
|
||||
"Only YAML Files Supported": "لا يتم دعم سوى ملفات YAML",
|
||||
"Settings Applied": "تم تطبيق الإعدادات",
|
||||
"Installing Service...": "جاري تثبيت الخدمة...",
|
||||
"Service Installed Successfully": "تم تثبيت الخدمة بنجاح",
|
||||
"Service Uninstalled Successfully": "تم إلغاء تثبيت الخدمة بنجاح",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "لا يمكن أن تقل مدة خادم الوكيل عن ثانية واحدة",
|
||||
@@ -407,19 +409,12 @@
|
||||
"Help": "مساعدة",
|
||||
"About": "حول",
|
||||
"Theme": "السمة",
|
||||
"TUN Mode": "وضع TUN",
|
||||
"Main Window": "النافذة الرئيسية",
|
||||
"Group Icon": "أيقونة المجموعة",
|
||||
"Menu Icon": "أيقونة القائمة",
|
||||
"System Proxy Bypass": "تخطي وكيل النظام",
|
||||
"PAC File": "ملف PAC",
|
||||
"Web UI": "واجهة الويب",
|
||||
"Hotkeys": "اختصارات لوحة المفاتيح",
|
||||
"Auto Close Connection": "إغلاق الاتصال تلقائيًا",
|
||||
"Enable Built-in Enhanced": "تفعيل التحسين المدمج",
|
||||
"Proxy Layout Column": "عمود عرض الوكيل",
|
||||
"Test List": "قائمة الاختبارات",
|
||||
"Enable Random Port": "تفعيل المنفذ العشوائي",
|
||||
"Verge Mixed Port": "منفذ Verge المختلط",
|
||||
"Verge Socks Port": "منفذ Verge SOCKS",
|
||||
"Verge Redir Port": "منفذ إعادة التوجيه لـ Verge",
|
||||
@@ -429,15 +424,16 @@
|
||||
"WebDAV URL": "رابط WebDAV",
|
||||
"WebDAV Username": "اسم المستخدم لـ WebDAV",
|
||||
"WebDAV Password": "كلمة مرور WebDAV",
|
||||
"Dashboard": "لوحة التحكم",
|
||||
"Restart App": "إعادة تشغيل التطبيق",
|
||||
"Restart Clash Core": "إعادة تشغيل نواة Clash",
|
||||
"TUN Mode": "وضع TUN",
|
||||
"Copy Env": "نسخ البيئة",
|
||||
"Conf Dir": "مجلد الإعدادات",
|
||||
"Core Dir": "مجلد النواة",
|
||||
"Logs Dir": "مجلد السجلات",
|
||||
"Open Dir": "فتح المجلد",
|
||||
"Restart Clash Core": "إعادة تشغيل نواة Clash",
|
||||
"Restart App": "إعادة تشغيل التطبيق",
|
||||
"More": "المزيد",
|
||||
"Dashboard": "لوحة التحكم",
|
||||
"Rule Mode": "وضع القواعد",
|
||||
"Global Mode": "الوضع العالمي",
|
||||
"Direct Mode": "الوضع المباشر",
|
||||
@@ -454,10 +450,14 @@
|
||||
"Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات",
|
||||
"Core Changed Successfully": "تم تغيير النواة بنجاح",
|
||||
"Failed to Change Core": "فشل تغيير النواة",
|
||||
"Verge Basic Setting": "الإعدادات الأساسية Verge",
|
||||
"Verge Advanced Setting": "الإعدادات الأساسية Verge",
|
||||
"TUN requires Service Mode": "يتطلب وضع TUN خدمة",
|
||||
"Install Service": "تثبيت الخدمة ",
|
||||
"Installing Service...": "جاري تثبيت الخدمة...",
|
||||
"Service Administrator Prompt": "يتطلب Clash Verge امتيازات المسؤول لإعادة تثبيت خدمة النظام"
|
||||
"Service Administrator Prompt": "يتطلب Clash Verge امتيازات المسؤول لإعادة تثبيت خدمة النظام",
|
||||
"Auto Close Connection": "إغلاق الاتصال تلقائيًا",
|
||||
"Default": "افتراضي",
|
||||
"Enable Built-in Enhanced": "تفعيل التحسين المدمج",
|
||||
"Enable Random Port": "تفعيل المنفذ العشوائي",
|
||||
"Label-Test": "اختبار",
|
||||
"Proxy Layout Column": "عمود عرض الوكيل",
|
||||
"System Proxy Bypass": "تخطي وكيل النظام",
|
||||
"Test List": "قائمة الاختبارات",
|
||||
"Verge Setting": "إعدادات Verge"
|
||||
}
|
||||
|
||||
@@ -195,10 +195,11 @@
|
||||
"Settings": "Einstellungen",
|
||||
"System Setting": "Systemeinstellungen",
|
||||
"Tun Mode": "Virtual Network Interface-Modus",
|
||||
"TUN requires Service Mode or Admin Mode": "TUN-Modus erfordert Service-Modus oder Administrator-Modus",
|
||||
"Install Service": "Service installieren",
|
||||
"Uninstall Service": "Dienst deinstallieren",
|
||||
"Reset to Default": "Auf Standardwerte zurücksetzen",
|
||||
"Tun Mode Info": "Der TUN-Modus (Virtual Network Interface) übernimmt den gesamten Systemverkehr. Wenn dieser Modus aktiviert ist, muss der Systemproxy nicht geöffnet werden.",
|
||||
"TUN requires Service Mode or Admin Mode": "TUN-Modus erfordert Service-Modus oder Administrator-Modus",
|
||||
"System Proxy Enabled": "Der Systemproxy ist aktiviert. Ihre Anwendungen werden über den Proxy auf das Netzwerk zugreifen.",
|
||||
"System Proxy Disabled": "Der Systemproxy ist deaktiviert. Es wird empfohlen, diesen Eintrag für die meisten Benutzer zu aktivieren.",
|
||||
"TUN Mode Enabled": "Der TUN-Modus ist aktiviert. Die Anwendungen werden über die virtuelle Netzwerkschnittstelle auf das Netzwerk zugreifen.",
|
||||
@@ -254,6 +255,7 @@
|
||||
"Unified Delay Info": "Wenn die einheitliche Latenz aktiviert ist, werden zwei Latenztests durchgeführt, um die Latenzunterschiede zwischen verschiedenen Knotentypen aufgrund von Verbindungsaufbau und anderen Faktoren zu eliminieren.",
|
||||
"Log Level": "Protokolliergrad",
|
||||
"Log Level Info": "Dies wirkt sich nur auf die Kernprotokolldateien im Verzeichnis Service im Protokollverzeichnis aus.",
|
||||
"Port Config": "Port-Konfiguration",
|
||||
"Random Port": "Zufälliger Port",
|
||||
"Mixed Port": "Mischter Proxy-Port",
|
||||
"Socks Port": "SOCKS-Proxy-Port",
|
||||
@@ -367,17 +369,16 @@
|
||||
"Stopping Core...": "Kern wird gestoppt...",
|
||||
"Restarting Core...": "Kern wird neu gestartet...",
|
||||
"Installing Service...": "Service wird installiert...",
|
||||
"Uninstall Service": "Dienst deinstallieren",
|
||||
"Service Installed Successfully": "Service erfolgreich installiert",
|
||||
"Service is ready and core restarted": "Service ist bereit und Kern wurde neu gestartet",
|
||||
"Core restarted. Service is now available.": "Kern wurde neu gestartet. Service ist jetzt verfügbar",
|
||||
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "Der Dienst war bereit, aber beim Neustart des Kerns könnten Probleme aufgetreten sein oder der Dienst ist möglicherweise nicht verfügbar. Bitte überprüfen Sie dies.",
|
||||
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Bei der Dienstinstallation oder dem Neustart des Kerns sind Probleme aufgetreten. Der Dienst ist möglicherweise nicht verfügbar. Bitte prüfen Sie die Systemprotokolle.",
|
||||
"Uninstalling Service...": "Service wird deinstalliert...",
|
||||
"Waiting for service to be ready...": "Auf Service-Bereitschaft gewartet...",
|
||||
"Service Installed Successfully": "Service erfolgreich installiert",
|
||||
"Service Uninstalled Successfully": "Service erfolgreich deinstalliert",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "Das Intervall des Proxy-Daemons darf nicht weniger als 1 Sekunde betragen.",
|
||||
"Invalid Bypass Format": "Ungültiges Format für die Proxy-Umgehung",
|
||||
"Waiting for service to be ready...": "Auf Service-Bereitschaft gewartet...",
|
||||
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "Der Dienst war bereit, aber beim Neustart des Kerns könnten Probleme aufgetreten sein oder der Dienst ist möglicherweise nicht verfügbar. Bitte überprüfen Sie dies.",
|
||||
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Bei der Dienstinstallation oder dem Neustart des Kerns sind Probleme aufgetreten. Der Dienst ist möglicherweise nicht verfügbar. Bitte prüfen Sie die Systemprotokolle.",
|
||||
"Service is ready and core restarted": "Service ist bereit und Kern wurde neu gestartet",
|
||||
"Core restarted. Service is now available.": "Kern wurde neu gestartet. Service ist jetzt verfügbar",
|
||||
"Core Version Updated": "Kernversion wurde aktualisiert",
|
||||
"Clash Core Restarted": "Clash-Kern wurde neu gestartet",
|
||||
"GeoData Updated": "Geo-Daten wurden aktualisiert",
|
||||
@@ -523,7 +524,6 @@
|
||||
"Unknown": "Unbekannt",
|
||||
"Auto update disabled": "Automatische Aktualisierung deaktiviert",
|
||||
"Update subscription successfully": "Abonnement erfolgreich aktualisiert",
|
||||
"Update failed, retrying with Clash proxy...": "Abonnement-Aktualisierung fehlgeschlagen. Versuche es mit dem Clash-Proxy erneut...",
|
||||
"Update with Clash proxy successfully": "Aktualisierung mit Clash-Proxy erfolgreich",
|
||||
"Update failed even with Clash proxy": "Aktualisierung auch mit Clash-Proxy fehlgeschlagen",
|
||||
"Profile creation failed, retrying with Clash proxy...": "Erstellung des Abonnements fehlgeschlagen. Versuche es mit dem Clash-Proxy erneut...",
|
||||
@@ -555,10 +555,9 @@
|
||||
"Disallowed ISP": "Nicht zugelassener Internetdienstanbieter",
|
||||
"Originals Only": "Nur Original",
|
||||
"Unsupported Country/Region": "Nicht unterstütztes Land/Region",
|
||||
"Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert",
|
||||
"Controller address copied to clipboard": "API-Port in die Zwischenablage kopiert",
|
||||
"Secret copied to clipboard": "API-Schlüssel in die Zwischenablage kopiert",
|
||||
"Copy to clipboard": "Klicken Sie hier, um zu kopieren",
|
||||
"Port Config": "Port-Konfiguration",
|
||||
"Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert",
|
||||
"Enable one-click random API port and key. Click to randomize the port and key": "Einstellsichere Zufalls-API-Port- und Schlüsselgenerierung aktivieren. Klicken Sie, um Port und Schlüssel zu randomisieren"
|
||||
}
|
||||
|
||||
@@ -628,7 +628,6 @@
|
||||
"Unknown": "Unknown",
|
||||
"Auto update disabled": "Auto update disabled",
|
||||
"Update subscription successfully": "Update subscription successfully",
|
||||
"Update failed, retrying with Clash proxy...": "Update failed, retrying with Clash proxy...",
|
||||
"Update with Clash proxy successfully": "Update with Clash proxy successfully",
|
||||
"Update failed even with Clash proxy": "Update failed even with Clash proxy",
|
||||
"Profile creation failed, retrying with Clash proxy...": "Profile creation failed, retrying with Clash proxy...",
|
||||
@@ -713,5 +712,7 @@
|
||||
"Allow Auto Update": "Allow Auto Update",
|
||||
"Menu reorder mode": "Menu reorder mode",
|
||||
"Unlock menu order": "Unlock menu order",
|
||||
"Lock menu order": "Lock menu order"
|
||||
"Lock menu order": "Lock menu order",
|
||||
"Open App Log": "Open App Log",
|
||||
"Open Core Log": "Open Core Log"
|
||||
}
|
||||
|
||||
@@ -195,10 +195,11 @@
|
||||
"Settings": "Ajustes",
|
||||
"System Setting": "Ajustes del sistema",
|
||||
"Tun Mode": "Modo de interfaz virtual (TUN)",
|
||||
"TUN requires Service Mode or Admin Mode": "El modo TUN requiere el modo de servicio o el modo de administrador",
|
||||
"Install Service": "Instalar servicio",
|
||||
"Uninstall Service": "Desinstalar servicio",
|
||||
"Reset to Default": "Restablecer a los valores predeterminados",
|
||||
"Tun Mode Info": "El modo TUN (interfaz virtual) gestiona todo el tráfico del sistema. No es necesario habilitar el proxy del sistema cuando está activado.",
|
||||
"TUN requires Service Mode or Admin Mode": "El modo TUN requiere el modo de servicio o el modo de administrador",
|
||||
"System Proxy Enabled": "El proxy del sistema está habilitado. Sus aplicaciones accederán a Internet a través del proxy.",
|
||||
"System Proxy Disabled": "El proxy del sistema está deshabilitado. Se recomienda a la mayoría de los usuarios habilitar esta opción.",
|
||||
"TUN Mode Enabled": "El modo TUN está habilitado. Las aplicaciones accederán a Internet a través de la interfaz virtual.",
|
||||
@@ -254,6 +255,7 @@
|
||||
"Unified Delay Info": "Al habilitar la latencia unificada, se realizarán dos pruebas de latencia para eliminar las diferencias de latencia entre diferentes tipos de nodos causadas por el handshake de conexión, etc.",
|
||||
"Log Level": "Nivel de registro",
|
||||
"Log Level Info": "Solo se aplica al archivo de registro del núcleo en la carpeta Service del directorio de registros.",
|
||||
"Port Config": "Configuración de puerto",
|
||||
"Random Port": "Puerto aleatorio",
|
||||
"Mixed Port": "Puerto de proxy mixto",
|
||||
"Socks Port": "Puerto de proxy SOCKS",
|
||||
@@ -367,17 +369,16 @@
|
||||
"Stopping Core...": "Deteniendo núcleo...",
|
||||
"Restarting Core...": "Reiniciando núcleo...",
|
||||
"Installing Service...": "Instalando servicio...",
|
||||
"Uninstall Service": "Desinstalar servicio",
|
||||
"Service Installed Successfully": "Servicio instalado con éxito",
|
||||
"Service is ready and core restarted": "El servicio está listo y el núcleo se ha reiniciado",
|
||||
"Core restarted. Service is now available.": "El núcleo se ha reiniciado. El servicio está disponible.",
|
||||
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "El servicio estaba listo, pero puede haber habido problemas al reiniciar el núcleo o el servicio se volvió inaccesible. Por favor, verifique.",
|
||||
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Hubo problemas durante la instalación del servicio o al reiniciar el núcleo. El servicio podría no estar disponible. Por favor, revise los registros del sistema.",
|
||||
"Uninstalling Service...": "Desinstalando servicio...",
|
||||
"Waiting for service to be ready...": "Esperando a que el servicio esté listo...",
|
||||
"Service Installed Successfully": "Servicio instalado con éxito",
|
||||
"Service Uninstalled Successfully": "Servicio desinstalado con éxito",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "El intervalo de tiempo del daemon de proxy no puede ser menor de 1 segundo",
|
||||
"Invalid Bypass Format": "Formato de omisión de proxy no válido",
|
||||
"Waiting for service to be ready...": "Esperando a que el servicio esté listo...",
|
||||
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "El servicio estaba listo, pero puede haber habido problemas al reiniciar el núcleo o el servicio se volvió inaccesible. Por favor, verifique.",
|
||||
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Hubo problemas durante la instalación del servicio o al reiniciar el núcleo. El servicio podría no estar disponible. Por favor, revise los registros del sistema.",
|
||||
"Service is ready and core restarted": "El servicio está listo y el núcleo se ha reiniciado",
|
||||
"Core restarted. Service is now available.": "El núcleo se ha reiniciado. El servicio está disponible.",
|
||||
"Core Version Updated": "Versión del núcleo actualizada",
|
||||
"Clash Core Restarted": "Núcleo de Clash reiniciado",
|
||||
"GeoData Updated": "GeoData actualizado",
|
||||
@@ -523,7 +524,6 @@
|
||||
"Unknown": "Desconocido",
|
||||
"Auto update disabled": "La actualización automática está deshabilitada",
|
||||
"Update subscription successfully": "Suscripción actualizada con éxito",
|
||||
"Update failed, retrying with Clash proxy...": "Error al actualizar la suscripción. Intentando con el proxy de Clash...",
|
||||
"Update with Clash proxy successfully": "Actualización con el proxy de Clash exitosa",
|
||||
"Update failed even with Clash proxy": "Error al actualizar incluso con el proxy de Clash",
|
||||
"Profile creation failed, retrying with Clash proxy...": "Error al crear la suscripción. Intentando con el proxy de Clash...",
|
||||
@@ -555,10 +555,9 @@
|
||||
"Disallowed ISP": "Proveedor de servicios de Internet no permitido",
|
||||
"Originals Only": "Solo originales",
|
||||
"Unsupported Country/Region": "País/región no soportado",
|
||||
"Configuration saved successfully": "Configuración aleatoria guardada correctamente",
|
||||
"Controller address copied to clipboard": "El puerto API se copió al portapapeles",
|
||||
"Secret copied to clipboard": "La clave API se copió al portapapeles",
|
||||
"Copy to clipboard": "Haz clic aquí para copiar",
|
||||
"Port Config": "Configuración de puerto",
|
||||
"Configuration saved successfully": "Configuración aleatoria guardada correctamente",
|
||||
"Enable one-click random API port and key. Click to randomize the port and key": "Habilitar la generación de puerto y clave API aleatorios con un solo clic. Haz clic para randomizar el puerto y la clave"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"Label-Connections": "اتصالات",
|
||||
"Label-Rules": "قوانین",
|
||||
"Label-Logs": "لاگها",
|
||||
"Label-Test": "آزمون",
|
||||
"Label-Settings": "تنظیمات",
|
||||
"Proxies": "پراکسیها",
|
||||
"Proxy Groups": "گروههای پراکسی",
|
||||
@@ -166,7 +165,6 @@
|
||||
"Table View": "نمای جدولی",
|
||||
"List View": "نمای لیستی",
|
||||
"Close All": "بستن همه",
|
||||
"Default": "پیشفرض",
|
||||
"Download Speed": "سرعت دانلود",
|
||||
"Upload Speed": "سرعت بارگذاری",
|
||||
"Host": "میزبان",
|
||||
@@ -197,6 +195,8 @@
|
||||
"Settings": "تنظیمات",
|
||||
"System Setting": "تنظیمات سیستم",
|
||||
"Tun Mode": "Tun (کارت شبکه مجازی)",
|
||||
"TUN requires Service Mode": "حالت تونلزنی نیاز به سرویس دارد",
|
||||
"Install Service": "نصب سرویس",
|
||||
"Reset to Default": "بازنشانی به پیشفرض",
|
||||
"Tun Mode Info": "حالت Tun (NIC مجازی): تمام ترافیک سیستم را ضبط می کند، وقتی فعال باشد، نیازی به فعال کردن پروکسی سیستم نیست.",
|
||||
"Stack": "انباشته Tun",
|
||||
@@ -275,13 +275,13 @@
|
||||
"Restart": "راهاندازی مجدد",
|
||||
"Release Version": "نسخه نهایی",
|
||||
"Alpha Version": "نسخه آلفا",
|
||||
"Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
|
||||
"Please enter your root password": "لطفاً رمز ریشه خود را وارد کنید",
|
||||
"Grant": "اعطا",
|
||||
"Open UWP tool": "باز کردن ابزار UWP",
|
||||
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامههای UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شدهاند و این ابزار میتواند برای دور زدن این محدودیت استفاده شود",
|
||||
"Update GeoData": "بهروزرسانی GeoData",
|
||||
"Verge Setting": "تنظیمات Verge",
|
||||
"Verge Basic Setting": "تنظیمات پایه Verge",
|
||||
"Verge Advanced Setting": "تنظیمات پیشرفته Verge",
|
||||
"Language": "زبان",
|
||||
"Theme Mode": "حالت تم",
|
||||
"theme.light": "روشن",
|
||||
@@ -368,6 +368,7 @@
|
||||
"Profile Reactivated": "پروفایل مجدداً فعال شد",
|
||||
"Only YAML Files Supported": "فقط فایلهای YAML پشتیبانی میشوند",
|
||||
"Settings Applied": "تنظیمات اعمال شد",
|
||||
"Installing Service...": "در حال نصب سرویس...",
|
||||
"Service Installed Successfully": "سرویس با موفقیت نصب شد",
|
||||
"Service Uninstalled Successfully": "سرویس با موفقیت حذف نصب شد",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "مدت زمان دیمن پراکسی نمیتواند کمتر از 1 ثانیه باشد",
|
||||
@@ -451,10 +452,9 @@
|
||||
"Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد",
|
||||
"Core Changed Successfully": "هسته با موفقیت تغییر کرد",
|
||||
"Failed to Change Core": "تغییر هسته ناموفق بود",
|
||||
"Verge Basic Setting": "تنظیمات پایه Verge",
|
||||
"Verge Advanced Setting": "تنظیمات پیشرفته Verge",
|
||||
"TUN requires Service Mode": "حالت تونلزنی نیاز به سرویس دارد",
|
||||
"Install Service": "نصب سرویس",
|
||||
"Installing Service...": "در حال نصب سرویس...",
|
||||
"Service Administrator Prompt": "Clash Verge برای نصب مجدد سرویس سیستم به امتیازات مدیر نیاز دارد"
|
||||
"Service Administrator Prompt": "Clash Verge برای نصب مجدد سرویس سیستم به امتیازات مدیر نیاز دارد",
|
||||
"Default": "پیشفرض",
|
||||
"Label-Test": "آزمون",
|
||||
"Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
|
||||
"Verge Setting": "تنظیمات Verge"
|
||||
}
|
||||
|
||||
@@ -21,40 +21,7 @@
|
||||
"Label-Connections": "Koneksi",
|
||||
"Label-Rules": "Aturan",
|
||||
"Label-Logs": "Log",
|
||||
"Label-Test": "Tes",
|
||||
"Label-Settings": "Pengaturan",
|
||||
"Dashboard": "Dasbor",
|
||||
"Profile": "Profil",
|
||||
"Help": "Bantuan",
|
||||
"About": "Tentang",
|
||||
"Theme": "Tema",
|
||||
"Main Window": "Jendela Utama",
|
||||
"Group Icon": "Ikon Grup",
|
||||
"Menu Icon": "Ikon Menu",
|
||||
"PAC File": "Berkas PAC",
|
||||
"Web UI": "Antarmuka Web",
|
||||
"Hotkeys": "Pintasan",
|
||||
"Verge Mixed Port": "Port Campuran Verge",
|
||||
"Verge Socks Port": "Port Socks Verge",
|
||||
"Verge Redir Port": "Port Pengalihan Verge",
|
||||
"Verge Tproxy Port": "Port Tproxy Verge",
|
||||
"Verge Port": "Port Verge",
|
||||
"Verge HTTP Enabled": "HTTP Verge Diaktifkan",
|
||||
"WebDAV URL": "URL WebDAV",
|
||||
"WebDAV Username": "Nama Pengguna WebDAV",
|
||||
"WebDAV Password": "Kata Sandi WebDAV",
|
||||
"Restart App": "Mulai Ulang Aplikasi",
|
||||
"Restart Clash Core": "Mulai Ulang Core Clash",
|
||||
"TUN Mode": "Mode TUN",
|
||||
"Copy Env": "Salin Env",
|
||||
"Conf Dir": "Direktori Konfigurasi",
|
||||
"Core Dir": "Direktori Core",
|
||||
"Logs Dir": "Direktori Log",
|
||||
"Open Dir": "Buka Direktori",
|
||||
"More": "Lainnya",
|
||||
"Rule Mode": "Mode Aturan",
|
||||
"Global Mode": "Mode Global",
|
||||
"Direct Mode": "Mode Langsung",
|
||||
"Proxies": "Proksi",
|
||||
"Proxy Groups": "Grup Proksi",
|
||||
"Proxy Provider": "Penyedia Proksi",
|
||||
@@ -198,7 +165,6 @@
|
||||
"Table View": "Tampilan Tabel",
|
||||
"List View": "Tampilan Daftar",
|
||||
"Close All": "Tutup Semua",
|
||||
"Default": "Default",
|
||||
"Download Speed": "Kecepatan Unduh",
|
||||
"Upload Speed": "Kecepatan Unggah",
|
||||
"Host": "Host",
|
||||
@@ -229,6 +195,8 @@
|
||||
"Settings": "Pengaturan",
|
||||
"System Setting": "Pengaturan Sistem",
|
||||
"Tun Mode": "Mode Tun (NIC Virtual)",
|
||||
"TUN requires Service Mode": "Mode TUN memerlukan layanan",
|
||||
"Install Service": "Instal Layanan",
|
||||
"Reset to Default": "Setel Ulang ke Default",
|
||||
"Tun Mode Info": "Mode Tun (NIC Virtual): Menangkap semua lalu lintas sistem, saat diaktifkan, tidak perlu mengaktifkan proksi sistem.",
|
||||
"Stack": "Tumpukan Tun",
|
||||
@@ -312,7 +280,8 @@
|
||||
"Open UWP tool": "Buka alat UWP",
|
||||
"Open UWP tool Info": "Sejak Windows 8, aplikasi UWP (seperti Microsoft Store) dibatasi dari mengakses layanan jaringan host lokal secara langsung, dan alat ini dapat digunakan untuk melewati pembatasan ini",
|
||||
"Update GeoData": "Perbarui GeoData",
|
||||
"Verge Setting": "Pengaturan Verge",
|
||||
"Verge Basic Setting": "Pengaturan Dasar Verge",
|
||||
"Verge Advanced Setting": "Pengaturan Lanjutan Verge",
|
||||
"Language": "Bahasa",
|
||||
"Theme Mode": "Mode Tema",
|
||||
"theme.light": "Terang",
|
||||
@@ -399,6 +368,7 @@
|
||||
"Profile Reactivated": "Profil Diaktifkan Kembali",
|
||||
"Only YAML Files Supported": "Hanya File YAML yang Didukung",
|
||||
"Settings Applied": "Pengaturan Diterapkan",
|
||||
"Installing Service...": "Memasang Layanan...",
|
||||
"Service Installed Successfully": "Layanan Berhasil Diinstal",
|
||||
"Service Uninstalled Successfully": "Layanan Berhasil Dicopot",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "Durasi Daemon Proksi Tidak Boleh Kurang dari 1 Detik",
|
||||
@@ -437,6 +407,38 @@
|
||||
"Confirm to restore this backup file?": "Konfirmasi untuk memulihkan file cadangan ini?",
|
||||
"Restore Success, App will restart in 1s": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik",
|
||||
"Failed to fetch backup files": "Gagal mengambil file cadangan",
|
||||
"Profile": "Profil",
|
||||
"Help": "Bantuan",
|
||||
"About": "Tentang",
|
||||
"Theme": "Tema",
|
||||
"Main Window": "Jendela Utama",
|
||||
"Group Icon": "Ikon Grup",
|
||||
"Menu Icon": "Ikon Menu",
|
||||
"PAC File": "Berkas PAC",
|
||||
"Web UI": "Antarmuka Web",
|
||||
"Hotkeys": "Pintasan",
|
||||
"Verge Mixed Port": "Port Campuran Verge",
|
||||
"Verge Socks Port": "Port Socks Verge",
|
||||
"Verge Redir Port": "Port Pengalihan Verge",
|
||||
"Verge Tproxy Port": "Port Tproxy Verge",
|
||||
"Verge Port": "Port Verge",
|
||||
"Verge HTTP Enabled": "HTTP Verge Diaktifkan",
|
||||
"WebDAV URL": "URL WebDAV",
|
||||
"WebDAV Username": "Nama Pengguna WebDAV",
|
||||
"WebDAV Password": "Kata Sandi WebDAV",
|
||||
"Dashboard": "Dasbor",
|
||||
"Restart App": "Mulai Ulang Aplikasi",
|
||||
"Restart Clash Core": "Mulai Ulang Core Clash",
|
||||
"TUN Mode": "Mode TUN",
|
||||
"Copy Env": "Salin Env",
|
||||
"Conf Dir": "Direktori Konfigurasi",
|
||||
"Core Dir": "Direktori Core",
|
||||
"Logs Dir": "Direktori Log",
|
||||
"Open Dir": "Buka Direktori",
|
||||
"More": "Lainnya",
|
||||
"Rule Mode": "Mode Aturan",
|
||||
"Global Mode": "Mode Global",
|
||||
"Direct Mode": "Mode Langsung",
|
||||
"Enable Tray Speed": "Aktifkan Tray Speed",
|
||||
"LightWeight Mode": "Mode Ringan",
|
||||
"LightWeight Mode Info": "Tutup GUI dan biarkan hanya kernel yang berjalan",
|
||||
@@ -450,10 +452,8 @@
|
||||
"Script File Error": "Kesalahan file skrip, perubahan dibatalkan",
|
||||
"Core Changed Successfully": "Inti berhasil diubah",
|
||||
"Failed to Change Core": "Gagal mengubah inti",
|
||||
"Verge Basic Setting": "Pengaturan Dasar Verge",
|
||||
"Verge Advanced Setting": "Pengaturan Lanjutan Verge",
|
||||
"TUN requires Service Mode": "Mode TUN memerlukan layanan",
|
||||
"Install Service": "Instal Layanan",
|
||||
"Installing Service...": "Memasang Layanan...",
|
||||
"Service Administrator Prompt": "Clash Verge memerlukan hak administrator untuk menginstal ulang layanan sistem"
|
||||
"Service Administrator Prompt": "Clash Verge memerlukan hak administrator untuk menginstal ulang layanan sistem",
|
||||
"Default": "Default",
|
||||
"Label-Test": "Tes",
|
||||
"Verge Setting": "Pengaturan Verge"
|
||||
}
|
||||
|
||||
@@ -195,10 +195,11 @@
|
||||
"Settings": "設定",
|
||||
"System Setting": "システム設定",
|
||||
"Tun Mode": "仮想ネットワークカードモード",
|
||||
"TUN requires Service Mode or Admin Mode": "TUNモードはサービスモードまたは管理者モードが必要です",
|
||||
"Install Service": "サービスをインストール",
|
||||
"Uninstall Service": "サービスのアンインストール",
|
||||
"Reset to Default": "デフォルト値にリセット",
|
||||
"Tun Mode Info": "TUN(仮想ネットワークカード)モードはシステムのすべてのトラフィックを制御します。有効にすると、システムプロキシを開く必要はありません。",
|
||||
"TUN requires Service Mode or Admin Mode": "TUNモードはサービスモードまたは管理者モードが必要です",
|
||||
"System Proxy Enabled": "システムプロキシが有効になっています。アプリケーションはプロキシを通じてネットワークにアクセスします。",
|
||||
"System Proxy Disabled": "システムプロキシが無効になっています。ほとんどのユーザーはこのオプションをオンにすることをお勧めします。",
|
||||
"TUN Mode Enabled": "TUNモードが有効になっています。アプリケーションは仮想ネットワークカードを通じてネットワークにアクセスします。",
|
||||
@@ -254,6 +255,7 @@
|
||||
"Unified Delay Info": "統一遅延を有効にすると、2回の遅延テストが行われ、接続ハンドシェイクなどによる異なるタイプのノードの遅延差を解消します。",
|
||||
"Log Level": "ログレベル",
|
||||
"Log Level Info": "ログディレクトリのServiceフォルダ内のコアログファイルにのみ適用されます。",
|
||||
"Port Config": "ポート設定",
|
||||
"Random Port": "ランダムポート",
|
||||
"Mixed Port": "混合プロキシポート",
|
||||
"Socks Port": "SOCKSプロキシポート",
|
||||
@@ -370,17 +372,16 @@
|
||||
"Stopping Core...": "コアを停止中...",
|
||||
"Restarting Core...": "コアを再起動中...",
|
||||
"Installing Service...": "サービスをインストール中...",
|
||||
"Uninstall Service": "サービスのアンインストール",
|
||||
"Service Installed Successfully": "サービスのインストールに成功しました。",
|
||||
"Service is ready and core restarted": "サービスが準備完了し、コアが再起動されました。",
|
||||
"Core restarted. Service is now available.": "コアが再起動され、サービスが利用可能になりました。",
|
||||
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "サービスは準備が整っていましたが、コアの再起動に問題が発生したか、サービスが利用できなくなった可能性があります。ご確認ください。",
|
||||
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "サービスのインストールまたはコアの再起動中に問題が発生しました。サービスが利用できない可能性があります。システムログを確認してください。",
|
||||
"Uninstalling Service...": "サービスをアンインストール中...",
|
||||
"Waiting for service to be ready...": "サービスの準備を待っています...",
|
||||
"Service Installed Successfully": "サービスのインストールに成功しました。",
|
||||
"Service Uninstalled Successfully": "サービスのアンインストールに成功しました。",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "プロキシデーモンの間隔は1秒以上に設定する必要があります。",
|
||||
"Invalid Bypass Format": "無効なバイパス形式",
|
||||
"Waiting for service to be ready...": "サービスの準備を待っています...",
|
||||
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "サービスは準備が整っていましたが、コアの再起動に問題が発生したか、サービスが利用できなくなった可能性があります。ご確認ください。",
|
||||
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "サービスのインストールまたはコアの再起動中に問題が発生しました。サービスが利用できない可能性があります。システムログを確認してください。",
|
||||
"Service is ready and core restarted": "サービスが準備完了し、コアが再起動されました。",
|
||||
"Core restarted. Service is now available.": "コアが再起動され、サービスが利用可能になりました。",
|
||||
"Core Version Updated": "コアバージョンが更新されました。",
|
||||
"Clash Core Restarted": "Clashコアが再起動されました。",
|
||||
"GeoData Updated": "GeoDataが更新されました。",
|
||||
@@ -526,7 +527,6 @@
|
||||
"Unknown": "不明",
|
||||
"Auto update disabled": "自動更新が無効になっています。",
|
||||
"Update subscription successfully": "サブスクリプションの更新に成功しました。",
|
||||
"Update failed, retrying with Clash proxy...": "サブスクリプションの更新に失敗しました。Clashプロキシを使用して再試行します...",
|
||||
"Update with Clash proxy successfully": "Clashプロキシを使用して更新に成功しました。",
|
||||
"Update failed even with Clash proxy": "Clashプロキシを使用しても更新に失敗しました。",
|
||||
"Profile creation failed, retrying with Clash proxy...": "プロファイルの作成に失敗しました。Clashプロキシを使用して再試行します...",
|
||||
@@ -558,12 +558,9 @@
|
||||
"Disallowed ISP": "許可されていないインターネットサービスプロバイダー",
|
||||
"Originals Only": "オリジナルのみ",
|
||||
"Unsupported Country/Region": "サポートされていない国/地域",
|
||||
"Configuration saved successfully": "ランダム設定を保存完了",
|
||||
"Controller address copied to clipboard": "API ポートがクリップボードにコピーされました",
|
||||
"Secret copied to clipboard": "API キーがクリップボードにコピーされました",
|
||||
"Copy to clipboard": "クリックしてコピー",
|
||||
"Port Config": "ポート設定",
|
||||
"Configuration saved successfully": "ランダム設定を保存完了",
|
||||
"Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください",
|
||||
"Batch Operations": "バッチ操作",
|
||||
"Delete Selected Profiles": "選択したプロファイルを削除",
|
||||
"Deselect All": "すべての選択を解除",
|
||||
@@ -571,5 +568,7 @@
|
||||
"items": "アイテム",
|
||||
"Select All": "すべて選択",
|
||||
"Selected": "選択済み",
|
||||
"Selected profiles deleted successfully": "選択したプロファイルが正常に削除されました"
|
||||
"Selected profiles deleted successfully": "選択したプロファイルが正常に削除されました",
|
||||
"Copy to clipboard": "クリックしてコピー",
|
||||
"Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください"
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
"Lazy": "지연 로딩",
|
||||
"Timeout": "타임아웃",
|
||||
"Max Failed Times": "최대 실패 횟수",
|
||||
"Interface Name": "인터페이스 이름",
|
||||
"Routing Mark": "라우팅 마크",
|
||||
"Include All": "모든 프록시 및 제공자 포함",
|
||||
"Include All Providers": "모든 제공자 포함",
|
||||
@@ -198,176 +199,52 @@
|
||||
"Edit Test": "테스트 편집",
|
||||
"Icon": "아이콘",
|
||||
"Test URL": "테스트 URL",
|
||||
"Timeout (ms)": "타임아웃 (ms)",
|
||||
"Expected": "예상됨",
|
||||
"URL": "URL",
|
||||
"Method": "메소드",
|
||||
"Failed": "실패",
|
||||
"Succeed": "성공",
|
||||
"Settings": "설정",
|
||||
"Core Config": "코어 설정",
|
||||
"Clash Setting": "Clash 설정",
|
||||
"Verge Setting": "Verge 설정",
|
||||
"System Setting": "시스템 설정",
|
||||
"Appearance": "외관",
|
||||
"Experimental Features": "실험적 기능",
|
||||
"Others": "기타",
|
||||
"Mixed Port": "혼합 포트",
|
||||
"Allow LAN": "LAN 허용",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "로그 레벨",
|
||||
"Core Type": "코어 유형",
|
||||
"General": "일반",
|
||||
"Mode": "모드",
|
||||
"Tun Mode": "Tun 모드",
|
||||
"Transparent Proxy": "투명 프록시",
|
||||
"Specify YAML": "YAML 지정",
|
||||
"Status": "상태",
|
||||
"Memory Usage": "메모리 사용량",
|
||||
"Stack": "스택",
|
||||
"Network": "네트워크",
|
||||
"MTU": "MTU",
|
||||
"Auto Route": "자동 라우팅",
|
||||
"Auto Detect Interface": "인터페이스 자동 감지",
|
||||
"Interface Name": "인터페이스 이름",
|
||||
"Endpoint Independent Nat": "엔드포인트 독립 NAT",
|
||||
"Include Reserved": "예약된 IP 포함",
|
||||
"Enable Default DNS Hijack": "기본 DNS 하이재킹 활성화",
|
||||
"TCP Fast Open": "TCP 빠른 열기",
|
||||
"Silent Start": "자동 시작",
|
||||
"TcpConcurrent": "TCP 동시성",
|
||||
"MTU": "MTU",
|
||||
"Service Mode": "서비스 모드",
|
||||
"System Proxy": "시스템 프록시",
|
||||
"Start With System": "시스템과 함께 시작",
|
||||
"Set System Proxy": "시스템 프록시 설정",
|
||||
"Set as System Proxy": "시스템 프록시로 설정",
|
||||
"System Proxy Status": "시스템 프록시 상태",
|
||||
"Start Option": "시작 옵션",
|
||||
"Start Core on Start": "시작 시 코어 시작",
|
||||
"Start Core with System": "시스템과 함께 코어 시작",
|
||||
"Start Core with System Proxy": "시스템 프록시와 함께 코어 시작",
|
||||
"Start Core with Tun": "Tun과 함께 코어 시작",
|
||||
"Silent Start Option": "자동 시작 옵션",
|
||||
"Hidden Window on Start": "시작 시 창 숨기기",
|
||||
"Log Notice": "로그 알림",
|
||||
"Warning": "경고",
|
||||
"Error": "오류",
|
||||
"Silent Start": "자동 시작",
|
||||
"Clash Setting": "Clash 설정",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "로그 레벨",
|
||||
"Mixed Port": "혼합 포트",
|
||||
"Verge Basic Setting": "Verge 기본 설정",
|
||||
"Language": "언어",
|
||||
"Theme Mode": "테마 모드",
|
||||
"Tray Click Event": "트레이 클릭 이벤트",
|
||||
"Show Main Window": "메인 창 표시",
|
||||
"Show Tray Menu": "트레이 메뉴 표시",
|
||||
"Open Config Folder": "설정 폴더 열기",
|
||||
"Open Dashboard": "대시보드 열기",
|
||||
"Hotkey Setting": "단축키 설정",
|
||||
"Misc Setting": "기타 설정",
|
||||
"Layout Setting": "레이아웃 설정",
|
||||
"Update Setting": "업데이트 설정",
|
||||
"Enable Hotkeys": "단축키 활성화",
|
||||
"System Hotkey": "시스템 단축키",
|
||||
"Hotkey Enable": "단축키 활성화",
|
||||
"Require Clash Core Running": "Clash 코어 실행 필요",
|
||||
"Copy Env Type": "환경 유형 복사",
|
||||
"Copy Success": "복사 성공",
|
||||
"Start Page": "시작 페이지",
|
||||
"Startup Script": "시작 스크립트",
|
||||
"Icon Group Type": "아이콘 그룹 유형",
|
||||
"Always": "항상",
|
||||
"On Update": "업데이트 시",
|
||||
"By Traffic": "트래픽별",
|
||||
"Web UI List": "웹 UI 목록",
|
||||
"Installed Web UI": "설치된 웹 UI",
|
||||
"Built-in Web UI": "내장 웹 UI",
|
||||
"Current Config": "현재 설정",
|
||||
"System Config": "시스템 설정",
|
||||
"Port": "포트",
|
||||
"WebUI Current Port": "웹 UI 현재 포트",
|
||||
"Theme": "테마",
|
||||
"Light": "라이트",
|
||||
"Dark": "다크",
|
||||
"Auto": "자동",
|
||||
"System": "시스템",
|
||||
"Proxy Item Width": "프록시 항목 너비",
|
||||
"Proxy Item Height": "프록시 항목 높이",
|
||||
"Compact Mode": "압축 모드",
|
||||
"Git Proxy": "Git 프록시",
|
||||
"Enable API": "API 활성화",
|
||||
"Enable Lan": "LAN 활성화",
|
||||
"Select a config file": "설정 파일 선택",
|
||||
"Open Config Dir": "설정 디렉토리 열기",
|
||||
"System Proxy Permission": "시스템 프록시 권한",
|
||||
"System Stack Type": "시스템 스택 유형",
|
||||
"Undefined stack": "정의되지 않은 스택",
|
||||
"Auto Start": "자동 시작",
|
||||
"Mixin": "혼합",
|
||||
"Set as System Auto Proxy": "시스템 자동 프록시로 설정",
|
||||
"System Auto Proxy Status": "시스템 자동 프록시 상태",
|
||||
"Authorization for requests coming through HTTP Proxy (e.g. local connections)": "HTTP 프록시를 통한 요청에 대한 인증 (예: 로컬 연결)",
|
||||
"Primary Color": "기본 색상",
|
||||
"Layout Setting": "레이아웃 설정",
|
||||
"Traffic Graph": "트래픽 그래프",
|
||||
"Memory Usage": "메모리 사용량",
|
||||
"Auto Delay Detection": "자동 지연 감지",
|
||||
"Auto Delay Detection Info": "백그라운드에서 현재 노드의 지연을 주기적으로 검사합니다",
|
||||
"Hotkey Setting": "단축키 설정",
|
||||
"Filter": "필터",
|
||||
"Import Subscription Successful": "구독 가져오기 성공",
|
||||
"Username": "사용자 이름",
|
||||
"Password": "비밀번호",
|
||||
"Auth Proxy": "인증 프록시",
|
||||
"Geox User": "Geox 사용자",
|
||||
"Geox Password": "Geox 비밀번호",
|
||||
"Log File": "로그 파일",
|
||||
"Enable Clash.Meta Logs": "Clash.Meta 로그 활성화",
|
||||
"Verge Log": "Verge 로그",
|
||||
"Enable Verge Logs": "Verge 로그 활성화",
|
||||
"Traffic Graph": "트래픽 그래프",
|
||||
"Profile Token": "프로필 토큰",
|
||||
"Profile User Agent": "프로필 사용자 에이전트",
|
||||
"The User Agent to use when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 사용할 사용자 에이전트입니다.",
|
||||
"Profile Format": "프로필 포맷",
|
||||
"The expected content type to send in the `Accept` header when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 `Accept` 헤더에 보낼 예상 컨텐츠 타입입니다.",
|
||||
"Theme Color": "테마 색상",
|
||||
"Primary Color": "기본 색상",
|
||||
"Customize primary color": "기본 색상 사용자 정의",
|
||||
"Danger Zone": "위험 영역",
|
||||
"Reset Verge Theme": "Verge 테마 재설정",
|
||||
"Inject CSS": "CSS 주입",
|
||||
"Inject a custom CSS content into the GUI": "사용자 정의 CSS 내용을 GUI에 주입",
|
||||
"Inject HTML": "HTML 주입",
|
||||
"Inject a custom HTML content into the GUI (appended in body)": "사용자 정의 HTML 내용을 GUI에 주입 (본문에 추가)",
|
||||
"Capture": "캡처",
|
||||
"Color Scheme": "색상 구성표",
|
||||
"Default": "기본값",
|
||||
"Pink": "분홍색",
|
||||
"Red": "빨간색",
|
||||
"Yellow": "노란색",
|
||||
"Green": "녹색",
|
||||
"Cyan": "청록색",
|
||||
"Blue": "파란색",
|
||||
"Purple": "보라색",
|
||||
"Proxy Detail": "프록시 상세",
|
||||
"Address": "주소",
|
||||
"Filter": "필터",
|
||||
"Check Updates on Start": "시작 시 업데이트 확인",
|
||||
"For Alpha Version": "알파 버전용",
|
||||
"Latest Build Version": "최신 빌드 버전",
|
||||
"Check Updates": "업데이트 확인",
|
||||
"Proxy Setting": "프록시 설정",
|
||||
"WebDav Setting": "WebDav 설정",
|
||||
"WebDav Upload": "WebDav 업로드",
|
||||
"WebDav Download": "WebDav 다운로드",
|
||||
"Clean Cache": "캐시 정리",
|
||||
"Check Network": "네트워크 확인",
|
||||
"WebDav Status": "WebDav 상태",
|
||||
"WebDav URL": "WebDav URL",
|
||||
"WebDav Username": "WebDav 사용자 이름",
|
||||
"WebDav Password": "WebDav 비밀번호",
|
||||
"Update Interval(minute)": "업데이트 간격(분)",
|
||||
"Skip Cert Verify": "인증서 확인 건너뛰기",
|
||||
"Import Subscription Successful": "구독 가져오기 성공",
|
||||
"Update with Clash proxy successfully": "Clash 프록시로 업데이트 성공",
|
||||
"Update failed, retrying with Clash proxy...": "업데이트 실패, Clash 프록시로 재시도 중...",
|
||||
"Update failed even with Clash proxy": "Clash 프록시로도 업데이트 실패",
|
||||
"Theme": "테마",
|
||||
"Config Validation Failed": "설정 검증 실패",
|
||||
"Boot Config Validation Failed": "부팅 설정 검증 실패",
|
||||
"Core Change Config Validation Failed": "코어 변경 설정 검증 실패",
|
||||
"Config Validation Failed": "설정 검증 실패",
|
||||
"Config Validation Process Terminated": "설정 검증 프로세스 종료됨",
|
||||
"Script File Error": "스크립트 파일 오류",
|
||||
"Script Syntax Error": "스크립트 구문 오류",
|
||||
"Script Missing Main": "스크립트 메인 없음",
|
||||
"File Not Found": "파일을 찾을 수 없음",
|
||||
"Script File Error": "스크립트 파일 오류",
|
||||
"Core Changed Successfully": "코어 변경 성공",
|
||||
"Failed to Change Core": "코어 변경 실패",
|
||||
"YAML Syntax Error": "YAML 구문 오류",
|
||||
"YAML Read Error": "YAML 읽기 오류",
|
||||
"YAML Mapping Error": "YAML 매핑 오류",
|
||||
@@ -377,28 +254,150 @@
|
||||
"Merge File Mapping Error": "병합 파일 매핑 오류",
|
||||
"Merge File Key Error": "병합 파일 키 오류",
|
||||
"Merge File Error": "병합 파일 오류",
|
||||
"Core Changed Successfully": "코어 변경 성공",
|
||||
"Failed to Change Core": "코어 변경 실패",
|
||||
"Copy Success": "복사 성공",
|
||||
"Copy Failed": "복사 실패",
|
||||
"Update with Clash proxy successfully": "Clash 프록시로 업데이트 성공",
|
||||
"Update failed even with Clash proxy": "Clash 프록시로도 업데이트 실패",
|
||||
"Failed": "실패",
|
||||
"Address": "주소",
|
||||
"Allow LAN": "LAN 허용",
|
||||
"Always": "항상",
|
||||
"Appearance": "외관",
|
||||
"Auth Proxy": "인증 프록시",
|
||||
"Authorization for requests coming through HTTP Proxy (e.g. local connections)": "HTTP 프록시를 통한 요청에 대한 인증 (예: 로컬 연결)",
|
||||
"Auto": "자동",
|
||||
"Auto Start": "자동 시작",
|
||||
"Blue": "파란색",
|
||||
"Built-in Web UI": "내장 웹 UI",
|
||||
"By Traffic": "트래픽별",
|
||||
"Cannot Import Empty Subscription URL": "빈 구독 URL을 가져올 수 없습니다",
|
||||
"Profile Already Exists": "프로필이 이미 존재합니다",
|
||||
"Input Subscription URL": "구독 URL 입력",
|
||||
"Create Profile Successful": "프로필 생성 성공",
|
||||
"Capture": "캡처",
|
||||
"Check Network": "네트워크 확인",
|
||||
"Check Updates": "업데이트 확인",
|
||||
"Check Updates on Start": "시작 시 업데이트 확인",
|
||||
"Clean Cache": "캐시 정리",
|
||||
"Color Scheme": "색상 구성표",
|
||||
"Compact Mode": "압축 모드",
|
||||
"Copy Failed": "복사 실패",
|
||||
"Core Config": "코어 설정",
|
||||
"Core Type": "코어 유형",
|
||||
"Create Profile Failed": "프로필 생성 실패",
|
||||
"Patch Profile Successful": "프로필 패치 성공",
|
||||
"Patch Profile Failed": "프로필 패치 실패",
|
||||
"Delete Profile Successful": "프로필 삭제 성공",
|
||||
"Create Profile Successful": "프로필 생성 성공",
|
||||
"Current Config": "현재 설정",
|
||||
"Customize primary color": "기본 색상 사용자 정의",
|
||||
"Cyan": "청록색",
|
||||
"Danger Zone": "위험 영역",
|
||||
"Dark": "다크",
|
||||
"Default": "기본값",
|
||||
"Delete Profile Failed": "프로필 삭제 실패",
|
||||
"Select Active Profile Successful": "활성 프로필 선택 성공",
|
||||
"Delete Profile Successful": "프로필 삭제 성공",
|
||||
"Enable API": "API 활성화",
|
||||
"Enable Clash.Meta Logs": "Clash.Meta 로그 활성화",
|
||||
"Enable Default DNS Hijack": "기본 DNS 하이재킹 활성화",
|
||||
"Enable Hotkeys": "단축키 활성화",
|
||||
"Enable Lan": "LAN 활성화",
|
||||
"Enable Verge Logs": "Verge 로그 활성화",
|
||||
"Endpoint Independent Nat": "엔드포인트 독립 NAT",
|
||||
"Error": "오류",
|
||||
"Expected": "예상됨",
|
||||
"Experimental Features": "실험적 기능",
|
||||
"For Alpha Version": "알파 버전용",
|
||||
"General": "일반",
|
||||
"Geox Password": "Geox 비밀번호",
|
||||
"Geox User": "Geox 사용자",
|
||||
"Git Proxy": "Git 프록시",
|
||||
"Green": "녹색",
|
||||
"Hidden Window on Start": "시작 시 창 숨기기",
|
||||
"Hotkey Enable": "단축키 활성화",
|
||||
"Icon Group Type": "아이콘 그룹 유형",
|
||||
"Include Reserved": "예약된 IP 포함",
|
||||
"Inject CSS": "CSS 주입",
|
||||
"Inject HTML": "HTML 주입",
|
||||
"Inject a custom CSS content into the GUI": "사용자 정의 CSS 내용을 GUI에 주입",
|
||||
"Inject a custom HTML content into the GUI (appended in body)": "사용자 정의 HTML 내용을 GUI에 주입 (본문에 추가)",
|
||||
"Input Subscription URL": "구독 URL 입력",
|
||||
"Installed Web UI": "설치된 웹 UI",
|
||||
"Latest Build Version": "최신 빌드 버전",
|
||||
"Light": "라이트",
|
||||
"Log File": "로그 파일",
|
||||
"Log Notice": "로그 알림",
|
||||
"Method": "메소드",
|
||||
"Misc Setting": "기타 설정",
|
||||
"Mixin": "혼합",
|
||||
"Mode": "모드",
|
||||
"Network": "네트워크",
|
||||
"On Update": "업데이트 시",
|
||||
"Open Config Dir": "설정 디렉토리 열기",
|
||||
"Open Config Folder": "설정 폴더 열기",
|
||||
"Open Dashboard": "대시보드 열기",
|
||||
"Others": "기타",
|
||||
"Patch Profile Failed": "프로필 패치 실패",
|
||||
"Patch Profile Successful": "프로필 패치 성공",
|
||||
"Pink": "분홍색",
|
||||
"Port": "포트",
|
||||
"Profile Already Exists": "프로필이 이미 존재합니다",
|
||||
"Profile Format": "프로필 포맷",
|
||||
"Profile Token": "프로필 토큰",
|
||||
"Profile User Agent": "프로필 사용자 에이전트",
|
||||
"Proxy Detail": "프록시 상세",
|
||||
"Proxy Item Height": "프록시 항목 높이",
|
||||
"Proxy Item Width": "프록시 항목 너비",
|
||||
"Proxy Setting": "프록시 설정",
|
||||
"Purple": "보라색",
|
||||
"Red": "빨간색",
|
||||
"Require Clash Core Running": "Clash 코어 실행 필요",
|
||||
"Reset Verge Theme": "Verge 테마 재설정",
|
||||
"Select Active Profile Failed": "활성 프로필 선택 실패",
|
||||
"View Profile-Runtime": "프로필-런타임 보기",
|
||||
"View Profile-Content": "프로필-내용 보기",
|
||||
"View Profile-Original": "프로필-원본 보기",
|
||||
"View Profile-Script": "프로필-스크립트 보기",
|
||||
"View Profile-Merge": "프로필-병합 보기",
|
||||
"Update Successful": "업데이트 성공",
|
||||
"Select Active Profile Successful": "활성 프로필 선택 성공",
|
||||
"Select a config file": "설정 파일 선택",
|
||||
"Set System Proxy": "시스템 프록시 설정",
|
||||
"Set as System Auto Proxy": "시스템 자동 프록시로 설정",
|
||||
"Set as System Proxy": "시스템 프록시로 설정",
|
||||
"Silent Start Option": "자동 시작 옵션",
|
||||
"Skip Cert Verify": "인증서 확인 건너뛰기",
|
||||
"Specify YAML": "YAML 지정",
|
||||
"Start Core on Start": "시작 시 코어 시작",
|
||||
"Start Core with System": "시스템과 함께 코어 시작",
|
||||
"Start Core with System Proxy": "시스템 프록시와 함께 코어 시작",
|
||||
"Start Core with Tun": "Tun과 함께 코어 시작",
|
||||
"Start Option": "시작 옵션",
|
||||
"Start With System": "시스템과 함께 시작",
|
||||
"Status": "상태",
|
||||
"Succeed": "성공",
|
||||
"System": "시스템",
|
||||
"System Auto Proxy Status": "시스템 자동 프록시 상태",
|
||||
"System Config": "시스템 설정",
|
||||
"System Hotkey": "시스템 단축키",
|
||||
"System Proxy Permission": "시스템 프록시 권한",
|
||||
"System Proxy Status": "시스템 프록시 상태",
|
||||
"System Stack Type": "시스템 스택 유형",
|
||||
"TCP Fast Open": "TCP 빠른 열기",
|
||||
"TcpConcurrent": "TCP 동시성",
|
||||
"The User Agent to use when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 사용할 사용자 에이전트입니다.",
|
||||
"The expected content type to send in the `Accept` header when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 `Accept` 헤더에 보낼 예상 컨텐츠 타입입니다.",
|
||||
"Theme Color": "테마 색상",
|
||||
"Timeout (ms)": "타임아웃 (ms)",
|
||||
"Transparent Proxy": "투명 프록시",
|
||||
"URL": "URL",
|
||||
"Undefined stack": "정의되지 않은 스택",
|
||||
"Update Failed": "업데이트 실패",
|
||||
"Auto Delay Detection": "자동 지연 감지",
|
||||
"Auto Delay Detection Info": "백그라운드에서 현재 노드의 지연을 주기적으로 검사합니다"
|
||||
"Update Interval(minute)": "업데이트 간격(분)",
|
||||
"Update Setting": "업데이트 설정",
|
||||
"Update Successful": "업데이트 성공",
|
||||
"Verge Log": "Verge 로그",
|
||||
"Verge Setting": "Verge 설정",
|
||||
"View Profile-Content": "프로필-내용 보기",
|
||||
"View Profile-Merge": "프로필-병합 보기",
|
||||
"View Profile-Original": "프로필-원본 보기",
|
||||
"View Profile-Runtime": "프로필-런타임 보기",
|
||||
"View Profile-Script": "프로필-스크립트 보기",
|
||||
"Warning": "경고",
|
||||
"Web UI List": "웹 UI 목록",
|
||||
"WebDav Download": "WebDav 다운로드",
|
||||
"WebDav Password": "WebDav 비밀번호",
|
||||
"WebDav Setting": "WebDav 설정",
|
||||
"WebDav Status": "WebDav 상태",
|
||||
"WebDav URL": "WebDav URL",
|
||||
"WebDav Upload": "WebDav 업로드",
|
||||
"WebDav Username": "WebDav 사용자 이름",
|
||||
"WebUI Current Port": "웹 UI 현재 포트",
|
||||
"Yellow": "노란색"
|
||||
}
|
||||
|
||||
@@ -508,7 +508,6 @@
|
||||
"Fake IP Filter Mode": "Sahte IP Filtre Modu",
|
||||
"Enable IPv6 DNS resolution": "IPv6 DNS çözümlemesini etkinleştir",
|
||||
"Prefer H3": "H3'ü Tercih Et",
|
||||
"DNS DOH uses HTTP/3": "DNS DOH HTTP/3 kullanır",
|
||||
"Respect Rules": "Kurallara Uy",
|
||||
"DNS connections follow routing rules": "DNS bağlantıları yönlendirme kurallarını takip eder",
|
||||
"Use Hosts": "Hosts Kullan",
|
||||
@@ -575,7 +574,6 @@
|
||||
"Unknown": "Bilinmiyor",
|
||||
"Auto update disabled": "Otomatik güncelleme devre dışı",
|
||||
"Update subscription successfully": "Abonelik başarıyla güncellendi",
|
||||
"Update failed, retrying with Clash proxy...": "Güncelleme başarısız oldu, Clash vekil ile yeniden deneniyor...",
|
||||
"Update with Clash proxy successfully": "Clash vekil ile güncelleme başarılı",
|
||||
"Update failed even with Clash proxy": "Clash vekil ile bile güncelleme başarısız oldu",
|
||||
"Profile creation failed, retrying with Clash proxy...": "Profil oluşturma başarısız oldu, Clash vekil ile yeniden deneniyor...",
|
||||
@@ -616,5 +614,6 @@
|
||||
"items": "öğeler",
|
||||
"Select All": "Tümünü Seç",
|
||||
"Selected": "Seçildi",
|
||||
"Selected profiles deleted successfully": "Seçili profiller başarıyla silindi"
|
||||
"Selected profiles deleted successfully": "Seçili profiller başarıyla silindi",
|
||||
"DNS DOH uses HTTP/3": "DNS DOH HTTP/3 kullanır"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"Label-Connections": "Тоташулар",
|
||||
"Label-Rules": "Кагыйдәләр",
|
||||
"Label-Logs": "Логлар",
|
||||
"Label-Test": "Тест",
|
||||
"Label-Settings": "Көйләүләр",
|
||||
"Proxies": "Прокси",
|
||||
"Proxy Groups": "Прокси төркемнәре",
|
||||
@@ -166,7 +165,6 @@
|
||||
"Table View": "Таблица күзаллау",
|
||||
"List View": "Исемлек күзаллау",
|
||||
"Close All": "Барысын да ябу",
|
||||
"Default": "Башлангыч",
|
||||
"Download Speed": "Йөкләү тизлеге",
|
||||
"Upload Speed": "Йөкләү (чыгару) тизлеге",
|
||||
"Host": "Хост",
|
||||
@@ -197,6 +195,8 @@
|
||||
"Settings": "Көйләүләр",
|
||||
"System Setting": "Система көйләүләре",
|
||||
"Tun Mode": "Tun режимы (виртуаль челтәр адаптеры)",
|
||||
"TUN requires Service Mode": "TUN режимы хезмәт күрсәтүне таләп итә",
|
||||
"Install Service": "Хезмәтне урнаштыру",
|
||||
"Reset to Default": "Башлангычка кайтару",
|
||||
"Tun Mode Info": "Tun режимы бөтен системаның трафигын тотып ала. Аны кабызган очракта системалы проксины аерым кабызу таләп ителми.",
|
||||
"Stack": "Стек",
|
||||
@@ -280,7 +280,8 @@
|
||||
"Open UWP tool": "UWP инструментын ачу",
|
||||
"Open UWP tool Info": "Windows 8'дән башлап UWP кушымталары (Microsoft Store кебек) локаль хосттагы челтәр хезмәтләренә турыдан-туры тоташа алмый. Бу инструмент әлеге чикләүне әйләнеп узарга ярдәм итә",
|
||||
"Update GeoData": "GeoData яңарту",
|
||||
"Verge Setting": "Verge көйләүләре",
|
||||
"Verge Basic Setting": "Verge Төп көйләүләр",
|
||||
"Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр",
|
||||
"Language": "Тел",
|
||||
"Theme Mode": "Теманың режимы",
|
||||
"theme.light": "Якты",
|
||||
@@ -296,8 +297,6 @@
|
||||
"Theme Setting": "Тема көйләүләре",
|
||||
"Primary Color": "Төп төс",
|
||||
"Secondary Color": "Икенче төс",
|
||||
"Primary Text Color": "Төп текст төсе",
|
||||
"Secondary Text Color": "Икенче текст төсе",
|
||||
"Info Color": "Мәгълүмат төсе",
|
||||
"Warning Color": "Кисәтү төсе",
|
||||
"Error Color": "Хата төсе",
|
||||
@@ -367,6 +366,7 @@
|
||||
"Profile Reactivated": "Профиль яңадан активлаштырылды",
|
||||
"Only YAML Files Supported": "Фәкать YAML-файллар гына хуплана",
|
||||
"Settings Applied": "Көйләүләр кулланылды",
|
||||
"Installing Service...": "Хезмәт урнаштырыла...",
|
||||
"Service Installed Successfully": "Сервис уңышлы урнаштырылды",
|
||||
"Service Uninstalled Successfully": "Сервис уңышлы салдырылды",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "Прокси-демон эш вакыты 1 секундтан ким була алмый",
|
||||
@@ -380,7 +380,6 @@
|
||||
"Clash Core Restarted": "Clash ядросы яңадан башланды",
|
||||
"GeoData Updated": "GeoData яңартылды",
|
||||
"Currently on the Latest Version": "Сездә иң соңгы версия урнаштырылган",
|
||||
"Import subscription successful": "Подписка уңышлы импортланды",
|
||||
"WebDAV Server URL": "WebDAV сервер URL-ы (http(s)://)",
|
||||
"Username": "Кулланучы исеме",
|
||||
"Password": "Пароль",
|
||||
@@ -450,10 +449,11 @@
|
||||
"Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды",
|
||||
"Core Changed Successfully": "Ядро уңышлы алыштырылды",
|
||||
"Failed to Change Core": "Ядро алыштыру уңышсыз булды",
|
||||
"Verge Basic Setting": "Verge Төп көйләүләр",
|
||||
"Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр",
|
||||
"TUN requires Service Mode": "TUN режимы хезмәт күрсәтүне таләп итә",
|
||||
"Install Service": "Хезмәтне урнаштыру",
|
||||
"Installing Service...": "Хезмәт урнаштырыла...",
|
||||
"Service Administrator Prompt": "Clash Verge система хезмәтен яңадан урнаштыру өчен администратор хокукларын таләп итә"
|
||||
"Service Administrator Prompt": "Clash Verge система хезмәтен яңадан урнаштыру өчен администратор хокукларын таләп итә",
|
||||
"Default": "Башлангыч",
|
||||
"Import subscription successful": "Подписка уңышлы импортланды",
|
||||
"Label-Test": "Тест",
|
||||
"Primary Text Color": "Төп текст төсе",
|
||||
"Secondary Text Color": "Икенче текст төсе",
|
||||
"Verge Setting": "Verge көйләүләре"
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@
|
||||
"Socks Port": "SOCKS 代理端口",
|
||||
"Http Port": "HTTP(S) 代理端口",
|
||||
"Redir Port": "Redir 透明代理端口",
|
||||
"TPROXY Port": "TPROXY 透明代理端口",
|
||||
"Tproxy Port": "Tproxy 透明代理端口",
|
||||
"Port settings saved": "端口设置已保存",
|
||||
"Failed to save port settings": "端口设置保存失败",
|
||||
"External": "外部控制",
|
||||
@@ -426,6 +426,8 @@
|
||||
"Uninstalling Service...": "卸载服务中...",
|
||||
"Service Installed Successfully": "已成功安装服务",
|
||||
"Service Uninstalled Successfully": "已成功卸载服务",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于 1 秒",
|
||||
"Invalid Bypass Format": "无效的代理绕过格式",
|
||||
"Waiting for service to be ready...": "等待服务准备就绪...",
|
||||
"Service not ready, retrying attempt {count}/{total}...": "服务未就绪,正在重试 {{count}}/{{total}} 次...",
|
||||
"Failed to check service status, retrying attempt {count}/{total}...": "检查服务状态失败,正在重试 {{count}}/{{total}} 次...",
|
||||
@@ -436,8 +438,6 @@
|
||||
"Fallback core restart also failed: {message}": "后备内核重启也失败了: {{message}}",
|
||||
"Service is ready and core restarted": "服务已就绪,内核已重启",
|
||||
"Core restarted. Service is now available.": "内核已重启,服务现已可用",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于 1 秒",
|
||||
"Invalid Bypass Format": "无效的代理绕过格式",
|
||||
"Clash Port Modified": "Clash 端口已修改",
|
||||
"Port Conflict": "端口冲突",
|
||||
"Restart Application to Apply Modifications": "重启 Verge 以应用修改",
|
||||
@@ -628,7 +628,6 @@
|
||||
"Unknown": "未知",
|
||||
"Auto update disabled": "自动更新已禁用",
|
||||
"Update subscription successfully": "订阅更新成功",
|
||||
"Update failed, retrying with Clash proxy...": "订阅更新失败,尝试使用 Clash 代理更新",
|
||||
"Update with Clash proxy successfully": "使用 Clash 代理更新成功",
|
||||
"Update failed even with Clash proxy": "使用 Clash 代理更新也失败",
|
||||
"Profile creation failed, retrying with Clash proxy...": "订阅创建失败,尝试使用 Clash 代理创建",
|
||||
@@ -713,5 +712,8 @@
|
||||
"Allow Auto Update": "允许自动更新",
|
||||
"Menu reorder mode": "菜单排序模式",
|
||||
"Unlock menu order": "解锁菜单排序",
|
||||
"Lock menu order": "锁定菜单排序"
|
||||
"Lock menu order": "锁定菜单排序",
|
||||
"Open App Log": "应用日志",
|
||||
"Open Core Log": "内核日志",
|
||||
"TPROXY Port": "TPROXY 透明代理端口"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"Label-Logs": "日 誌",
|
||||
"Label-Unlock": "解 鎖",
|
||||
"Label-Settings": "設 定",
|
||||
"Proxies": "代理",
|
||||
"Proxy Groups": "代理組",
|
||||
"Proxy Chain Mode": "鏈式代理模式",
|
||||
"Connect": "連線",
|
||||
@@ -37,8 +38,16 @@
|
||||
"rule": "規則",
|
||||
"global": "全域",
|
||||
"direct": "直連",
|
||||
"script": "指令碼",
|
||||
"Chain Proxy": "🔗 鏈式代理",
|
||||
"Chain Proxy Config": "鏈式代理設定",
|
||||
"Proxy Rules": "代理規則",
|
||||
"Select Rules": "选择规则",
|
||||
"Click nodes in order to add to proxy chain": "依序點擊節點新增到鏈式代理中",
|
||||
"No proxy chain configured": "暫無鏈式代理設定",
|
||||
"Proxy Order": "代理順序",
|
||||
"timeout": "逾時",
|
||||
"Clear All": "全部清除",
|
||||
"script": "指令碼",
|
||||
"locate": "目前節點",
|
||||
"Delay check": "延遲測試",
|
||||
"Sort by default": "預設排序",
|
||||
@@ -144,6 +153,8 @@
|
||||
"Group Name Already Exists": "代理組名稱已存在",
|
||||
"Extend Config": "擴充覆寫設定",
|
||||
"Extend Script": "擴充指令碼",
|
||||
"Global Merge": "全域擴充覆寫設定",
|
||||
"Global Script": "全域擴充指令碼",
|
||||
"Type": "類型",
|
||||
"Name": "名稱",
|
||||
"Descriptions": "描述",
|
||||
@@ -152,6 +163,7 @@
|
||||
"Choose File": "選擇檔案",
|
||||
"Use System Proxy": "使用系統代理更新",
|
||||
"Use Clash Proxy": "使用內核代理更新",
|
||||
"Accept Invalid Certs (Danger)": "允許無效憑證(危險)",
|
||||
"Refresh": "重整",
|
||||
"Home": "首頁",
|
||||
"Select": "使用",
|
||||
@@ -159,13 +171,18 @@
|
||||
"Edit File": "編輯檔案",
|
||||
"Open File": "開啟檔案",
|
||||
"Update": "更新",
|
||||
"Update via proxy": "更新(代理)",
|
||||
"Update(Proxy)": "更新(代理)",
|
||||
"Confirm deletion": "確認刪除",
|
||||
"This operation is not reversible": "此操作無法復原",
|
||||
"Script Console": "指令碼控制台輸出",
|
||||
"To Top": "前往頂端",
|
||||
"To End": "前往末端",
|
||||
"Connections": "連線",
|
||||
"Table View": "表格檢視",
|
||||
"List View": "列表檢視",
|
||||
"Close All": "關閉全部",
|
||||
"Close All Connections": "關閉全部連線",
|
||||
"Upload": "上傳",
|
||||
"Download": "下載",
|
||||
"Download Speed": "下載速度",
|
||||
@@ -199,29 +216,46 @@
|
||||
"Test URL": "測試網址",
|
||||
"Settings": "設定",
|
||||
"System Setting": "系統設定",
|
||||
"Tun Mode": "虛擬網卡模式",
|
||||
"TUN requires Service Mode or Admin Mode": "虛擬網卡模式需要服務模式或管理員模式",
|
||||
"Tun Mode": "虛擬網路介面卡模式",
|
||||
"Install Service": "安裝服務",
|
||||
"Install Service failed": "安裝服務失敗",
|
||||
"Uninstall Service": "解除安裝服務",
|
||||
"Restart Core failed": "重新啟動內核失敗",
|
||||
"Reset to Default": "重設為預設值",
|
||||
"Tun Mode Info": "TUN(虛擬網路介面卡)模式接管系統所有流量,啟用時無需開啟系統代理",
|
||||
"TUN requires Service Mode or Admin Mode": "虛擬網路介面卡模式需要安裝服務模式或以系統管理員身分執行",
|
||||
"TUN Mode automatically disabled due to service unavailable": "由於服務不可使用,虛擬網路介面卡模式已自動停用",
|
||||
"Failed to disable TUN Mode automatically": "自動停用虛擬網路介面卡模式失敗",
|
||||
"System Proxy Enabled": "系統代理已啟用,您的應用程式將透過代理存取網路",
|
||||
"System Proxy Disabled": "系統代理已關閉,建議大多數使用者開啟此選項",
|
||||
"TUN Mode Enabled": "虛擬網卡模式已啟用,應用程式將透過虛擬網路介面卡存取網路",
|
||||
"TUN Mode Disabled": "虛擬網卡模式已關閉,適用於特殊應用程式",
|
||||
"TUN Mode Service Required": "虛擬網卡模式需要服務模式,請先安裝服務",
|
||||
"TUN Mode Intercept Info": "虛擬網卡模式可以接管所有應用程式流量,適用於不遵循系統代理設定的特殊應用程式",
|
||||
"Stack": "虛擬網卡模式堆疊",
|
||||
"Device": "TUN 網路介面卡名稱",
|
||||
"TUN Mode Enabled": "虛擬網路介面卡模式已啟用,應用程式將透過虛擬網路介面卡存取網路",
|
||||
"TUN Mode Disabled": "虛擬網路介面卡模式已關閉,適用於特殊應用程式",
|
||||
"TUN Mode Service Required": "虛擬網路介面卡模式需要服務模式,請先安裝服務",
|
||||
"TUN Mode Intercept Info": "虛擬網路介面卡模式可以接管所有應用程式流量,適用於不遵循系統代理設定的特殊應用程式",
|
||||
"Core communication error": "內核通信錯誤",
|
||||
"Rule Mode Description": "基於預設規則智慧判斷流量走向,提供更靈活的代理策略",
|
||||
"Global Mode Description": "所有流量均透過代理伺服器,適用於需要全域科學上網的場合",
|
||||
"Direct Mode Description": "所有流量不透過代理伺服器,但經過Clash內核轉發連線目標伺服器,適用於需要透過內核進行分流的特定場合",
|
||||
"Stack": "虛擬網路介面卡模式堆疊",
|
||||
"System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服務模式下使用",
|
||||
"Device": "虛擬網路介面卡名稱",
|
||||
"Auto Route": "自動設定全域路由",
|
||||
"Strict Route": "嚴格路由",
|
||||
"Auto Detect Interface": "自動偵測流量輸出介面",
|
||||
"DNS Hijack": "DNS 綁架",
|
||||
"MTU": "最大傳輸單位",
|
||||
"Service Mode": "服務模式",
|
||||
"Service Mode Info": "啟用虛擬網路介面卡模式前請先安裝服務模式,該服務啟動的內核處理程序可以獲得安裝虛擬網路介面卡(TUN 模式)的權限",
|
||||
"Current State": "当前状态",
|
||||
"pending": "等待中",
|
||||
"installed": "已安装",
|
||||
"uninstall": "未安装",
|
||||
"active": "作用中",
|
||||
"unknown": "未知",
|
||||
"Information: Please make sure that the Clash Verge Service is installed and enabled": "提示資訊:請確認 Clash Verge Service 已安裝並啟用",
|
||||
"Install": "安裝",
|
||||
"Uninstall": "解除安裝",
|
||||
"Disable Service Mode": "停用服務模式",
|
||||
"System Proxy": "系統代理",
|
||||
"System Proxy Info": "修改作業系統的代理設定,如果開啟失敗,可手動修改作業系統的代理設定",
|
||||
"System Proxy Setting": "系統代理設定",
|
||||
@@ -237,6 +271,7 @@
|
||||
"Proxy Guard Info": "開啟以防止其他軟體修改作業系統的代理設定",
|
||||
"Guard Duration": "代理守護間隔",
|
||||
"Always use Default Bypass": "始終使用預設繞過",
|
||||
"Use Bypass Check": "啟用代理繞過檢查",
|
||||
"Proxy Bypass": "代理繞過設定:",
|
||||
"Bypass": "目前繞過:",
|
||||
"Use PAC Mode": "使用 PAC 模式",
|
||||
@@ -246,6 +281,10 @@
|
||||
"Administrator mode may not support auto launch": "管理員模式可能不支援開機自啟",
|
||||
"Silent Start": "靜默啟動",
|
||||
"Silent Start Info": "程序啟動時以後台模式執行,不顯示程序面板",
|
||||
"Hover Jump Navigator": "懸浮跳轉導航",
|
||||
"Hover Jump Navigator Info": "滑鼠懸停在字母上時自動捲動到對應代理組",
|
||||
"Hover Jump Navigator Delay": "懸浮跳轉導航延遲",
|
||||
"Hover Jump Navigator Delay Info": "滑鼠懸停後觸發自動跳轉前等待的毫秒數",
|
||||
"TG Channel": "Telegram 頻道",
|
||||
"Manual": "使用手冊",
|
||||
"Github Repo": "GitHub 專案位址",
|
||||
@@ -259,13 +298,16 @@
|
||||
"Unified Delay Info": "開啟統一延遲時,會進行兩次延遲測試,以消除連線握手等帶來的不同類型節點的延遲差異",
|
||||
"Log Level": "日誌等級",
|
||||
"Log Level Info": "僅對日誌目錄 Service 資料夾下的內核日誌檔案生效",
|
||||
"Port Config": "連接埠設定",
|
||||
"Random Port": "隨機連接埠",
|
||||
"Mixed Port": "混合代理連接埠",
|
||||
"Socks Port": "SOCKS 代理連接埠",
|
||||
"Http Port": "HTTP(S) 代理連接埠",
|
||||
"Redir Port": "Redir 透明代理連接埠",
|
||||
"Tproxy Port": "TPROXY 透明代理連接埠",
|
||||
"Port settings saved": "連結埠設定已儲存",
|
||||
"Failed to save port settings": "連結埠設定儲存失敗",
|
||||
"External": "外部控制",
|
||||
"Enable External Controller": "啟用外部控制器",
|
||||
"External Controller": "外部控制器監聽位址",
|
||||
"Core Secret": "API 存取金鑰",
|
||||
"Recommended": "建議設定",
|
||||
@@ -277,7 +319,9 @@
|
||||
"Restart": "重啟內核",
|
||||
"Release Version": "正式版",
|
||||
"Alpha Version": "預覽版",
|
||||
"Please Enable Service Mode": "請先安裝並啟用服務模式",
|
||||
"Please enter your root password": "請輸入您的 root 密碼",
|
||||
"Grant": "授權",
|
||||
"Open UWP tool": "UWP 工具",
|
||||
"Open UWP tool Info": "Windows 8 開始限制 UWP 應用程式(如Microsoft Store)直接存取本機主機的網路服務,使用此工具可繞過該限制",
|
||||
"Update GeoData": "更新 GeoData",
|
||||
@@ -312,17 +356,13 @@
|
||||
"Memory Usage": "內核佔用",
|
||||
"Memory Cleanup": "點擊清理記憶體",
|
||||
"Proxy Group Icon": "代理組圖示",
|
||||
"Hover Jump Navigator": "懸浮跳轉導航",
|
||||
"Hover Jump Navigator Info": "滑鼠懸停在字母上時自動捲動到對應代理組",
|
||||
"Hover Jump Navigator Delay": "懸浮跳轉導航延遲",
|
||||
"Hover Jump Navigator Delay Info": "滑鼠懸停後觸發自動跳轉前等待的毫秒數",
|
||||
"Nav Icon": "導覽列圖示",
|
||||
"Monochrome": "單色圖示",
|
||||
"Colorful": "彩色圖示",
|
||||
"Tray Icon": "系統匣圖示",
|
||||
"Common Tray Icon": "一般系統匣圖示",
|
||||
"System Proxy Tray Icon": "系統代理系統匣圖示",
|
||||
"Tun Tray Icon": "虛擬網卡模式系統匣圖示",
|
||||
"Tun Tray Icon": "虛擬網路介面卡模式系統匣圖示",
|
||||
"Miscellaneous": "雜項設定",
|
||||
"App Log Level": "應用程式日誌等級",
|
||||
"Auto Close Connections": "自動關閉連線",
|
||||
@@ -347,7 +387,7 @@
|
||||
"clash_mode_global": "全域模式",
|
||||
"clash_mode_direct": "直連模式",
|
||||
"toggle_system_proxy": "開啟/關閉系統代理",
|
||||
"toggle_tun_mode": "開啟/關閉 虛擬網卡模式",
|
||||
"toggle_tun_mode": "開啟/關閉 虛擬網路介面卡模式",
|
||||
"entry_lightweight_mode": "進入輕量模式",
|
||||
"Backup Setting": "備份設定",
|
||||
"Backup Setting Info": "支援本機或 WebDAV 方式備份配置檔案",
|
||||
@@ -362,6 +402,7 @@
|
||||
"Break Change Update Error": "此版本為重大更新,不支援應用程式內更新,請解除安裝後手動下載安裝",
|
||||
"Open Dev Tools": "開發人員工具",
|
||||
"Export Diagnostic Info": "匯出診斷資訊",
|
||||
"Export Diagnostic Info For Issue Reporting": "匯出診斷資訊用於問題回報",
|
||||
"Exit": "離開",
|
||||
"Verge Version": "Verge 版本",
|
||||
"ReadOnly": "唯讀",
|
||||
@@ -374,26 +415,37 @@
|
||||
"Profile Imported Successfully": "匯入設定檔成功",
|
||||
"Profile Switched": "訂閱已切換",
|
||||
"Profile Reactivated": "訂閱已啟用",
|
||||
"Profile switch interrupted by new selection": "配置切換被新的選擇中斷",
|
||||
"Only YAML Files Supported": "僅支援 YAML 檔案",
|
||||
"Settings Applied": "設定已套用",
|
||||
"Stopping Core...": "內核停止中...",
|
||||
"Restarting Core...": "內核重啟中...",
|
||||
"Installing Service...": "安裝服務中...",
|
||||
"Uninstall Service": "解除安裝服務",
|
||||
"Service Installed Successfully": "已成功安裝服務",
|
||||
"Service is ready and core restarted": "服務已就緒,內核已重啟",
|
||||
"Core restarted. Service is now available.": "內核已重啟,服務已就緒",
|
||||
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "服務已就緒,但內核重啟可能存在問題或服務已不可用。請檢查。",
|
||||
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "服務安裝或內核重啟遇到問題。服務可能不可用。請檢查系統日誌。",
|
||||
"Uninstalling Service...": "服務解除安裝中...",
|
||||
"Waiting for service to be ready...": "等待服務就緒...",
|
||||
"Service Installed Successfully": "已成功安裝服務",
|
||||
"Service Uninstalled Successfully": "已成功解除安裝服務",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守護間隔時間不得低於 1 秒",
|
||||
"Invalid Bypass Format": "無效的代理繞過格式",
|
||||
"Waiting for service to be ready...": "等待服務就緒...",
|
||||
"Service not ready, retrying attempt {count}/{total}...": "服務未就緒,正在重試 {{count}}/{{total}} 次...",
|
||||
"Failed to check service status, retrying attempt {count}/{total}...": "檢查服務狀態失敗,正在重試 {{count}}/{{total}} 次...",
|
||||
"Service did not become ready after attempts. Proceeding with core restart.": "服務在嘗試後仍未就緒,正在重新啟動內核。",
|
||||
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "服務已就緒,但內核重啟可能存在問題或服務已不可用。請檢查。",
|
||||
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "服務安裝或內核重啟遇到問題。服務可能不可用。請檢查系統日誌。",
|
||||
"Attempting to restart core as a fallback...": "嘗試重新啟動內核作為備援方案...",
|
||||
"Fallback core restart also failed: {message}": "被園內核重新啟動也失敗了:{{message}}",
|
||||
"Service is ready and core restarted": "服務已就緒,內核已重啟",
|
||||
"Core restarted. Service is now available.": "內核已重啟,服務已就緒",
|
||||
"Clash Port Modified": "Clash 連結埠已修改",
|
||||
"Port Conflict": "連結埠衝突",
|
||||
"Restart Application to Apply Modifications": "重新啟動 Verge 以套用修改",
|
||||
"External Controller Address Modified": "外部控制器監聽位址已修改",
|
||||
"Permissions Granted Successfully for _clash Core": "{{core}} 內核授權成功",
|
||||
"Core Version Updated": "內核版本已更新",
|
||||
"Clash Core Restarted": "已重啟 Clash 內核",
|
||||
"GeoData Updated": "已更新 GeoData",
|
||||
"Currently on the Latest Version": "目前已是最新版本",
|
||||
"Already Using Latest Core": "已是最新內核版本",
|
||||
"Import Subscription Successful": "匯入訂閱成功",
|
||||
"WebDAV Server URL": "WebDAV 伺服器位址 http(s)://",
|
||||
"Username": "使用者名稱",
|
||||
@@ -411,6 +463,7 @@
|
||||
"Invalid WebDAV URL": "無效的 WebDAV 伺服器位址格式",
|
||||
"Username Required": "使用者名稱不能為空",
|
||||
"Password Required": "密碼不能為空",
|
||||
"Failed to Fetch Backups": "取得備份檔案失敗",
|
||||
"WebDAV Config Saved": "WebDAV 配置儲存成功",
|
||||
"WebDAV Config Save Failed": "儲存 WebDAV 配置失敗: {{error}}",
|
||||
"Backup Created": "備份建立成功",
|
||||
@@ -424,14 +477,33 @@
|
||||
"Export Backup": "匯出備份",
|
||||
"Restore Backup": "還原備份",
|
||||
"Backup Time": "備份時間",
|
||||
"Confirm to delete this backup file?": "確認是否刪除此備份檔案嗎?",
|
||||
"Confirm to restore this backup file?": "確認還原此份檔案嗎?",
|
||||
"Restore Success, App will restart in 1s": "還原成功,應用程式將在 1 秒後重啟",
|
||||
"Failed to fetch backup files": "取得備份檔案失敗",
|
||||
"Profile": "配置檔案",
|
||||
"Profile": "配置",
|
||||
"Help": "幫助",
|
||||
"About": "關於",
|
||||
"Theme": "主題",
|
||||
"Main Window": "主視窗",
|
||||
"Group Icon": "群組圖示",
|
||||
"Menu Icon": "選單圖示",
|
||||
"PAC File": "PAC 檔案",
|
||||
"Web UI": "網頁介面",
|
||||
"Hotkeys": "快捷键",
|
||||
"Verge Mixed Port": "Verge 混合連結埠",
|
||||
"Verge Socks Port": "Verge SOCKS 連結埠",
|
||||
"Verge Redir Port": "Verge 重新導向連結埠",
|
||||
"Verge Tproxy Port": "Verge 透明代理連結埠",
|
||||
"Verge Port": "Verge 連結埠",
|
||||
"Verge HTTP Enabled": "Verge HTTP 已啟用",
|
||||
"WebDAV URL": "WebDAV 位址",
|
||||
"WebDAV Username": "WebDAV 用戶名",
|
||||
"WebDAV Password": "WebDAV 密碼",
|
||||
"Dashboard": "儀表板",
|
||||
"Restart App": "重啟應用程式",
|
||||
"Restart Clash Core": "重啟 Clash 內核",
|
||||
"TUN Mode": "虛擬網卡模式",
|
||||
"TUN Mode": "虛擬網路介面卡模式",
|
||||
"Copy Env": "複製環境變數",
|
||||
"Conf Dir": "配置目錄",
|
||||
"Core Dir": "內核目錄",
|
||||
@@ -471,6 +543,10 @@
|
||||
"Merge File Mapping Error": "覆寫檔案映射錯誤,變更已撤銷",
|
||||
"Merge File Key Error": "覆寫檔案鍵錯誤,變更已撤銷",
|
||||
"Merge File Error": "覆寫檔案錯誤,變更已撤銷",
|
||||
"Validate YAML File": "驗證YAML檔案",
|
||||
"Validate Merge File": "驗證覆寫檔案",
|
||||
"Validation Success": "驗證成功",
|
||||
"Validation Failed": "驗證失敗",
|
||||
"Service Administrator Prompt": "Clash Verge 需要管理員權限安裝系統服務",
|
||||
"DNS Settings": "DNS 設定",
|
||||
"DNS settings saved": "DNS 設定已儲存",
|
||||
@@ -517,11 +593,14 @@
|
||||
"Hosts Settings": "Hosts 設定",
|
||||
"Hosts": "Hosts",
|
||||
"Custom domain to IP or domain mapping": "自訂網域到 IP 或網域的映射,用逗號分隔",
|
||||
"Enable Alpha Channel": "啟用 Alpha 頻道",
|
||||
"Alpha versions may contain experimental features and bugs": "Alpha 版本可能內含實驗性功能和已知問題,常有不穩定的情況發生。",
|
||||
"Home Settings": "首頁設定",
|
||||
"Profile Card": "訂閱卡",
|
||||
"Current Proxy Card": "目前代理卡",
|
||||
"Network Settings Card": "網路設定卡",
|
||||
"Proxy Mode Card": "代理模式卡",
|
||||
"Clash Mode Card": "Clash模式卡",
|
||||
"Traffic Stats Card": "流量統計卡",
|
||||
"Clash Info Cards": "Clash 資訊卡",
|
||||
"System Info Cards": "系統資訊卡",
|
||||
@@ -538,6 +617,7 @@
|
||||
"Running Mode": "執行模式",
|
||||
"Sidecar Mode": "使用者模式",
|
||||
"Administrator Mode": "管理員模式",
|
||||
"Administrator + Service Mode": "系統管理員 + 服務模式",
|
||||
"Last Check Update": "最後檢查更新",
|
||||
"Click to import subscription": "點擊匯入訂閱",
|
||||
"Last Update failed": "上次更新失敗",
|
||||
@@ -546,7 +626,6 @@
|
||||
"Unknown": "未知",
|
||||
"Auto update disabled": "自動更新已停用",
|
||||
"Update subscription successfully": "訂閱更新成功",
|
||||
"Update failed, retrying with Clash proxy...": "訂閱更新失敗,嘗試使用 Clash 代理更新",
|
||||
"Update with Clash proxy successfully": "使用 Clash 代理更新成功",
|
||||
"Update failed even with Clash proxy": "使用 Clash 代理更新也失敗",
|
||||
"Profile creation failed, retrying with Clash proxy...": "訂閱建立失敗,嘗試使用 Clash 代理建立",
|
||||
@@ -577,11 +656,60 @@
|
||||
"Completed": "檢測完成",
|
||||
"Disallowed ISP": "不允許的網際網路服務供應商",
|
||||
"Originals Only": "僅限原創",
|
||||
"No (IP Banned By Disney+)": "不支援(IP被Disney+禁止)",
|
||||
"Unsupported Country/Region": "不支援的國家/地區",
|
||||
"Failed (Network Connection)": "測試失敗(網路連線問題)",
|
||||
"DashboardToggledTitle": "儀錶板已切換",
|
||||
"DashboardToggledBody": "已透過快速鍵切換儀錶板顯示狀態",
|
||||
"ClashModeChangedTitle": "Clash模式切換",
|
||||
"ClashModeChangedBody": "已切換為 {mode} 模式",
|
||||
"SystemProxyToggledTitle": "系統代理切換",
|
||||
"SystemProxyToggledBody": "已透過快速鍵切換系統代理狀態",
|
||||
"TunModeToggledTitle": "TUN模式切換",
|
||||
"TunModeToggledBody": "已透過快速鍵切換TUN模式",
|
||||
"LightweightModeEnteredTitle": "輕量模式",
|
||||
"LightweightModeEnteredBody": "已透過快速鍵進入輕量模式",
|
||||
"AppQuitTitle": "應用程式退出",
|
||||
"AppQuitBody": "已透過快速鍵退出應用程式",
|
||||
"AppHiddenTitle": "應用程式隱藏",
|
||||
"AppHiddenBody": "已透過快速鍵隱藏應用程式視窗",
|
||||
"Invalid Profile URL": "無效的訂閱網址,請輸入以 http:// 或 https:// 開頭的位址",
|
||||
"Saved Successfully": "儲存成功",
|
||||
"External Cors": "外部跨來源資源共享",
|
||||
"Enable one-click CORS for external API. Click to toggle CORS": "設定內核跨來源存取,點擊切換跨來源資源共享是否啟用",
|
||||
"External Cors Settings": "外部跨來源資源共享設定",
|
||||
"External Cors Configuration": "外部跨來源資源共享設定",
|
||||
"Allow private network access": "允許專用網路存取",
|
||||
"Allowed Origins": "允許的來源",
|
||||
"Please enter a valid url": "請輸入有效的網址",
|
||||
"Add": "新增",
|
||||
"Always included origins: {{urls}}": "始終包含來源:{{urls}}",
|
||||
"Invalid regular expression": "無效的正規表示式",
|
||||
"Copy Version": "複製Verge版本號",
|
||||
"Version copied to clipboard": "Verge版本已複製到剪貼簿",
|
||||
"Controller address cannot be empty": "控制器位址不能為空",
|
||||
"Secret cannot be empty": "存取金鑰不能為空",
|
||||
"Configuration saved successfully": "設定儲存完成",
|
||||
"Failed to save configuration": "設定儲存失敗",
|
||||
"Controller address copied to clipboard": "API 連接埠已複製到剪貼簿",
|
||||
"Secret copied to clipboard": "API 密鑰已複製到剪貼簿",
|
||||
"Copy to clipboard": "點擊我複製到剪貼簿",
|
||||
"Port Config": "連接埠設定",
|
||||
"Configuration saved successfully": "配置儲存完成",
|
||||
"Enable one-click random API port and key. Click to randomize the port and key": "開啟一鍵隨機 API 連接埠和密鑰,點擊即可隨機化連接埠和密鑰"
|
||||
"Secret copied to clipboard": "API 金鑰已複製到剪貼簿",
|
||||
"Saving...": "儲存中...",
|
||||
"Proxy node already exists in chain": "該節點已在鏈式代理表中",
|
||||
"Detection timeout or failed": "檢測逾時或失敗",
|
||||
"Batch Operations": "批次操作",
|
||||
"Delete Selected Profiles": "刪除選取訂閱",
|
||||
"Deselect All": "取消選取",
|
||||
"Done": "完成",
|
||||
"items": "項目",
|
||||
"Select All": "全選",
|
||||
"Selected": "已選取",
|
||||
"Selected profiles deleted successfully": "選取的訂閱已成功刪除",
|
||||
"Prefer System Titlebar": "優先使用系統標題欄",
|
||||
"App Log Max Size": "應用程式日誌最大大小",
|
||||
"App Log Max Count": "應用程式日誌最大數量",
|
||||
"Allow Auto Update": "允許自動更新",
|
||||
"Menu reorder mode": "選單排序模式",
|
||||
"Unlock menu order": "解鎖選單排序",
|
||||
"Lock menu order": "鎖定選單排序",
|
||||
"TPROXY Port": "TPROXY 透明代理連接埠"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user