Merge branch 'fix-migrate-tauri2-errors'

* fix-migrate-tauri2-errors: (288 commits)

# Conflicts:
#	.github/ISSUE_TEMPLATE/bug_report.yml
This commit is contained in:
huzibaca 2024-11-24 00:14:46 +08:00
commit 922020c57a
123 changed files with 8721 additions and 5789 deletions

5
.cargo/config.toml Normal file
View File

@ -0,0 +1,5 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

View File

@ -1,4 +0,0 @@
FROM rust:bookworm
COPY entrypoint.sh /entrypoint.sh
RUN chmod a+x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -1,14 +0,0 @@
name: "Build for Linux"
branding:
icon: user-check
color: gray-dark
inputs:
target:
required: true
description: "Rust Target"
runs:
using: "docker"
image: "Dockerfile"
args:
- ${{ inputs.target }}

View File

@ -1,5 +0,0 @@
pnpm install
pnpm check $INPUT_TARGET
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
pnpm build --target $INPUT_TARGET

View File

@ -1,17 +0,0 @@
#!/bin/bash
wget https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-x64.tar.xz
tar -Jxvf ./node-v20.10.0-linux-x64.tar.xz
export PATH=$(pwd)/node-v20.10.0-linux-x64/bin:$PATH
npm install pnpm -g
rustup target add "$INPUT_TARGET"
echo "rustc version: $(rustc --version)"
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
apt-get update
apt-get install -y libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig
fi
bash .github/build-for-linux/build.sh

View File

@ -22,14 +22,14 @@ jobs:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: i686-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
@ -37,7 +37,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@1.77.0
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@ -49,6 +49,12 @@ jobs:
cache-all-crates: true
cache-on-failure: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install Node
uses: actions/setup-node@v4
with:
@ -92,26 +98,107 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
alpha-for-linux:
alpha-for-linux-arm:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-22.04
target: aarch64-unknown-linux-gnu
arch: arm64
- os: ubuntu-22.04
target: armv7-unknown-linux-gnueabihf
arch: armhf
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
EOF
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
sudo mv /tmp/sources.list /etc/apt/sources.list
sudo dpkg --add-architecture ${{ matrix.arch }}
sudo apt update
sudo apt install -y \
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
libayatana-appindicator3-dev:${{ matrix.arch }} \
libssl-dev:${{ matrix.arch }} \
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: "Install aarch64 tools"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu
- name: "Install armv7 tools"
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
uses: ./.github/build-for-linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
elif [ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]; then
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf/
fi
pnpm build --target ${{ matrix.target }}
env:
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
target: ${{ matrix.target }}
- name: Get Version
run: |
@ -140,9 +227,6 @@ jobs:
- os: windows-latest
target: x86_64-pc-windows-msvc
arch: x64
- os: windows-latest
target: i686-pc-windows-msvc
arch: x86
- os: windows-latest
target: aarch64-pc-windows-msvc
arch: arm64
@ -219,7 +303,7 @@ jobs:
update_tag:
name: Update tag
runs-on: ubuntu-latest
needs: [alpha, alpha-for-linux, alpha-for-fixed-webview2]
needs: [alpha, alpha-for-linux-arm, alpha-for-fixed-webview2]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@ -240,30 +324,30 @@ jobs:
cat > release.txt << 'EOF'
## 我应该下载哪个版本?
### MacOS (提示文件损坏或开发者无法验证请查看下面FAQ)
### MacOS
- MacOS intel芯片: x64.dmg
- MacOS apple M芯片: aarch64.dmg
### Linux
- Linux 64位: amd64.deb/amd64.rpm
- Linux arm64 architecture: arm64.deb/aarch64.rpm
- Linux armv7架构: armhf.deb/armhfp.rpm
### Windows (Win7 用户请查看下面FAQ中的解决方案)
#### 正常版本(推荐)
- 64位: x64-setup.exe
- 32位: x86-setup.exe
- arm64架构: arm64-setup.exe
#### 便携版(不推荐使用,无法自动更新)
- 64位: x64_portable.zip
- 32位: x86_portable.zip
- arm64架构: arm64_portable.zip
#### 内置Webview2版(体积较大仅在企业版系统或Win7无法安装webview2时使用)
#### 便携版问题很多不再提供
#### 内置Webview2版(体积较大仅在企业版系统或无法安装webview2时使用)
- 64位: x64_fixed_webview2-setup.exe
- 32位: x86_fixed_webview2-setup.exe
- arm64架构: arm64_fixed_webview2-setup.exe
### FAQ
- [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF

View File

@ -33,7 +33,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@1.77.0
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@ -65,6 +65,14 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}

View File

@ -15,14 +15,14 @@ jobs:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: i686-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@1.77.0
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@ -41,10 +41,16 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@ -61,8 +67,8 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
@ -82,38 +88,114 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-for-linux:
release-for-linux-arm:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: i686-unknown-linux-gnu
- os: ubuntu-latest
- os: ubuntu-22.04
target: aarch64-unknown-linux-gnu
- os: ubuntu-latest
arch: arm64
- os: ubuntu-22.04
target: armv7-unknown-linux-gnueabihf
arch: armhf
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
EOF
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
sudo mv /tmp/sources.list /etc/apt/sources.list
sudo dpkg --add-architecture ${{ matrix.arch }}
sudo apt update
sudo apt install -y \
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
libayatana-appindicator3-dev:${{ matrix.arch }} \
libssl-dev:${{ matrix.arch }} \
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: "Install aarch64 tools"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu
- name: "Install armv7 tools"
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
uses: ./.github/build-for-linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
elif [ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]; then
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf/
fi
pnpm build --target ${{ matrix.target }}
env:
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
target: ${{ matrix.target }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release
uses: softprops/action-gh-release@v2
@ -156,7 +238,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@ -181,8 +263,8 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
@ -209,7 +291,7 @@ jobs:
release-update:
runs-on: ubuntu-latest
needs: [release, release-for-linux]
needs: [release, release-for-linux-arm]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@ -217,7 +299,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@ -242,7 +324,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm

View File

@ -17,15 +17,28 @@ If you're a Windows user, you may need to perform some additional steps:
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
- The gnu `patch` tool should be installed
### Install Node.js Packages
After installing Rust and Node.js, install the necessary Node.js packages:
When you setup `Rust` environment, Only use toolchain with `Windows MSVC` , to change settings follow command:
```shell
pnpm i
rustup target add x86_64-pc-windows-msvc
rustup set default-host x86_64-pc-windows-msvc
```
### Download the Clash Binary
### Install Node.js Package
After installing Rust and Node.js, install the necessary Node.js and Node Package Manager:
```shell
npm install pnpm -g
```
### Install Dependencies
```shell
pnpm install
```
### Download the Clash Mihomo Core Binary
You have two options for downloading the clash binary:
@ -49,12 +62,14 @@ pnpm dev:diff
### Build the Project
If you want to build the project, use:
To build this project:
```shell
pnpm build
pnpm run build
```
The `Artifacts` will display in the `log` in the Terminal.
## Contributing Your Changes
Once you have made your changes:

View File

@ -25,7 +25,7 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
---
### TG Group: [@clash_verge_rev](https://t.me/clash_verge_rev)
### TG 频道: [@clash_verge_rev](https://t.me/clash_verge_re)
## Promotion

View File

@ -1,9 +1,67 @@
## v2.0.0
### Notice
- 历时3个月的紧密开发与严格测试稳定版2.0.0终于发布了巨量改进与性能、稳定性提升目前Clash Verge Rev已经有了比肩cfw的健壮性而且更强大易用
- 由于更改了服务安装逻辑Mac/Linux 首次安装需要输入 2 遍系统密码卸载和安装服务,以后可以丝滑使用 tun(虚拟网卡)模式
- 因 Tauri 2.0 底层 bug关闭窗口暂时修改为最小化功能
### Breaking changes
- 重大框架升级:使用 Tauri 2.0(巨量改进与性能提升)
- 敬请测试,出现 bug 到 issues 中提出
- 强烈建议完全删除 1.x 老版本再安装此版本
- 出现 bug 到 issues 中提出以后不再接受1.x版本的bug反馈。
### Features
- Meta(mihomo)内核升级 1.18.10
- Win 下的系统代理替换为 Shadowsocks/CFW/v2rayN 等成熟的 sysproxy.exe 方案,解决拨号/VPN 环境下无法设置系统代理的问题
- 服务模式改进为启动软件时自动安装TUN 模式可自由开启不再限制于服务模式
- Mac 下可用 URL Scheme 导入订阅
- 可使用 Ctrl(cmd)+Q 快捷键退出程序
- 成功导入订阅的提示消息
- 能自动选中新导入的订阅
- 日志加入颜色区分
- 改进多处文本表述
- 加入图标 svg 格式检测
- 增加更多 app 调试日志
- 添加 MacOS 下白色桌面的 tray 黑色配色但会代理系统代理、tun 模式图标失效的问题)
- 增加 Webdav 备份功能
- 添加统一延迟的设置开关
- 添加 Windows 下自动检测并下载 vc runtime 的功能
- 支持显示 mux 和 mptcp 的节点标识
- 延迟测试连接更换 https 的 cp.cloudflare.com/generate_204 以防止机场劫持(关闭统一延迟的情况下延迟测试结果会有所增加)。
- 重构日志记录逻辑可以收集和筛选所有日志类型了之前无法记录debug的日志类型
### Performance
- 优化及重构内核启动管理逻辑
- 优化 TUN 启动逻辑
- 重构和优化 app_handle
- 重构系统代理绕过逻辑
- 移除无用的 PID 创建逻辑
- 优化系统 DNS 设置逻辑
- 后端实现窗口控制
- 重构 MacOS 下的 DNS 设置逻辑
### Bugs Fixes
- 修复已有多个订阅导入新订阅会跳选订阅的问题
- 修复多个 Linux 下的 bug, Tun 模式在 Linux 下目前工作正常
- 修复 Linux wayland 下任务栏图标缺失的问题
- 修复 Linux KDE 桌面环境无法启动的问题
- 移除多余退出变量和钩子
- 修复 MacOS 下 tray 菜单重启 app 失效的问题
- 修复某些特定配置文件载入失败的问题
- 修复 MacOS 下 tun 模式 fakeip 不生效的问题
- 修复 Linux 下 关闭 tun 模式文件报错的问题
- 修复快捷键设置的相关 bug
- 修复 Win 下点左键菜单闪现的问题Mac 下的操作逻辑相反,默认情况下不管点左/右键均会打开菜单,闪现不属于 bug
### Known issues
- Windows 下窗口大小无法记忆(等待上游修复)
- Webdav 备份因为安全性和兼容性问题,暂不支持同步 Webdav 服务器地址和登录信息;跨平台配置同步
---

View File

@ -3,95 +3,96 @@
"version": "2.0.0",
"license": "GPL-3.0-only",
"scripts": {
"dev": "tauri dev",
"dev:diff": "tauri dev -f verge-dev",
"build": "tauri build",
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
"tauri": "tauri",
"web:dev": "vite",
"web:build": "tsc && vite build",
"web:build": "tsc --noEmit && vite build",
"web:serve": "vite preview",
"check": "node scripts/check.mjs",
"updater": "node scripts/updater.mjs",
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
"portable": "node scripts/portable.mjs",
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"prepare": "husky install"
"prepare": "husky"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.16.0",
"@mui/icons-material": "^6.1.6",
"@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.16.0",
"@mui/x-data-grid": "^7.9.0",
"@tauri-apps/api": "2.0.0-rc.4",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
"@tauri-apps/plugin-dialog": "^2.0.0-rc",
"@tauri-apps/plugin-fs": "^2.0.0-rc",
"@tauri-apps/plugin-global-shortcut": "^2.0.0-rc",
"@tauri-apps/plugin-notification": "^2.0.0-rc",
"@tauri-apps/plugin-process": "^2.0.0-rc",
"@tauri-apps/plugin-shell": "^2.0.0-rc",
"@tauri-apps/plugin-updater": "^2.0.0-rc",
"@mui/material": "^6.1.6",
"@mui/x-data-grid": "^7.22.2",
"@tauri-apps/api": "2.1.1",
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-fs": "^2.0.2",
"@tauri-apps/plugin-global-shortcut": "^2.0.0",
"@tauri-apps/plugin-notification": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"@tauri-apps/plugin-updater": "^2.0.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.8.0",
"axios": "^1.7.2",
"dayjs": "1.11.5",
"foxact": "^0.2.35",
"i18next": "^23.11.5",
"ahooks": "^3.8.1",
"axios": "^1.7.7",
"cli-color": "^2.0.4",
"dayjs": "1.11.13",
"foxact": "^0.2.41",
"glob": "^11.0.0",
"i18next": "^23.16.5",
"js-base64": "^3.7.7",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"meta-json-schema": "1.18.6",
"monaco-editor": "^0.49.0",
"monaco-yaml": "^5.2.0",
"nanoid": "^5.0.7",
"peggy": "^4.0.3",
"monaco-editor": "^0.52.0",
"monaco-yaml": "^5.2.3",
"nanoid": "^5.0.8",
"peggy": "^4.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.52.1",
"react-i18next": "^13.5.0",
"react-error-boundary": "^4.1.2",
"react-hook-form": "^7.53.2",
"react-i18next": "^15.1.1",
"react-markdown": "^9.0.1",
"react-monaco-editor": "^0.55.0",
"react-router-dom": "^6.24.1",
"react-monaco-editor": "^0.56.2",
"react-router-dom": "^6.28.0",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.7.11",
"react-virtuoso": "^4.12.0",
"sockette": "^2.0.6",
"swr": "^2.2.5",
"tar": "^6.2.1",
"types-pac": "^1.0.2"
"tar": "^7.4.3",
"types-pac": "^1.0.3",
"zustand": "^5.0.1"
},
"devDependencies": {
"@actions/github": "^5.1.1",
"@tauri-apps/cli": "2.0.0-rc.8",
"@types/fs-extra": "^9.0.13",
"@actions/github": "^6.0.0",
"@tauri-apps/cli": "2.1.0",
"@types/js-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-transition-group": "^4.4.10",
"@vitejs/plugin-legacy": "^5.4.1",
"@vitejs/plugin-react": "^4.3.1",
"adm-zip": "^0.5.14",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-transition-group": "^4.4.11",
"@vitejs/plugin-legacy": "^5.4.3",
"@vitejs/plugin-react": "^4.3.3",
"adm-zip": "^0.5.16",
"cross-env": "^7.0.3",
"fs-extra": "^11.2.0",
"https-proxy-agent": "^5.0.1",
"husky": "^7.0.4",
"https-proxy-agent": "^7.0.5",
"husky": "^9.1.7",
"meta-json-schema": "^1.18.10",
"node-fetch": "^3.3.2",
"prettier": "^2.8.8",
"pretty-quick": "^3.3.1",
"sass": "^1.77.6",
"terser": "^5.31.1",
"typescript": "^5.5.3",
"vite": "^5.3.3",
"prettier": "^3.3.3",
"pretty-quick": "^4.0.0",
"sass": "^1.81.0",
"terser": "^5.36.0",
"typescript": "^5.6.3",
"vite": "^5.4.11",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
"vite-plugin-svgr": "^4.3.0"
},
"prettier": {
"tabWidth": 2,
@ -99,5 +100,6 @@
"singleQuote": false,
"endOfLine": "lf"
},
"packageManager": "pnpm@9.1.4"
"type": "module",
"packageManager": "pnpm@9.13.2"
}

5384
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,14 @@
import fs from "fs-extra";
import fs from "fs";
import fsp from "fs/promises";
import zlib from "zlib";
import tar from "tar";
import { extract } from "tar";
import path from "path";
import AdmZip from "adm-zip";
import fetch from "node-fetch";
import proxyAgent from "https-proxy-agent";
import { execSync } from "child_process";
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
import { glob } from "glob";
const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
@ -91,9 +94,9 @@ async function getLatestAlphaVersion() {
});
let v = await response.text();
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest alpha version: ${META_ALPHA_VERSION}`);
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`);
} catch (error) {
console.error("Error fetching latest alpha version:", error.message);
log_error("Error fetching latest alpha version:", error.message);
process.exit(1);
}
}
@ -138,9 +141,9 @@ async function getLatestReleaseVersion() {
});
let v = await response.text();
META_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest release version: ${META_VERSION}`);
log_info(`Latest release version: ${META_VERSION}`);
} catch (error) {
console.error("Error fetching latest release version:", error.message);
log_error("Error fetching latest release version:", error.message);
process.exit(1);
}
}
@ -150,13 +153,13 @@ async function getLatestReleaseVersion() {
*/
if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
@ -205,44 +208,44 @@ async function resolveSidecar(binInfo) {
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
const sidecarPath = path.join(sidecarDir, targetFile);
await fs.mkdirp(sidecarDir);
if (!FORCE && (await fs.pathExists(sidecarPath))) return;
await fsp.mkdir(sidecarDir, { recursive: true });
if (!FORCE && fs.existsSync(sidecarPath)) return;
const tempDir = path.join(TEMP_DIR, name);
const tempZip = path.join(tempDir, zipFile);
const tempExe = path.join(tempDir, exeFile);
await fs.mkdirp(tempDir);
await fsp.mkdir(tempDir, { recursive: true });
try {
if (!(await fs.pathExists(tempZip))) {
if (!fs.existsSync(tempZip)) {
await downloadFile(downloadURL, tempZip);
}
if (zipFile.endsWith(".zip")) {
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
log_debug(`"${name}" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
await fs.rename(tempExe, sidecarPath);
console.log(`[INFO]: "${name}" unzip finished`);
await fsp.rename(tempExe, sidecarPath);
log_success(`unzip finished: "${name}"`);
} else if (zipFile.endsWith(".tgz")) {
// tgz
await fs.mkdirp(tempDir);
await tar.extract({
await fsp.mkdir(tempDir, { recursive: true });
await extract({
cwd: tempDir,
file: tempZip,
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
});
const files = await fs.readdir(tempDir);
console.log(`[DEBUG]: "${name}" files in tempDir:`, files);
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 fs.rename(extractedFilePath, sidecarPath);
console.log(`[INFO]: "${name}" file renamed to "${sidecarPath}"`);
await fsp.rename(extractedFilePath, sidecarPath);
log_success(`"${name}" file renamed to "${sidecarPath}"`);
execSync(`chmod 755 ${sidecarPath}`);
console.log(`[INFO]: "${name}" chmod binary finished`);
log_success(`chmod binary finished: "${name}"`);
} else {
throw new Error(`Expected file not found in ${tempDir}`);
}
@ -252,16 +255,15 @@ async function resolveSidecar(binInfo) {
const writeStream = fs.createWriteStream(sidecarPath);
await new Promise((resolve, reject) => {
const onError = (error) => {
console.error(`[ERROR]: "${name}" gz failed:`, error.message);
log_error(`"${name}" gz failed:`, error.message);
reject(error);
};
readStream
.pipe(zlib.createGunzip().on("error", onError))
.pipe(writeStream)
.on("finish", () => {
console.log(`[INFO]: "${name}" gunzip finished`);
execSync(`chmod 755 ${sidecarPath}`);
console.log(`[INFO]: "${name}" chmod binary finished`);
log_success(`chmod binary finished: "${name}"`);
resolve();
})
.on("error", onError);
@ -269,35 +271,58 @@ async function resolveSidecar(binInfo) {
}
} catch (err) {
// 需要删除文件
await fs.remove(sidecarPath);
await fsp.rm(sidecarPath, { recursive: true, force: true });
throw err;
} finally {
// delete temp dir
await fs.remove(tempDir);
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 } = binInfo;
const { file, downloadURL, localPath } = binInfo;
const resDir = path.join(cwd, "src-tauri/resources");
const targetPath = path.join(resDir, file);
if (!FORCE && (await fs.pathExists(targetPath))) return;
if (!FORCE && fs.existsSync(targetPath)) return;
await fs.mkdirp(resDir);
if (downloadURL) {
await fsp.mkdir(resDir, { recursive: true });
await downloadFile(downloadURL, targetPath);
}
console.log(`[INFO]: ${file} finished`);
if (localPath) {
await fs.copyFile(localPath, targetPath, (err) => {
if (err) {
console.error("Error copying file:", err);
} else {
console.log("File was copied successfully");
}
});
log_debug(`copy file finished: "${localPath}"`);
}
log_success(`${file} finished`);
}
/**
* download file and save to `path`
*/
async function downloadFile(url, path) {
*/ async function downloadFile(url, path) {
const options = {};
const httpProxy =
@ -316,9 +341,9 @@ async function downloadFile(url, path) {
headers: { "Content-Type": "application/octet-stream" },
});
const buffer = await response.arrayBuffer();
await fs.writeFile(path, new Uint8Array(buffer));
await fsp.writeFile(path, new Uint8Array(buffer));
console.log(`[INFO]: download finished "${url}"`);
log_success(`download finished: ${url}`);
}
// SimpleSC.dll
@ -329,43 +354,46 @@ const resolvePlugin = async () => {
const tempDir = path.join(TEMP_DIR, "SimpleSC");
const tempZip = path.join(
tempDir,
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip"
"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 pluginPath = path.join(pluginDir, "SimpleSC.dll");
await fs.mkdirp(pluginDir);
await fs.mkdirp(tempDir);
if (!FORCE && (await fs.pathExists(pluginPath))) return;
await fsp.mkdir(pluginDir, { recursive: true });
await fsp.mkdir(tempDir, { recursive: true });
if (!FORCE && fs.existsSync(pluginPath)) return;
try {
if (!(await fs.pathExists(tempZip))) {
if (!fs.existsSync(tempZip)) {
await downloadFile(url, tempZip);
}
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: "SimpleSC" entry name`, entry.entryName);
log_debug(`"SimpleSC" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
await fs.copyFile(tempDll, pluginPath);
console.log(`[INFO]: "SimpleSC" unzip finished`);
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
log_success(`unzip finished: "SimpleSC"`);
} finally {
await fs.remove(tempDir);
await fsp.rm(tempDir, { recursive: true, force: true });
}
};
// service chmod
const resolveServicePermission = async () => {
const serviceExecutables = [
"clash-verge-service",
"install-service",
"uninstall-service",
"clash-verge-service*",
"install-service*",
"uninstall-service*",
];
const resDir = path.join(cwd, "src-tauri/resources");
for (let f of serviceExecutables) {
const targetPath = path.join(resDir, f);
if (await fs.pathExists(targetPath)) {
execSync(`chmod 755 ${targetPath}`);
console.log(`[INFO]: "${targetPath}" chmod finished`);
// 使用glob模块来处理通配符
const files = glob.sync(path.join(resDir, f));
for (let filePath of files) {
if (fs.existsSync(filePath)) {
execSync(`chmod 755 ${filePath}`);
log_success(`chmod finished: "${filePath}"`);
}
}
}
};
@ -373,29 +401,32 @@ const resolveServicePermission = async () => {
/**
* main
*/
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "clash-verge-service" + ext,
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({
file: "install-service" + ext,
file: "install-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/install-service${ext}`,
});
};
const resolveUninstall = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "uninstall-service" + ext,
file: "uninstall-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
});
};
@ -421,6 +452,12 @@ const resolveEnableLoopback = () =>
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 tasks = [
// { name: "clash", func: resolveClash, retry: 5 },
{
@ -454,6 +491,24 @@ const tasks = [
retry: 1,
unixOnly: true,
},
{
name: "windows-sysproxy",
func: resolveWinSysproxy,
retry: 5,
winOnly: true,
},
{
name: "set_dns_script",
func: resolveSetDnsScript,
retry: 5,
macosOnly: true,
},
{
name: "unset_dns_script",
func: resolveUnSetDnsScript,
retry: 5,
macosOnly: true,
},
];
async function runTask() {
@ -462,13 +517,14 @@ async function runTask() {
if (task.winOnly && platform !== "win32") return runTask();
if (task.linuxOnly && platform !== "linux") return runTask();
if (task.unixOnly && platform === "win32") return runTask();
if (task.macosOnly && platform !== "darwin") return runTask();
for (let i = 0; i < task.retry; i++) {
try {
await task.func();
break;
} catch (err) {
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message);
log_error(`task::${task.name} try ${i} ==`, err.message);
if (i === task.retry - 1) throw err;
}
}

View File

@ -1,4 +1,5 @@
import fs from "fs-extra";
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
import AdmZip from "adm-zip";
import { createRequire } from "module";
@ -30,12 +31,14 @@ async function resolvePortable() {
const configDir = path.join(releaseDir, ".config");
if (!(await fs.pathExists(releaseDir))) {
if (!fs.existsSync(releaseDir)) {
throw new Error("could not found the release dir");
}
await fs.mkdir(configDir);
await fs.createFile(path.join(configDir, "PORTABLE"));
await fsp.mkdir(configDir, { recursive: true });
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
}
const zip = new AdmZip();
@ -46,9 +49,9 @@ async function resolvePortable() {
zip.addLocalFolder(
path.join(
releaseDir,
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
),
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
);
zip.addLocalFolder(configDir, ".config");

View File

@ -1,4 +1,4 @@
import fs from "fs-extra";
import fs from "fs";
import path from "path";
import AdmZip from "adm-zip";
import { createRequire } from "module";
@ -29,12 +29,14 @@ async function resolvePortable() {
: `./src-tauri/target/release`;
const configDir = path.join(releaseDir, ".config");
if (!(await fs.pathExists(releaseDir))) {
if (!fs.existsSync(releaseDir)) {
throw new Error("could not found the release dir");
}
await fs.mkdir(configDir);
await fs.createFile(path.join(configDir, "PORTABLE"));
await fsp.mkdir(configDir, { recursive: true });
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
}
const zip = new AdmZip();

66
scripts/set_dns.sh Normal file
View File

@ -0,0 +1,66 @@
#!/bin/bash
# 验证IPv4地址格式
function is_valid_ipv4() {
local ip=$1
local IFS='.'
local -a octets
[[ ! $ip =~ ^([0-9]+\.){3}[0-9]+$ ]] && return 1
read -r -a octets <<<"$ip"
[ "${#octets[@]}" -ne 4 ] && return 1
for octet in "${octets[@]}"; do
if ! [[ "$octet" =~ ^[0-9]+$ ]] || ((octet < 0 || octet > 255)); then
return 1
fi
done
return 0
}
# 验证IPv6地址格式
function is_valid_ipv6() {
local ip=$1
if [[ ! $ip =~ ^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$ ]] &&
[[ ! $ip =~ ^(([0-9a-fA-F]{0,4}:){0,7}:|(:[0-9a-fA-F]{0,4}:){0,6}:[0-9a-fA-F]{0,4})$ ]]; then
return 1
fi
return 0
}
# 验证IP地址是否为有效的IPv4或IPv6
function is_valid_ip() {
is_valid_ipv4 "$1" || is_valid_ipv6 "$1"
}
# 检查参数
[ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1
! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1
# 获取网络接口和硬件端口
nic=$(route -n get default | grep "interface" | awk '{print $2}')
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
/Hardware Port:/{port=$0; gsub("Hardware Port: ", "", port)}
/Device: /{if ($2 == dev) {print port; exit}}
')
# 获取当前DNS设置
original_dns=$(networksetup -getdnsservers "$hardware_port")
# 检查当前DNS设置是否有效
is_valid_dns=false
for ip in $original_dns; do
ip=$(echo "$ip" | tr -d '[:space:]')
if [ -n "$ip" ] && (is_valid_ipv4 "$ip" || is_valid_ipv6 "$ip"); then
is_valid_dns=true
break
fi
done
# 更新DNS设置
if [ "$is_valid_dns" = false ]; then
echo "empty" >.original_dns.txt
else
echo "$original_dns" >.original_dns.txt
fi
networksetup -setdnsservers "$hardware_port" "$1"

20
scripts/unset_dns.sh Normal file
View File

@ -0,0 +1,20 @@
#!/bin/bash
nic=$(route -n get default | grep "interface" | awk '{print $2}')
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
/Hardware Port:/{
port=$0; gsub("Hardware Port: ", "", port)
}
/Device: /{
if ($2 == dev) {
print port;
exit
}
}
')
if [ -f .original_dns.txt ]; then
original_dns=$(cat .original_dns.txt)
networksetup -setdnsservers "$hardware_port" $original_dns
rm -rf .original_dns.txt
fi

View File

@ -1,4 +1,5 @@
import fs from "fs-extra";
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
const UPDATE_LOG = "UPDATELOG.md";
@ -12,11 +13,11 @@ export async function resolveUpdateLog(tag) {
const file = path.join(cwd, UPDATE_LOG);
if (!(await fs.pathExists(file))) {
if (!fs.existsSync(file)) {
throw new Error("could not found UPDATELOG.md");
}
const data = await fs.readFile(file).then((d) => d.toString("utf8"));
const data = await fsp.readFile(file, "utf-8");
const map = {};
let p = "";

11
scripts/utils.mjs Normal file
View File

@ -0,0 +1,11 @@
import clc from "cli-color";
export const log_success = (msg, ...optionalParams) =>
console.log(clc.green(msg), ...optionalParams);
export const log_error = (msg, ...optionalParams) =>
console.log(clc.red(msg), ...optionalParams);
export const log_info = (msg, ...optionalParams) =>
console.log(clc.bgBlue(msg), ...optionalParams);
var debugMsg = clc.xterm(245);
export const log_debug = (msg, ...optionalParams) =>
console.log(debugMsg(msg), ...optionalParams);

2392
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

43
src-tauri/Cargo.toml Normal file → Executable file
View File

@ -10,7 +10,7 @@ edition = "2021"
build = "build.rs"
[build-dependencies]
tauri-build = { version = "2.0.0-rc", features = [] }
tauri-build = { version = "2.0.3", features = [] }
[dependencies]
warp = "0.3"
@ -22,22 +22,21 @@ dunce = "1.0"
log4rs = "1"
nanoid = "0.4"
chrono = "0.4"
sysinfo = "0.30"
boa_engine = "0.18"
sysinfo = "0.32.0"
boa_engine = "0.19.1"
serde_json = "1.0"
serde_yaml = "0.9"
once_cell = "1.19"
port_scanner = "0.1.5"
delay_timer = "0.11"
parking_lot = "0.12"
auto-launch = "0.5.0"
percent-encoding = "2.3.1"
window-shadows = { version = "0.2.2" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git = "https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
tauri = { version = "2.0.0-rc", features = [
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "main" }
tauri = { version = "2.1.1", features = [
"protocol-asset",
"devtools",
"tray-icon",
@ -45,24 +44,36 @@ tauri = { version = "2.0.0-rc", features = [
"image-png",
] }
network-interface = { version = "2.0.0", features = ["serde"] }
tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-dialog = "2.0.0-rc"
tauri-plugin-fs = "2.0.0-rc"
tauri-plugin-notification = "2.0.0-rc"
tauri-plugin-process = "2.0.0-rc"
tauri-plugin-clipboard-manager = "2.1.0-beta.7"
tauri-plugin-shell = "2.0.2"
tauri-plugin-dialog = "2.0.2"
tauri-plugin-fs = "2.0.2"
tauri-plugin-notification = "2.0.1"
tauri-plugin-process = "2.0.1"
tauri-plugin-clipboard-manager = "2.0.1"
tauri-plugin-deep-link = "2.0.1"
tauri-plugin-devtools = "2.0.0-rc"
url = "2.5.2"
zip = "2.2.0"
reqwest_dav = "0.1.14"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.2"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
deelevate = "0.2.0"
winreg = "0.52.0"
url = "2.5.2"
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2.0.0-rc"
tauri-plugin-updater = "2.0.0-rc"
tauri-plugin-autostart = "2.0.0-rc"
tauri-plugin-global-shortcut = "2.0.1"
tauri-plugin-updater = "2.0.2"
tauri-plugin-window-state = "2.0.2"
#openssl
[features]
@ -79,3 +90,7 @@ strip = true
[profile.dev]
incremental = true
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Clash Verge</string>
<key>CFBundleURLSchemes</key>
<array>
<string>clash</string>
</array>
</dict>
</array>
</dict>
</plist>

9
src-tauri/capabilities/desktop.json Normal file → Executable file
View File

@ -3,5 +3,12 @@
"platforms": ["macOS", "windows", "linux"],
"webviews": ["main"],
"windows": ["main"],
"permissions": ["global-shortcut:default", "updater:default"]
"permissions": [
"global-shortcut:default",
"updater:default",
"deep-link:default",
"window-state:default",
"window-state:default",
"autostart:default"
]
}

View File

@ -11,6 +11,11 @@
"identifier": "fs:scope",
"allow": ["$APPDATA/**", "$RESOURCE/../**", "**"]
},
"fs:allow-write-file",
{
"identifier": "fs:scope",
"allow": ["$APPDATA/**", "$RESOURCE/../**", "**"]
},
"core:window:allow-create",
"core:window:allow-center",
"core:window:allow-request-user-attention",
@ -43,6 +48,13 @@
"core:window:allow-set-cursor-position",
"core:window:allow-set-ignore-cursor-events",
"core:window:allow-start-dragging",
"core:window:allow-maximize",
"core:window:allow-toggle-maximize",
"core:window:allow-unmaximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-set-maximizable",
"core:window:allow-set-minimizable",
"core:webview:allow-print",
"shell:allow-execute",
"shell:allow-open",

View File

@ -0,0 +1,4 @@
#!/bin/bash
chmod +x /usr/bin/install-service
chmod +x /usr/bin/uninstall-service
chmod +x /usr/bin/clash-verge-service

View File

@ -0,0 +1,2 @@
#!/bin/bash
/usr/bin/uninstall-service

View File

@ -1,5 +1,6 @@
; This file is copied from https://github.com/tauri-apps/tauri/blob/tauri-v1.5/tooling/bundler/src/bundle/windows/templates/installer.nsi
; and edit to fit the needs of the project. the latest tauri 2.x has a different base nsi script.
RequestExecutionLevel admin
Unicode true
; Set the compression algorithm. Default is LZMA.
@ -16,6 +17,8 @@ Unicode true
!include "StrFunc.nsh"
!include "Win\COM.nsh"
!include "Win\Propkey.nsh"
!include "WinVer.nsh"
!include "LogicLib.nsh"
!addplugindir "$%AppData%\Local\NSIS\"
${StrCase}
${StrLoc}
@ -688,9 +691,41 @@ SectionEnd
app_check_done:
!macroend
Var VC_REDIST_URL
Var VC_REDIST_EXE
Section CheckAndInstallVSRuntime
${If} ${IsNativeARM64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.arm64.exe"
IfFileExists "$SYSDIR\msvcp140.dll" Done
${ElseIf} ${RunningX64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x64.exe"
IfFileExists "$WINDIR\SysWOW64\msvcp140.dll" Done
${Else}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x86.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x86.exe"
IfFileExists "$SYSDIR\msvcp140.dll" Done
${EndIf}
; 下载并安装VC运行库
nsisdl::download "$VC_REDIST_URL" "$TEMP\$VC_REDIST_EXE"
Pop $0
${If} $0 == "success"
nsExec::Exec '"$TEMP\$VC_REDIST_EXE" /quiet /norestart'
${EndIf}
Done:
SectionEnd
Section Install
SetOutPath $INSTDIR
nsExec::Exec 'netsh int tcp res'
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
; Copy main executable

View File

@ -2,20 +2,21 @@ use crate::{
config::*,
core::*,
feat,
utils::{dirs, help, resolve},
utils::{dirs, help},
};
use crate::{ret_err, wrap_err};
use anyhow::{Context, Result};
use network_interface::NetworkInterface;
use serde_yaml::Mapping;
use std::collections::{HashMap, VecDeque};
use std::collections::HashMap;
use sysproxy::{Autoproxy, Sysproxy};
type CmdResult<T = ()> = Result<T, String>;
use reqwest_dav::list_cmd::ListFile;
use tauri::Manager;
#[tauri::command]
pub fn copy_clash_env(app_handle: tauri::AppHandle) -> CmdResult {
feat::copy_clash_env(&app_handle);
pub fn copy_clash_env() -> CmdResult {
feat::copy_clash_env();
Ok(())
}
@ -168,8 +169,10 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
}
#[tauri::command]
pub fn get_verge_config() -> CmdResult<IVerge> {
Ok(Config::verge().data().clone())
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge();
let verge_data = verge.data().clone();
Ok(IVergeResponse::from(verge_data))
}
#[tauri::command]
@ -184,8 +187,8 @@ pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
/// restart the sidecar
#[tauri::command]
pub async fn restart_sidecar() -> CmdResult {
wrap_err!(CoreManager::global().run_core().await)
pub async fn restart_core() -> CmdResult {
wrap_err!(CoreManager::global().restart_core().await)
}
/// get the system proxy
@ -215,11 +218,6 @@ pub fn get_auto_proxy() -> CmdResult<Mapping> {
Ok(map)
}
#[tauri::command]
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
Ok(logger::Logger::global().get_log())
}
#[tauri::command]
pub fn open_app_dir() -> CmdResult<()> {
let app_dir = wrap_err!(dirs::app_home_dir())?;
@ -337,7 +335,7 @@ pub fn get_network_interfaces() -> Vec<String> {
for (interface_name, _) in &networks {
result.push(interface_name.clone());
}
return result;
result
}
#[tauri::command]
@ -371,31 +369,52 @@ pub fn open_devtools(app_handle: tauri::AppHandle) {
}
#[tauri::command]
pub fn exit_app(app_handle: tauri::AppHandle) {
let _ = resolve::save_window_size_position(&app_handle, true);
resolve::resolve_reset();
app_handle.exit(0);
std::process::exit(0);
}
pub mod service {
use super::*;
use crate::core::service;
#[tauri::command]
pub async fn check_service() -> CmdResult<service::JsonResponse> {
wrap_err!(service::check_service().await)
pub fn exit_app() {
feat::quit(Some(0));
}
#[tauri::command]
pub async fn install_service(passwd: String) -> CmdResult {
wrap_err!(service::install_service(passwd).await)
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
let patch = IVerge {
webdav_url: Some(url),
webdav_username: Some(username),
webdav_password: Some(password),
..IVerge::default()
};
Config::verge().draft().patch_config(patch.clone());
Config::verge().apply();
Config::verge()
.data()
.save_file()
.map_err(|err| err.to_string())?;
backup::WebDavClient::global().reset();
Ok(())
}
#[tauri::command]
pub async fn uninstall_service(passwd: String) -> CmdResult {
wrap_err!(service::uninstall_service(passwd).await)
pub async fn create_webdav_backup() -> CmdResult<()> {
wrap_err!(feat::create_backup_and_upload_webdav().await)
}
#[tauri::command]
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
wrap_err!(feat::list_wevdav_backup().await)
}
#[tauri::command]
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
wrap_err!(feat::delete_webdav_backup(filename).await)
}
#[tauri::command]
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
wrap_err!(feat::restore_webdav_backup(filename).await)
}
#[tauri::command]
pub async fn restart_app() -> CmdResult<()> {
feat::restart_app();
Ok(())
}
#[cfg(not(windows))]

View File

@ -32,13 +32,12 @@ impl IClashTemp {
pub fn template() -> Self {
let mut map = Mapping::new();
let mut tun = Mapping::new();
tun.insert("enable".into(), false.into());
tun.insert("stack".into(), "gvisor".into());
tun.insert("device".into(), "Mihomo".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53"].into());
tun.insert("mtu".into(), 1500.into());
#[cfg(not(target_os = "windows"))]
map.insert("redir-port".into(), 7895.into());
#[cfg(target_os = "linux")]
@ -50,9 +49,13 @@ impl IClashTemp {
map.insert("allow-lan".into(), false.into());
map.insert("mode".into(), "rule".into());
map.insert("external-controller".into(), "127.0.0.1:9097".into());
let mut cors_map = Mapping::new();
cors_map.insert("allow-private-network".into(), true.into());
cors_map.insert("allow-origins".into(), vec!["*"].into());
map.insert("secret".into(), "".into());
map.insert("tun".into(), tun.into());
map.insert("external-controller-cors".into(), cors_map.into());
map.insert("unified-delay".into(), true.into());
Self(map)
}

View File

@ -6,7 +6,7 @@ use crate::{
};
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use std::{env::temp_dir, path::PathBuf};
use std::path::PathBuf;
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
@ -85,7 +85,7 @@ impl Config {
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
let path = match typ {
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
ConfigType::Check => temp_dir().join(CHECK_CONFIG),
ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG),
};
let runtime = Config::runtime();

View File

@ -0,0 +1,95 @@
use crate::utils::dirs::get_encryption_key;
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const NONCE_LENGTH: usize = 12;
/// Encrypt data
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
let encryption_key = get_encryption_key()?;
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
let cipher = Aes256Gcm::new(key);
// Generate random nonce
let mut nonce = vec![0u8; NONCE_LENGTH];
getrandom::getrandom(&mut nonce)?;
// Encrypt data
let ciphertext = cipher
.encrypt(nonce.as_slice().into(), data.as_bytes())
.map_err(|e| format!("Encryption failed: {}", e))?;
// Concatenate nonce and ciphertext and encode them in base64
let mut combined = nonce;
combined.extend(ciphertext);
Ok(STANDARD.encode(combined))
}
/// Decrypt data
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
let encryption_key = get_encryption_key()?;
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
let cipher = Aes256Gcm::new(key);
// Decode from base64
let data = STANDARD.decode(encrypted)?;
if data.len() < NONCE_LENGTH {
return Err("Invalid encrypted data".into());
}
// Separate nonce and ciphertext
let (nonce, ciphertext) = data.split_at(NONCE_LENGTH);
// Decrypt data
let plaintext = cipher
.decrypt(nonce.into(), ciphertext)
.map_err(|e| format!("Decryption failed: {}", e))?;
String::from_utf8(plaintext).map_err(|e| e.into())
}
/// Serialize encrypted function
pub fn serialize_encrypted<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: Serialize,
S: Serializer,
{
// 如果序列化失败,返回 None
let json = match serde_json::to_string(value) {
Ok(j) => j,
Err(_) => return serializer.serialize_none(),
};
// 如果加密失败,返回 None
match encrypt_data(&json) {
Ok(encrypted) => serializer.serialize_str(&encrypted),
Err(_) => serializer.serialize_none(),
}
}
/// Deserialize decrypted function
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: for<'de> Deserialize<'de> + Default,
D: Deserializer<'a>,
{
// 如果反序列化字符串失败,返回默认值
let encrypted = match String::deserialize(deserializer) {
Ok(s) => s,
Err(_) => return Ok(T::default()),
};
// 如果解密失败,返回默认值
let decrypted_string = match decrypt_data(&encrypted) {
Ok(data) => data,
Err(_) => return Ok(T::default()),
};
// 如果 JSON 解析失败,返回默认值
match serde_json::from_str(&decrypted_string) {
Ok(value) => Ok(value),
Err(_) => Ok(T::default()),
}
}

View File

@ -2,6 +2,7 @@ mod clash;
#[allow(clippy::module_inception)]
mod config;
mod draft;
mod encrypt;
mod prfitem;
mod profiles;
mod runtime;
@ -10,6 +11,7 @@ mod verge;
pub use self::clash::*;
pub use self::config::*;
pub use self::draft::*;
pub use self::encrypt::*;
pub use self::prfitem::*;
pub use self::profiles::*;
pub use self::runtime::*;

View File

@ -19,18 +19,20 @@ impl IRuntime {
// 这里只更改 allow-lan | ipv6 | log-level | tun
pub fn patch_config(&mut self, patch: Mapping) {
if let Some(config) = self.config.as_mut() {
["allow-lan", "ipv6", "log-level"]
["allow-lan", "ipv6", "log-level", "unified-delay"]
.into_iter()
.for_each(|key| {
if let Some(value) = patch.get(key).to_owned() {
config.insert(key.into(), value.clone());
}
});
let patch_tun = patch.get("tun");
if patch_tun.is_some() {
let tun = config.get("tun");
let mut tun = tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let patch_tun = patch.get("tun");
let patch_tun = patch_tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
@ -39,7 +41,9 @@ impl IRuntime {
tun.insert(key.into(), value.clone());
}
});
config.insert("tun".into(), Value::from(tun));
}
}
}
}

View File

@ -1,4 +1,5 @@
use crate::config::DEFAULT_PAC;
use crate::config::{deserialize_encrypted, serialize_encrypted};
use crate::utils::{dirs, help};
use anyhow::Result;
use log::LevelFilter;
@ -7,9 +8,6 @@ use serde::{Deserialize, Serialize};
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IVerge {
/// app listening port for app singleton
pub app_singleton_port: Option<u16>,
/// app log level
/// silent | error | warn | info | debug | trace
pub app_log_level: Option<String>,
@ -59,10 +57,6 @@ pub struct IVerge {
/// clash tun mode
pub enable_tun_mode: Option<bool>,
/// windows service mode
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_service_mode: Option<bool>,
/// can the app auto startup
pub enable_auto_launch: Option<bool>,
@ -122,21 +116,13 @@ pub struct IVerge {
/// proxy 页面布局 列数
pub proxy_layout_column: Option<i32>,
/// 测试站列表
/// 测试站列表
pub test_list: Option<Vec<IVergeTestItem>>,
/// 日志清理
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
pub auto_log_clean: Option<i32>,
/// window size and position
#[serde(skip_serializing_if = "Option::is_none")]
pub window_size_position: Option<Vec<f64>>,
/// window size and position
#[serde(skip_serializing_if = "Option::is_none")]
pub window_is_maximized: Option<bool>,
/// 是否启用随机端口
pub enable_random_port: Option<bool>,
@ -162,6 +148,33 @@ pub struct IVerge {
pub verge_port: Option<u16>,
pub verge_http_enabled: Option<bool>,
/// WebDAV 配置 (加密存储)
#[serde(
serialize_with = "serialize_encrypted",
deserialize_with = "deserialize_encrypted",
skip_serializing_if = "Option::is_none",
default
)]
pub webdav_url: Option<String>,
/// WebDAV 用户名 (加密存储)
#[serde(
serialize_with = "serialize_encrypted",
deserialize_with = "deserialize_encrypted",
skip_serializing_if = "Option::is_none",
default
)]
pub webdav_username: Option<String>,
/// WebDAV 密码 (加密存储)
#[serde(
serialize_with = "serialize_encrypted",
deserialize_with = "deserialize_encrypted",
skip_serializing_if = "Option::is_none",
default
)]
pub webdav_password: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@ -244,6 +257,9 @@ impl IVerge {
auto_check_update: Some(true),
enable_builtin_enhanced: Some(true),
auto_log_clean: Some(3),
webdav_url: None,
webdav_username: None,
webdav_password: None,
..Self::default()
}
}
@ -282,7 +298,6 @@ impl IVerge {
patch!(tun_tray_icon);
patch!(enable_tun_mode);
patch!(enable_service_mode);
patch!(enable_auto_launch);
patch!(enable_silent_start);
patch!(enable_random_port);
@ -320,8 +335,10 @@ impl IVerge {
patch!(proxy_layout_column);
patch!(test_list);
patch!(auto_log_clean);
patch!(window_size_position);
patch!(window_is_maximized);
patch!(webdav_url);
patch!(webdav_username);
patch!(webdav_password);
}
/// 在初始化前尝试拿到单例端口的值
@ -330,11 +347,7 @@ impl IVerge {
const SERVER_PORT: u16 = 33331;
#[cfg(feature = "verge-dev")]
const SERVER_PORT: u16 = 11233;
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config.app_singleton_port.unwrap_or(SERVER_PORT),
Err(_) => SERVER_PORT, // 这里就不log错误了
}
SERVER_PORT
}
/// 获取日志等级
@ -354,3 +367,124 @@ impl IVerge {
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct IVergeResponse {
pub app_log_level: Option<String>,
pub language: Option<String>,
pub theme_mode: Option<String>,
pub tray_event: Option<String>,
pub env_type: Option<String>,
pub start_page: Option<String>,
pub startup_script: Option<String>,
pub traffic_graph: Option<bool>,
pub enable_memory_usage: Option<bool>,
pub enable_group_icon: Option<bool>,
pub common_tray_icon: Option<bool>,
#[cfg(target_os = "macos")]
pub tray_icon: Option<String>,
pub menu_icon: Option<String>,
pub sysproxy_tray_icon: Option<bool>,
pub tun_tray_icon: Option<bool>,
pub enable_tun_mode: Option<bool>,
pub enable_auto_launch: Option<bool>,
pub enable_silent_start: Option<bool>,
pub enable_system_proxy: Option<bool>,
pub enable_proxy_guard: Option<bool>,
pub use_default_bypass: Option<bool>,
pub system_proxy_bypass: Option<String>,
pub proxy_guard_duration: Option<u64>,
pub proxy_auto_config: Option<bool>,
pub pac_file_content: Option<String>,
pub theme_setting: Option<IVergeTheme>,
pub web_ui_list: Option<Vec<String>>,
pub clash_core: Option<String>,
pub hotkeys: Option<Vec<String>>,
pub auto_close_connection: Option<bool>,
pub auto_check_update: Option<bool>,
pub default_latency_test: Option<String>,
pub default_latency_timeout: Option<i32>,
pub enable_builtin_enhanced: Option<bool>,
pub proxy_layout_column: Option<i32>,
pub test_list: Option<Vec<IVergeTestItem>>,
pub auto_log_clean: Option<i32>,
pub enable_random_port: Option<bool>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_port: Option<u16>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_enabled: Option<bool>,
#[cfg(target_os = "linux")]
pub verge_tproxy_port: Option<u16>,
#[cfg(target_os = "linux")]
pub verge_tproxy_enabled: Option<bool>,
pub verge_mixed_port: Option<u16>,
pub verge_socks_port: Option<u16>,
pub verge_socks_enabled: Option<bool>,
pub verge_port: Option<u16>,
pub verge_http_enabled: Option<bool>,
pub webdav_url: Option<String>,
pub webdav_username: Option<String>,
pub webdav_password: Option<String>,
}
impl From<IVerge> for IVergeResponse {
fn from(verge: IVerge) -> Self {
Self {
app_log_level: verge.app_log_level,
language: verge.language,
theme_mode: verge.theme_mode,
tray_event: verge.tray_event,
env_type: verge.env_type,
start_page: verge.start_page,
startup_script: verge.startup_script,
traffic_graph: verge.traffic_graph,
enable_memory_usage: verge.enable_memory_usage,
enable_group_icon: verge.enable_group_icon,
common_tray_icon: verge.common_tray_icon,
#[cfg(target_os = "macos")]
tray_icon: verge.tray_icon,
menu_icon: verge.menu_icon,
sysproxy_tray_icon: verge.sysproxy_tray_icon,
tun_tray_icon: verge.tun_tray_icon,
enable_tun_mode: verge.enable_tun_mode,
enable_auto_launch: verge.enable_auto_launch,
enable_silent_start: verge.enable_silent_start,
enable_system_proxy: verge.enable_system_proxy,
enable_proxy_guard: verge.enable_proxy_guard,
use_default_bypass: verge.use_default_bypass,
system_proxy_bypass: verge.system_proxy_bypass,
proxy_guard_duration: verge.proxy_guard_duration,
proxy_auto_config: verge.proxy_auto_config,
pac_file_content: verge.pac_file_content,
theme_setting: verge.theme_setting,
web_ui_list: verge.web_ui_list,
clash_core: verge.clash_core,
hotkeys: verge.hotkeys,
auto_close_connection: verge.auto_close_connection,
auto_check_update: verge.auto_check_update,
default_latency_test: verge.default_latency_test,
default_latency_timeout: verge.default_latency_timeout,
enable_builtin_enhanced: verge.enable_builtin_enhanced,
proxy_layout_column: verge.proxy_layout_column,
test_list: verge.test_list,
auto_log_clean: verge.auto_log_clean,
enable_random_port: verge.enable_random_port,
#[cfg(not(target_os = "windows"))]
verge_redir_port: verge.verge_redir_port,
#[cfg(not(target_os = "windows"))]
verge_redir_enabled: verge.verge_redir_enabled,
#[cfg(target_os = "linux")]
verge_tproxy_port: verge.verge_tproxy_port,
#[cfg(target_os = "linux")]
verge_tproxy_enabled: verge.verge_tproxy_enabled,
verge_mixed_port: verge.verge_mixed_port,
verge_socks_port: verge.verge_socks_port,
verge_socks_enabled: verge.verge_socks_enabled,
verge_port: verge.verge_port,
verge_http_enabled: verge.verge_http_enabled,
webdav_url: verge.webdav_url,
webdav_username: verge.webdav_username,
webdav_password: verge.webdav_password,
}
}
}

View File

@ -0,0 +1,156 @@
use crate::config::Config;
use crate::utils::dirs;
use anyhow::Error;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reqwest_dav::list_cmd::{ListEntity, ListFile};
use std::env::{consts::OS, temp_dir};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use zip::write::SimpleFileOptions;
pub struct WebDavClient {
client: Arc<Mutex<Option<reqwest_dav::Client>>>,
}
impl WebDavClient {
pub fn global() -> &'static WebDavClient {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
client: Arc::new(Mutex::new(None)),
})
}
async fn get_client(&self) -> Result<reqwest_dav::Client, Error> {
if self.client.lock().is_none() {
let verge = Config::verge().latest().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
{
let msg =
"Unable to create web dav client, please make sure the webdav config is correct"
.to_string();
log::error!(target: "app","{}",msg);
return Err(anyhow::Error::msg(msg));
}
let url = verge.webdav_url.unwrap_or_default();
let username = verge.webdav_username.unwrap_or_default();
let password = verge.webdav_password.unwrap_or_default();
let url = url.trim_end_matches('/');
let client = reqwest_dav::ClientBuilder::new()
.set_agent(
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(3))
.build()
.unwrap(),
)
.set_host(url.to_owned())
.set_auth(reqwest_dav::Auth::Basic(
username.to_owned(),
password.to_owned(),
))
.build()?;
if (client
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
.await)
.is_err()
{
client.mkcol(dirs::BACKUP_DIR).await?;
}
*self.client.lock() = Some(client.clone());
}
Ok(self.client.lock().clone().unwrap())
}
pub fn reset(&self) {
if !self.client.lock().is_none() {
self.client.lock().take();
}
}
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
let client = self.get_client().await?;
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
client
.put(webdav_path.as_ref(), fs::read(file_path)?)
.await?;
Ok(())
}
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
let client = self.get_client().await?;
let path = format!("{}/{}", dirs::BACKUP_DIR, filename);
let response = client.get(path.as_str()).await?;
let content = response.bytes().await?;
fs::write(&storage_path, &content)?;
Ok(())
}
pub async fn list(&self) -> Result<Vec<ListFile>, Error> {
let client = self.get_client().await?;
let path = format!("{}/", dirs::BACKUP_DIR);
let files = client
.list(path.as_str(), reqwest_dav::Depth::Number(1))
.await?;
let mut final_files = Vec::new();
for file in files {
if let ListEntity::File(file) = file {
final_files.push(file);
}
}
Ok(final_files)
}
pub async fn delete(&self, file_name: String) -> Result<(), Error> {
let client = self.get_client().await?;
let path = format!("{}/{}", dirs::BACKUP_DIR, file_name);
client.delete(&path).await?;
Ok(())
}
}
pub fn create_backup() -> Result<(String, PathBuf), Error> {
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
let zip_path = temp_dir().join(&zip_file_name);
let file = fs::File::create(&zip_path)?;
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.unwrap();
let path = entry.path();
if path.is_file() {
let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap());
zip.start_file(backup_path, options)?;
zip.write_all(fs::read(path).unwrap().as_slice())?;
}
}
}
zip.start_file(dirs::CLASH_CONFIG, options)?;
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
let mut verge_config: serde_json::Value =
serde_yaml::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
if let Some(obj) = verge_config.as_object_mut() {
obj.remove("webdav_username");
obj.remove("webdav_password");
obj.remove("webdav_url");
}
zip.start_file(dirs::VERGE_CONFIG, options)?;
zip.write_all(serde_yaml::to_string(&verge_config)?.as_bytes())?;
zip.start_file(dirs::PROFILE_YAML, options)?;
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
zip.finish()?;
Ok((zip_file_name, zip_path))
}

View File

@ -9,7 +9,7 @@ use std::collections::HashMap;
/// path 是绝对路径
pub async fn put_configs(path: &str) -> Result<()> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/configs");
let url = format!("{url}/configs?force=true");
let mut data = HashMap::new();
data.insert("path", path);
@ -52,7 +52,7 @@ pub async fn get_proxy_delay(
let (url, headers) = clash_client_info()?;
let url = format!("{url}/proxies/{name}/delay");
let default_url = "http://1.1.1.1";
let default_url = "http://cp.cloudflare.com/generate_204";
let test_url = test_url
.map(|s| if s.is_empty() { default_url.into() } else { s })
.unwrap_or(default_url.into());
@ -96,8 +96,7 @@ pub fn parse_log(log: String) -> String {
log
}
/// 缩短clash -t的错误输出
/// 仅适配 clash p核 8-26、clash meta 1.13.1
#[allow(dead_code)]
pub fn parse_check_output(log: String) -> String {
let t = log.find("time=");
let m = log.find("msg=");
@ -127,7 +126,7 @@ pub fn parse_check_output(log: String) -> String {
#[test]
fn test_parse_check_output() {
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;
let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
//let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
let str3 = r#"
"time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress"
time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'"
@ -135,12 +134,8 @@ fn test_parse_check_output() {
"#;
let res1 = parse_check_output(str1.into());
let res2 = parse_check_output(str2.into());
// let res2 = parse_check_output(str2.into());
let res3 = parse_check_output(str3.into());
println!("res1: {res1}");
println!("res2: {res2}");
println!("res3: {res3}");
assert_eq!(res1, res3);
}

View File

@ -1,47 +1,33 @@
use crate::config::*;
use crate::core::{clash_api, handle, logger::Logger, service};
use crate::core::{clash_api, handle, service};
use crate::log_err;
use crate::utils::dirs;
use anyhow::{bail, Context, Result};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use serde_yaml::Mapping;
use std::{fs, io::Write, sync::Arc, time::Duration};
use sysinfo::{ProcessRefreshKind, RefreshKind, System};
use tauri::AppHandle;
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use std::{sync::Arc, time::Duration};
use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex;
use tokio::time::sleep;
#[derive(Debug)]
pub struct CoreManager {
app_handle: Arc<Mutex<Option<AppHandle>>>,
sidecar: Arc<Mutex<Option<CommandChild>>>,
#[allow(unused)]
use_service_mode: Arc<Mutex<bool>>,
running: Arc<Mutex<bool>>,
}
impl CoreManager {
pub fn global() -> &'static CoreManager {
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
CORE_MANAGER.get_or_init(|| CoreManager {
app_handle: Arc::new(Mutex::new(None)),
sidecar: Arc::new(Mutex::new(None)),
use_service_mode: Arc::new(Mutex::new(false)),
running: Arc::new(Mutex::new(false)),
})
}
pub fn init(&self, app_handle: &AppHandle) -> Result<()> {
*self.app_handle.lock() = Some(app_handle.clone());
tauri::async_runtime::spawn(async {
pub async fn init(&self) -> Result<()> {
log::trace!("run core start");
// 启动clash
log_err!(Self::global().run_core().await);
log_err!(Self::global().start_core().await);
log::trace!("run core end");
});
Ok(())
}
@ -51,241 +37,76 @@ impl CoreManager {
let config_path = dirs::path_to_str(&config_path)?;
let clash_core = { Config::verge().latest().clash_core.clone() };
let mut clash_core = clash_core.unwrap_or("verge-mihomo".into());
// compatibility
if clash_core.contains("clash") {
clash_core = "verge-mihomo".to_string();
Config::verge().draft().patch_config(IVerge {
clash_core: Some("verge-mihomo".to_string()),
..IVerge::default()
});
Config::verge().apply();
match Config::verge().data().save_file() {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
}
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
let test_dir = dirs::app_home_dir()?.join("test");
let test_dir = dirs::path_to_str(&test_dir)?;
let app_handle_option = {
let lock = self.app_handle.lock();
lock.as_ref().cloned()
};
let app_handle = handle::Handle::global().app_handle().unwrap();
if let Some(app_handle) = app_handle_option {
let output = app_handle
let _ = app_handle
.shell()
.sidecar(clash_core)?
.args(["-t", "-d", test_dir, "-f", config_path])
.output()
.await?;
if !output.status.success() {
let stdout = String::from_utf8(output.stdout).unwrap_or_default();
let error = clash_api::parse_check_output(stdout.clone());
let error = match !error.is_empty() {
true => error,
false => stdout.clone(),
};
Logger::global().set_log(stdout.clone());
bail!("{error}");
}
}
Ok(())
}
/// 启动核心
pub async fn run_core(&self) -> Result<()> {
let config_path = Config::generate_file(ConfigType::Run)?;
// 关闭tun模式
let mut disable = Mapping::new();
let mut tun = Mapping::new();
tun.insert("enable".into(), false.into());
disable.insert("tun".into(), tun.into());
log::debug!(target: "app", "disable tun mode");
let _ = clash_api::patch_configs(&disable).await;
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
log_err!(service::stop_core_by_service().await);
} else {
let system = System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::everything()),
);
let procs = system.processes_by_name("verge-mihomo");
for proc in procs {
log::debug!(target: "app", "kill all clash process");
proc.kill();
}
}
// 服务模式
let enable = { Config::verge().latest().enable_service_mode };
let enable = enable.unwrap_or(false);
*self.use_service_mode.lock() = enable;
if enable {
// 服务模式启动失败就直接运行sidecar
log::debug!(target: "app", "try to run core in service mode");
let res = async {
service::check_service().await?;
service::run_core_by_service(&config_path).await
}
.await;
match res {
Ok(_) => return Ok(()),
Err(err) => {
// 修改这个值免得stop出错
*self.use_service_mode.lock() = false;
log::error!(target: "app", "{err}");
}
}
}
let app_dir = dirs::app_home_dir()?;
let app_dir = dirs::path_to_str(&app_dir)?;
let clash_core = { Config::verge().latest().clash_core.clone() };
let mut clash_core = clash_core.unwrap_or("verge-mihomo".into());
// compatibility
if clash_core.contains("clash") {
clash_core = "verge-mihomo".to_string();
Config::verge().draft().patch_config(IVerge {
clash_core: Some("verge-mihomo".to_string()),
..IVerge::default()
});
Config::verge().apply();
match Config::verge().data().save_file() {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
}
let config_path = dirs::path_to_str(&config_path)?;
let args = vec!["-d", app_dir, "-f", config_path];
let app_handle = self.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
let cmd = app_handle.shell().sidecar(clash_core)?;
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
// 将pid写入文件中
crate::log_err!((|| {
let pid = cmd_child.pid();
let path = dirs::clash_pid_path()?;
fs::File::create(path)
.context("failed to create the pid file")?
.write(format!("{pid}").as_bytes())
.context("failed to write pid to the file")?;
<Result<()>>::Ok(())
})());
let mut sidecar = self.sidecar.lock();
*sidecar = Some(cmd_child);
drop(sidecar);
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
let line = String::from_utf8(line).unwrap_or_default();
log::info!(target: "app", "[mihomo]: {line}");
Logger::global().set_log(line);
}
CommandEvent::Stderr(err) => {
let err = String::from_utf8(err).unwrap_or_default();
log::error!(target: "app", "[mihomo]: {err}");
Logger::global().set_log(err);
}
CommandEvent::Error(err) => {
log::error!(target: "app", "[mihomo]: {err}");
Logger::global().set_log(err);
}
CommandEvent::Terminated(_) => {
log::info!(target: "app", "mihomo core terminated");
let _ = CoreManager::global().recover_core();
break;
}
_ => {}
}
}
});
}
Ok(())
}
/// 重启内核
pub fn recover_core(&'static self) -> Result<()> {
// 服务模式不管
if *self.use_service_mode.lock() {
return Ok(());
}
// 清空原来的sidecar值
let _ = self.sidecar.lock().take();
tauri::async_runtime::spawn(async move {
// 6秒之后再查看服务是否正常 (时间随便搞的)
// terminated 可能是切换内核 (切换内核已经有500ms的延迟)
sleep(Duration::from_millis(6666)).await;
if self.sidecar.lock().is_none() {
log::info!(target: "app", "recover clash core");
// 重新启动app
if let Err(err) = self.run_core().await {
log::error!(target: "app", "failed to recover clash core");
log::error!(target: "app", "{err}");
let _ = self.recover_core();
}
}
});
Ok(())
}
/// 停止核心运行
pub async fn stop_core(&self) -> Result<()> {
let mut running = self.running.lock().await;
if !*running {
log::debug!("core is not running");
return Ok(());
}
// 关闭tun模式
let mut disable = Mapping::new();
let mut tun = Mapping::new();
tun.insert("enable".into(), false.into());
disable.insert("tun".into(), tun.into());
log::debug!(target: "app", "disable tun mode");
let _ = clash_api::patch_configs(&disable).await;
log_err!(clash_api::patch_configs(&disable).await);
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
log_err!(service::stop_core_by_service().await);
// 服务模式
if service::check_service().await.is_ok() {
log::info!(target: "app", "stop the core by service");
service::stop_core_by_service().await?;
}
*running = false;
Ok(())
}
/// 启动核心
pub async fn start_core(&self) -> Result<()> {
let mut running = self.running.lock().await;
if *running {
log::info!("core is running");
return Ok(());
}
let mut sidecar = self.sidecar.lock();
let _ = sidecar.take();
let config_path = Config::generate_file(ConfigType::Run)?;
let system = System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::everything()),
);
let procs = system.processes_by_name("verge-mihomo");
for proc in procs {
log::debug!(target: "app", "kill all clash process");
proc.kill();
// 服务模式
if service::check_service().await.is_ok() {
log::info!(target: "app", "try to run core in service mode");
service::run_core_by_service(&config_path).await?;
*running = true;
}
Ok(())
}
/// 重启内核
pub async fn restart_core(&self) -> Result<()> {
// 重新启动app
self.stop_core().await?;
self.start_core().await?;
Ok(())
}
/// 切换核心
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
@ -295,7 +116,7 @@ impl CoreManager {
bail!("invalid clash core name \"{clash_core}\"");
}
log::debug!(target: "app", "change core to `{clash_core}`");
log::info!(target: "app", "change core to `{clash_core}`");
Config::verge().draft().clash_core = Some(clash_core);
@ -304,10 +125,7 @@ impl CoreManager {
self.check_config().await?;
// 清掉旧日志
Logger::global().clear_log();
match self.run_core().await {
match self.restart_core().await {
Ok(_) => {
Config::verge().apply();
Config::runtime().apply();

View File

@ -1,14 +1,15 @@
use super::tray::Tray;
use crate::log_err;
use anyhow::{bail, Result};
use anyhow::Result;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use parking_lot::RwLock;
use std::sync::Arc;
use tauri::{AppHandle, WebviewWindow, Manager, Emitter};
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
#[derive(Debug, Default, Clone)]
pub struct Handle {
pub app_handle: Arc<Mutex<Option<AppHandle>>>,
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
pub is_exiting: Arc<RwLock<bool>>,
}
impl Handle {
@ -16,19 +17,27 @@ impl Handle {
static HANDLE: OnceCell<Handle> = OnceCell::new();
HANDLE.get_or_init(|| Handle {
app_handle: Arc::new(Mutex::new(None)),
app_handle: Arc::new(RwLock::new(None)),
is_exiting: Arc::new(RwLock::new(false)),
})
}
pub fn init(&self, app_handle: &AppHandle) {
*self.app_handle.lock() = Some(app_handle.clone());
let mut handle = self.app_handle.write();
*handle = Some(app_handle.clone());
}
pub fn app_handle(&self) -> Option<AppHandle> {
self.app_handle.read().clone()
}
pub fn get_window(&self) -> Option<WebviewWindow> {
self.app_handle
.lock()
.as_ref()
.and_then(|a| a.get_webview_window("main"))
let app_handle = self.app_handle().unwrap();
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
if window.is_none() {
log::debug!(target:"app", "main window not found");
}
window
}
pub fn refresh_clash() {
@ -56,22 +65,18 @@ impl Handle {
}
}
pub fn update_systray() -> Result<()> {
let app_handle = Self::global().app_handle.lock();
if app_handle.is_none() {
bail!("update_systray unhandled error");
}
Tray::update_systray(app_handle.as_ref().unwrap())?;
/// update the system tray state
pub fn update_systray_part() -> Result<()> {
Tray::update_part()?;
Ok(())
}
/// update the system tray state
pub fn update_systray_part() -> Result<()> {
let app_handle = Self::global().app_handle.lock();
if app_handle.is_none() {
bail!("update_systray unhandled error");
pub fn set_is_exiting(&self) {
let mut is_exiting = self.is_exiting.write();
*is_exiting = true;
}
Tray::update_part(app_handle.as_ref().unwrap())?;
Ok(())
pub fn is_exiting(&self) -> bool {
*self.is_exiting.read()
}
}

57
src-tauri/src/core/hotkey.rs Normal file → Executable file
View File

@ -1,14 +1,13 @@
use crate::core::handle;
use crate::{config::Config, feat, log_err};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc};
use tauri::AppHandle;
use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tauri::Manager;
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
app_handle: Arc<Mutex<Option<AppHandle>>>,
}
impl Hotkey {
@ -17,12 +16,10 @@ impl Hotkey {
HOTKEY.get_or_init(|| Hotkey {
current: Arc::new(Mutex::new(Vec::new())),
app_handle: Arc::new(Mutex::new(None)),
})
}
pub fn init(&self, app_handle: &AppHandle) -> Result<()> {
*self.app_handle.lock() = Some(app_handle.clone());
pub fn init(&self) -> Result<()> {
let verge = Config::verge();
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
@ -48,12 +45,9 @@ impl Hotkey {
Ok(())
}
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let app_handle = self.app_handle.lock();
if app_handle.is_none() {
bail!("failed to get the hotkey manager");
}
let manager = app_handle.as_ref().unwrap().global_shortcut();
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
if manager.is_registered(hotkey) {
manager.unregister(hotkey)?;
@ -66,23 +60,35 @@ impl Hotkey {
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
"toggle_system_proxy" => feat::toggle_system_proxy,
"toggle_tun_mode" => feat::toggle_tun_mode,
"quit" => || feat::quit(Some(0)),
_ => bail!("invalid function \"{func}\""),
};
let _ = manager.on_shortcut(hotkey, move |_, _, _| f());
log::info!(target: "app", "register hotkey {hotkey} {func}");
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
if event.state == ShortcutState::Pressed {
if hotkey.key == Code::KeyQ {
if let Some(window) = app_handle.get_webview_window("main") {
if window.is_focused().unwrap_or(false) {
f();
}
}
} else {
f();
}
}
});
log::debug!(target: "app", "register hotkey {hotkey} {func}");
Ok(())
}
fn unregister(&self, hotkey: &str) -> Result<()> {
let app_handle = self.app_handle.lock();
if app_handle.is_none() {
bail!("failed to get the hotkey manager");
}
let manager = app_handle.as_ref().unwrap().global_shortcut();
pub fn unregister(&self, hotkey: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
manager.unregister(hotkey)?;
log::info!(target: "app", "unregister hotkey {hotkey}");
log::debug!(target: "app", "unregister hotkey {hotkey}");
Ok(())
}
@ -105,7 +111,7 @@ impl Hotkey {
Ok(())
}
fn get_map_from_vec(hotkeys: &Vec<String>) -> HashMap<&str, &str> {
fn get_map_from_vec(hotkeys: &[String]) -> HashMap<&str, &str> {
let mut map = HashMap::new();
hotkeys.iter().for_each(|hotkey| {
@ -153,10 +159,9 @@ impl Hotkey {
impl Drop for Hotkey {
fn drop(&mut self) {
if let Some(app_handle) = self.app_handle.lock().as_ref() {
let app_handle = handle::Handle::global().app_handle().unwrap();
if let Err(e) = app_handle.global_shortcut().unregister_all() {
log::error!("Error unregistering all hotkeys: {:?}", e);
}
log::error!(target:"app", "Error unregistering all hotkeys: {:?}", e);
}
}
}

View File

@ -1,36 +0,0 @@
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{collections::VecDeque, sync::Arc};
const LOGS_QUEUE_LEN: usize = 100;
pub struct Logger {
log_data: Arc<Mutex<VecDeque<String>>>,
}
impl Logger {
pub fn global() -> &'static Logger {
static LOGGER: OnceCell<Logger> = OnceCell::new();
LOGGER.get_or_init(|| Logger {
log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
})
}
pub fn get_log(&self) -> VecDeque<String> {
self.log_data.lock().clone()
}
pub fn set_log(&self, text: String) {
let mut logs = self.log_data.lock();
if logs.len() > LOGS_QUEUE_LEN {
logs.pop_front();
}
logs.push_back(text);
}
pub fn clear_log(&self) {
let mut logs = self.log_data.lock();
logs.clear();
}
}

View File

@ -1,9 +1,9 @@
pub mod backup;
pub mod clash_api;
#[allow(clippy::module_inception)]
mod core;
pub mod handle;
pub mod hotkey;
pub mod logger;
pub mod service;
pub mod sysopt;
pub mod timer;

View File

@ -1,13 +1,11 @@
use crate::config::{Config, IVerge};
use crate::core::handle;
use crate::config::Config;
use crate::utils::dirs;
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use std::{env::current_exe, process::Command as StdCommand};
use tokio::time::sleep;
use tokio::time::Duration;
// Windows only
@ -28,32 +26,34 @@ pub struct JsonResponse {
pub data: Option<ResponseBody>,
}
#[cfg(not(target_os = "windows"))]
pub fn sudo(passwd: &String, cmd: String) -> StdCommand {
let shell = format!("echo \"{}\" | sudo -S {}", passwd, cmd);
let mut command = StdCommand::new("bash");
command.arg("-c").arg(shell);
command
}
/// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
///
#[cfg(target_os = "windows")]
pub async fn install_service(_passwd: String) -> Result<()> {
pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service");
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service.exe");
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
if !install_path.exists() {
bail!("installer exe not found");
bail!(format!("installer not found: {install_path:?}"));
}
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let _ = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
_ => StdCommand::new(uninstall_path)
.creation_flags(0x08000000)
.status()?,
};
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
@ -73,111 +73,48 @@ pub async fn install_service(_passwd: String) -> Result<()> {
}
#[cfg(target_os = "linux")]
pub async fn install_service(passwd: String) -> Result<()> {
pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service");
use users::get_effective_uid;
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() {
bail!("installer not found");
}
let install_path = tauri::utils::platform::current_exe()?.with_file_name("install-service");
let output = match get_effective_uid() {
0 => {
StdCommand::new("chmod")
.arg("+x")
.arg(installer_path.clone())
.output()?;
StdCommand::new("chmod")
.arg("+x")
.arg(binary_path)
.output()?;
StdCommand::new(installer_path.clone()).output()?
}
_ => {
sudo(
&passwd,
format!("chmod +x {}", installer_path.to_string_lossy()),
)
.output()?;
sudo(
&passwd,
format!("chmod +x {}", binary_path.to_string_lossy()),
)
.output()?;
sudo(&passwd, format!("{}", installer_path.to_string_lossy())).output()?
}
};
if !output.status.success() {
bail!(
"failed to install service with error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let uninstall_path = tauri::utils::platform::current_exe()?.with_file_name("uninstall-service");
Ok(())
if !install_path.exists() {
bail!(format!("installer not found: {install_path:?}"));
}
#[cfg(target_os = "macos")]
pub async fn install_service(passwd: String) -> Result<()> {
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() {
bail!("installer not found");
}
sudo(
&passwd,
format!(
"chmod +x {}",
installer_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
format!("{}", installer_path.to_string_lossy().replace(" ", "\\ ")),
)
.output()?;
if !output.status.success() {
bail!(
"failed to install service with error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
/// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")]
pub async fn uninstall_service(_passwd: String) -> Result<()> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
if !uninstall_path.exists() {
bail!("uninstaller exe not found");
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let install_shell: String = install_path.to_string_lossy().replace(" ", "\\ ");
let uninstall_shell: String = uninstall_path.to_string_lossy().replace(" ", "\\ ");
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
_ => StdCommand::new(uninstall_path)
.creation_flags(0x08000000)
let elevator = crate::utils::help::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(uninstall_shell).status()?,
_ => StdCommand::new(elevator.clone())
.arg("sh")
.arg("-c")
.arg(uninstall_shell)
.status()?,
};
log::info!(target:"app", "status code:{}", status.code().unwrap());
let status = match get_effective_uid() {
0 => StdCommand::new(install_shell).status()?,
_ => StdCommand::new(elevator.clone())
.arg("sh")
.arg("-c")
.arg(install_shell)
.status()?,
};
if !status.success() {
bail!(
"failed to uninstall service with status {}",
"failed to install service with status {}",
status.code().unwrap()
);
}
@ -185,76 +122,40 @@ pub async fn uninstall_service(_passwd: String) -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn uninstall_service(passwd: String) -> Result<()> {
use users::get_effective_uid;
let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service");
if !uninstaller_path.exists() {
bail!("uninstaller not found");
}
let output = match get_effective_uid() {
0 => {
StdCommand::new("chmod")
.arg("+x")
.arg(uninstaller_path.clone())
.output()?;
StdCommand::new(uninstaller_path.clone()).output()?
}
_ => {
sudo(
&passwd,
format!("chmod +x {}", uninstaller_path.to_string_lossy()),
)
.output()?;
sudo(&passwd, format!("{}", uninstaller_path.to_string_lossy())).output()?
}
};
if !output.status.success() {
bail!(
"failed to install service with error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn uninstall_service(passwd: String) -> Result<()> {
let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service");
pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service");
if !uninstaller_path.exists() {
bail!("uninstaller not found");
let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service");
let uninstall_path = binary_path.with_file_name("uninstall-service");
if !install_path.exists() {
bail!(format!("installer not found: {install_path:?}"));
}
sudo(
&passwd,
format!(
"chmod +x {}",
uninstaller_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
format!("{}", uninstaller_path.to_string_lossy().replace(" ", "\\ ")),
)
.output()?;
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
if !output.status.success() {
let install_shell: String = install_path.to_string_lossy().into_owned();
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
let command = format!(
r#"do shell script "sudo '{uninstall_shell}' && sudo '{install_shell}'" with administrator privileges"#
);
log::debug!(target: "app", "command: {}", command);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!(
"failed to uninstall service with error: {}",
String::from_utf8_lossy(&output.stderr)
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
@ -263,6 +164,7 @@ pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash");
let response = reqwest::ClientBuilder::new()
.no_proxy()
.timeout(Duration::from_secs(3))
.build()?
.get(url)
.send()
@ -277,29 +179,8 @@ pub async fn check_service() -> Result<JsonResponse> {
/// start the clash by service
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
let status = check_service().await?;
if status.code == 0 {
stop_core_by_service().await?;
sleep(Duration::from_secs(1)).await;
}
let clash_core = { Config::verge().latest().clash_core.clone() };
let mut clash_core = clash_core.unwrap_or("verge-mihomo".into());
// compatibility
if clash_core.contains("clash") {
clash_core = "verge-mihomo".to_string();
Config::verge().draft().patch_config(IVerge {
clash_core: Some("verge-mihomo".to_string()),
..IVerge::default()
});
Config::verge().apply();
match Config::verge().data().save_file() {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
}
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}");
@ -321,81 +202,31 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
map.insert("config_file", config_file);
map.insert("log_file", log_path);
log::info!(target:"app", "start service: {:?}", map.clone());
let url = format!("{SERVICE_URL}/start_clash");
let res = reqwest::ClientBuilder::new()
let _ = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.json(&map)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// stop the clash by service
pub(super) async fn stop_core_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/stop_clash");
let res = reqwest::ClientBuilder::new()
let _ = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// set dns by service
pub async fn set_dns_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/set_dns");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// unset dns by service
pub async fn unset_dns_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/unset_dns");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}

View File

@ -1,36 +1,24 @@
use crate::core::handle::Handle;
use crate::{
config::{Config, IVerge},
log_err,
};
use anyhow::{anyhow, Result};
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use anyhow::Result;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::env::current_exe;
use std::sync::Arc;
use sysproxy::{Autoproxy, Sysproxy};
use tauri::async_runtime::Mutex as TokioMutex;
use tauri_plugin_autostart::ManagerExt;
use tokio::time::{sleep, Duration};
pub struct Sysopt {
/// current system proxy setting
cur_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
/// record the original system proxy
/// recover it when exit
old_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
/// current auto proxy setting
cur_autoproxy: Arc<Mutex<Option<Autoproxy>>>,
/// record the original auto proxy
/// recover it when exit
old_autoproxy: Arc<Mutex<Option<Autoproxy>>>,
update_sysproxy: Arc<TokioMutex<bool>>,
reset_sysproxy: Arc<TokioMutex<bool>>,
/// helps to auto launch the app
auto_launch: Arc<Mutex<Option<AutoLaunch>>>,
auto_launch: Arc<Mutex<bool>>,
/// record whether the guard async is running or not
guard_state: Arc<TokioMutex<bool>>,
guard_state: Arc<Mutex<bool>>,
}
#[cfg(target_os = "windows")]
@ -42,7 +30,6 @@ static DEFAULT_BYPASS: &str =
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
fn get_bypass() -> String {
// let bypass = DEFAULT_BYPASS.to_string();
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true);
let res = {
let verge = Config::verge();
@ -53,53 +40,43 @@ fn get_bypass() -> String {
Some(bypass) => bypass,
None => "".to_string(),
};
#[cfg(target_os = "windows")]
let bypass = if custom_bypass.is_empty() {
if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string()
} else {
if use_default {
format!("{};{}", DEFAULT_BYPASS, custom_bypass)
} else {
custom_bypass
}
};
#[cfg(not(target_os = "windows"))]
let bypass = if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string()
} else {
if use_default {
} else if use_default {
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
} else {
custom_bypass
}
};
bypass
}
impl Sysopt {
pub fn global() -> &'static Sysopt {
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
SYSOPT.get_or_init(|| Sysopt {
cur_sysproxy: Arc::new(Mutex::new(None)),
old_sysproxy: Arc::new(Mutex::new(None)),
cur_autoproxy: Arc::new(Mutex::new(None)),
old_autoproxy: Arc::new(Mutex::new(None)),
auto_launch: Arc::new(Mutex::new(None)),
guard_state: Arc::new(TokioMutex::new(false)),
update_sysproxy: Arc::new(TokioMutex::new(false)),
reset_sysproxy: Arc::new(TokioMutex::new(false)),
auto_launch: Arc::new(Mutex::new(false)),
guard_state: Arc::new(false.into()),
})
}
pub fn init_guard_sysproxy(&self) -> Result<()> {
self.guard_proxy();
Ok(())
}
/// init the sysproxy
pub fn init_sysproxy(&self) -> Result<()> {
pub async fn update_sysproxy(&self) -> Result<()> {
let _lock = self.update_sysproxy.lock().await;
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let pac_port = IVerge::get_singleton_port();
let (enable, pac) = {
let (sys_enable, pac_enable) = {
let verge = Config::verge();
let verge = verge.latest();
(
@ -107,257 +84,156 @@ impl Sysopt {
verge.proxy_auto_config.unwrap_or(false),
)
};
#[cfg(not(target_os = "windows"))]
{
let mut sys = Sysproxy {
enable,
enable: false,
host: String::from("127.0.0.1"),
port,
bypass: get_bypass(),
};
let mut auto = Autoproxy {
enable,
enable: false,
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
};
if pac {
if !sys_enable {
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
return Ok(());
}
if pac_enable {
sys.enable = false;
let old = Sysproxy::get_system_proxy().ok();
auto.enable = true;
sys.set_system_proxy()?;
*self.old_sysproxy.lock() = old;
*self.cur_sysproxy.lock() = Some(sys);
let old = Autoproxy::get_auto_proxy().ok();
auto.set_auto_proxy()?;
*self.old_autoproxy.lock() = old;
*self.cur_autoproxy.lock() = Some(auto);
} else {
return Ok(());
}
if sys_enable {
auto.enable = false;
let old = Autoproxy::get_auto_proxy().ok();
sys.enable = true;
auto.set_auto_proxy()?;
*self.old_autoproxy.lock() = old;
*self.cur_autoproxy.lock() = Some(auto);
let old = Sysproxy::get_system_proxy().ok();
sys.set_system_proxy()?;
*self.old_sysproxy.lock() = old;
*self.cur_sysproxy.lock() = Some(sys);
return Ok(());
}
}
#[cfg(target_os = "windows")]
{
if !sys_enable {
return self.reset_sysproxy().await;
}
use crate::core::handle::Handle;
use crate::utils::dirs;
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
bail!("sysproxy.exe not found");
}
// run the system proxy guard
self.guard_proxy();
Ok(())
}
/// update the system proxy
pub fn update_sysproxy(&self) -> Result<()> {
let mut cur_sysproxy = self.cur_sysproxy.lock();
let old_sysproxy = self.old_sysproxy.lock();
let mut cur_autoproxy = self.cur_autoproxy.lock();
let old_autoproxy = self.old_autoproxy.lock();
let (enable, pac) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
)
};
if pac && (cur_autoproxy.is_none() || old_autoproxy.is_none()) {
drop(cur_autoproxy);
drop(old_autoproxy);
return self.init_sysproxy();
}
if !pac && (cur_sysproxy.is_none() || old_sysproxy.is_none()) {
drop(cur_sysproxy);
drop(old_sysproxy);
return self.init_sysproxy();
}
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let pac_port = IVerge::get_singleton_port();
let mut sysproxy = cur_sysproxy.take().unwrap();
sysproxy.bypass = get_bypass();
sysproxy.port = port;
let mut autoproxy = cur_autoproxy.take().unwrap();
autoproxy.url = format!("http://127.0.0.1:{pac_port}/commands/pac");
if pac {
sysproxy.enable = false;
sysproxy.set_system_proxy()?;
*cur_sysproxy = Some(sysproxy);
autoproxy.enable = enable;
autoproxy.set_auto_proxy()?;
*cur_autoproxy = Some(autoproxy);
let shell = app_handle.shell();
let output = if pac_enable {
let address = format!("http://{}:{}/commands/pac", "127.0.0.1", pac_port);
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["pac", address.as_str()])
.output()
.await
.unwrap();
output
} else {
autoproxy.enable = false;
autoproxy.set_auto_proxy()?;
*cur_autoproxy = Some(autoproxy);
sysproxy.enable = enable;
sysproxy.set_system_proxy()?;
*cur_sysproxy = Some(sysproxy);
let address = format!("{}:{}", "127.0.0.1", port);
let bypass = get_bypass();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["global", address.as_str(), bypass.as_ref()])
.output()
.await
.unwrap();
output
};
if !output.status.success() {
bail!("sysproxy exe run failed");
}
}
Ok(())
}
/// reset the sysproxy
pub fn reset_sysproxy(&self) -> Result<()> {
let mut cur_sysproxy = self.cur_sysproxy.lock();
let mut old_sysproxy = self.old_sysproxy.lock();
let mut cur_autoproxy = self.cur_autoproxy.lock();
let mut old_autoproxy = self.old_autoproxy.lock();
let cur_sysproxy = cur_sysproxy.take();
let cur_autoproxy = cur_autoproxy.take();
if let Some(mut old) = old_sysproxy.take() {
// 如果原代理和当前代理 端口一致就disable关闭否则就恢复原代理设置
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
let port_same = cur_sysproxy.map_or(true, |cur| old.port == cur.port);
if old.enable && port_same {
old.enable = false;
log::info!(target: "app", "reset proxy by disabling the original proxy");
} else {
log::info!(target: "app", "reset proxy to the original proxy");
pub async fn reset_sysproxy(&self) -> Result<()> {
let _lock = self.reset_sysproxy.lock().await;
//直接关闭所有代理
#[cfg(not(target_os = "windows"))]
{
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
let mut autoproxy = Autoproxy::get_auto_proxy()?;
sysproxy.enable = false;
autoproxy.enable = false;
autoproxy.set_auto_proxy()?;
sysproxy.set_system_proxy()?;
}
old.set_system_proxy()?;
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy {
// 没有原代理就按现在的代理设置disable即可
log::info!(target: "app", "reset proxy by disabling the current proxy");
cur.enable = false;
cur.set_system_proxy()?;
} else {
log::info!(target: "app", "reset proxy with no action");
}
if let Some(mut old) = old_autoproxy.take() {
// 如果原代理和当前代理 URL一致就disable关闭否则就恢复原代理设置
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
let url_same = cur_autoproxy.map_or(true, |cur| old.url == cur.url);
if old.enable && url_same {
old.enable = false;
log::info!(target: "app", "reset proxy by disabling the original proxy");
} else {
log::info!(target: "app", "reset proxy to the original proxy");
}
old.set_auto_proxy()?;
} else if let Some(mut cur @ Autoproxy { enable: true, .. }) = cur_autoproxy {
// 没有原代理就按现在的代理设置disable即可
log::info!(target: "app", "reset proxy by disabling the current proxy");
cur.enable = false;
cur.set_auto_proxy()?;
} else {
log::info!(target: "app", "reset proxy with no action");
}
Ok(())
}
/// init the auto launch
pub fn init_launch(&self) -> Result<()> {
let app_exe = current_exe()?;
// let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe
.file_stem()
.and_then(|f| f.to_str())
.ok_or(anyhow!("failed to get file stem"))?;
let app_path = app_exe
.as_os_str()
.to_str()
.ok_or(anyhow!("failed to get app_path"))?
.to_string();
// fix issue #26
#[cfg(target_os = "windows")]
let app_path = format!("\"{app_path}\"");
// use the /Applications/Clash Verge.app path
#[cfg(target_os = "macos")]
let app_path = (|| -> Option<String> {
let path = std::path::PathBuf::from(&app_path);
let path = path.parent()?.parent()?.parent()?;
let extension = path.extension()?.to_str()?;
match extension == "app" {
true => Some(path.as_os_str().to_str()?.to_string()),
false => None,
}
})()
.unwrap_or(app_path);
// fix #403
#[cfg(target_os = "linux")]
let app_path = {
{
use crate::core::handle::Handle;
use tauri::Manager;
use crate::utils::dirs;
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
let handle = Handle::global();
match handle.app_handle.lock().as_ref() {
Some(app_handle) => {
let appimage = app_handle.env().appimage;
appimage
.and_then(|p| p.to_str().map(|s| s.to_string()))
.unwrap_or(app_path)
let app_handle = Handle::global().app_handle().unwrap();
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
bail!("sysproxy.exe not found");
}
let shell = app_handle.shell();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["set", "1"])
.output()
.await
.unwrap();
if !output.status.success() {
bail!("sysproxy exe run failed");
}
None => app_path,
}
};
let auto = AutoLaunchBuilder::new()
.set_app_name(app_name)
.set_app_path(&app_path)
.build()?;
*self.auto_launch.lock() = Some(auto);
Ok(())
}
/// update the startup
pub fn update_launch(&self) -> Result<()> {
let auto_launch = self.auto_launch.lock();
if auto_launch.is_none() {
drop(auto_launch);
return self.init_launch();
}
let _lock = self.auto_launch.lock();
let enable = { Config::verge().latest().enable_auto_launch };
let enable = enable.unwrap_or(false);
let auto_launch = auto_launch.as_ref().unwrap();
let app_handle = Handle::global().app_handle().unwrap();
let autostart_manager = app_handle.autolaunch();
println!("enable: {}", enable);
match enable {
true => auto_launch.enable()?,
false => log_err!(auto_launch.disable()), // 忽略关闭的错误
true => log_err!(autostart_manager.enable()),
false => log_err!(autostart_manager.disable()),
};
Ok(())
}
/// launch a system proxy guard
/// read config from file directly
pub fn guard_proxy(&self) {
use tokio::time::{sleep, Duration};
let guard_state = self.guard_state.clone();
fn guard_proxy(&self) {
let _lock = self.guard_state.lock();
tauri::async_runtime::spawn(async move {
// if it is running, exit
let mut state = guard_state.lock().await;
if *state {
return;
}
*state = true;
drop(state);
// default duration is 10s
let mut wait_secs = 10u64;
@ -377,7 +253,7 @@ impl Sysopt {
// stop loop
if !enable || !guard {
break;
continue;
}
// update duration
@ -385,6 +261,20 @@ impl Sysopt {
log::debug!(target: "app", "try to guard the system proxy");
let sysproxy = Sysproxy::get_system_proxy();
let autoproxy = Autoproxy::get_auto_proxy();
if sysproxy.is_err() || autoproxy.is_err() {
log::error!(target: "app", "failed to get the system proxy");
continue;
}
let sysproxy_enable = sysproxy.ok().map(|s| s.enable).unwrap_or(false);
let autoproxy_enable = autoproxy.ok().map(|s| s.enable).unwrap_or(false);
if sysproxy_enable || autoproxy_enable {
continue;
}
let port = {
Config::verge()
.latest()
@ -392,6 +282,8 @@ impl Sysopt {
.unwrap_or(Config::clash().data().get_mixed_port())
};
let pac_port = IVerge::get_singleton_port();
#[cfg(not(target_os = "windows"))]
{
if pac {
let autoproxy = Autoproxy {
enable: true,
@ -410,9 +302,46 @@ impl Sysopt {
}
}
let mut state = guard_state.lock().await;
*state = false;
drop(state);
#[cfg(target_os = "windows")]
{
use crate::core::handle::Handle;
use crate::utils::dirs;
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let binary_path = dirs::service_path().unwrap();
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
break;
}
let shell = app_handle.shell();
let output = if pac {
let address = format!("http://{}:{}/commands/pac", "127.0.0.1", pac_port);
shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["pac", address.as_str()])
.output()
.await
.unwrap()
} else {
let address = format!("{}:{}", "127.0.0.1", port);
let bypass = get_bypass();
shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["global", address.as_str(), bypass.as_ref()])
.output()
.await
.unwrap()
};
if !output.status.success() {
break;
}
};
}
});
}
}

View File

@ -8,41 +8,65 @@ use crate::{
},
};
use anyhow::Result;
use tauri::tray::{MouseButton, MouseButtonState, TrayIconEvent};
use tauri::AppHandle;
use tauri::{
menu::CheckMenuItem,
tray::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId},
};
use tauri::{
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
Wry,
};
use tauri::{AppHandle, Manager};
use super::handle;
pub struct Tray {}
impl Tray {
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
let tray = app_handle.tray_by_id("main").unwrap();
tray.on_tray_icon_event(|tray, event| {
pub fn create_systray() -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let tray_incon_id = TrayIconId::new("main");
let tray = app_handle.tray_by_id(&tray_incon_id).unwrap();
tray.on_tray_icon_event(|_, event| {
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into());
#[cfg(target_os = "macos")]
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
button: MouseButton::Right,
button_state: MouseButtonState::Down,
..
} = event
{
let app = tray.app_handle();
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"main_window" => resolve::create_window(app),
"main_window" => resolve::create_window(),
_ => {}
}
}
#[cfg(not(target_os = "macos"))]
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
..
} = event
{
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"main_window" => resolve::create_window(),
_ => {}
}
}
});
tray.on_menu_event(on_menu_event);
Ok(())
}
pub fn update_part(app_handle: &AppHandle) -> Result<()> {
pub fn update_part() -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let use_zh = { Config::verge().latest().language == Some("zh".into()) };
let version = VERSION.get().unwrap();
let mode = {
@ -58,42 +82,35 @@ impl Tray {
let verge = Config::verge().latest().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);
#[cfg(target_os = "macos")]
let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
let tray = app_handle.tray_by_id("main").unwrap();
#[cfg(target_os = "macos")]
let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let _ = tray.set_menu(Some(create_tray_menu(
app_handle,
&app_handle,
Some(mode.as_str()),
*system_proxy,
*tun_mode,
)?));
// let _ = tray.
#[cfg(target_os = "macos")]
match tray_icon.as_str() {
"monochrome" => {
let _ = tray.set_icon_as_template(true);
}
"colorful" => {
let _ = tray.set_icon_as_template(false);
}
_ => {}
}
let mut indication_icon = if *system_proxy {
let mut use_custom_icon = false;
#[allow(unused)]
let mut indication_icon = if *system_proxy && !*tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"monochrome" => include_bytes!("../../icons/tray-icon-sys-mono.ico").to_vec(),
"colorful" => include_bytes!("../../icons/tray-icon-sys.ico").to_vec(),
"colorful" => {
use_custom_icon = true;
include_bytes!("../../icons/tray-icon-sys.ico").to_vec()
}
_ => include_bytes!("../../icons/tray-icon-sys-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon-sys.ico").to_vec();
if *sysproxy_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("sysproxy.png");
@ -103,37 +120,22 @@ impl Tray {
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
} else {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"monochrome" => include_bytes!("../../icons/tray-icon-mono.ico").to_vec(),
"colorful" => include_bytes!("../../icons/tray-icon.ico").to_vec(),
_ => include_bytes!("../../icons/tray-icon-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon.ico").to_vec();
if *common_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("common.png");
let ico_path = icon_dir_path.join("common.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
{
use_custom_icon = true;
}
}
icon
};
if *tun_mode {
} else if *tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"monochrome" => include_bytes!("../../icons/tray-icon-tun-mono.ico").to_vec(),
"colorful" => include_bytes!("../../icons/tray-icon-tun.ico").to_vec(),
"colorful" => {
use_custom_icon = true;
include_bytes!("../../icons/tray-icon-tun.ico").to_vec()
}
_ => include_bytes!("../../icons/tray-icon-tun-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon-tun.ico").to_vec();
if *tun_tray_icon {
@ -145,9 +147,52 @@ impl Tray {
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
#[cfg(target_os = "macos")]
{
use_custom_icon = true;
}
indication_icon = icon
}
icon
} else {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => {
use_custom_icon = true;
include_bytes!("../../icons/tray-icon.ico").to_vec()
}
_ => include_bytes!("../../icons/tray-icon-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon.ico").to_vec();
if *common_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("common.png");
let ico_path = icon_dir_path.join("common.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
#[cfg(target_os = "macos")]
{
use_custom_icon = true;
}
}
icon
};
#[cfg(target_os = "macos")]
{
if use_custom_icon {
let _ = tray.set_icon_as_template(false);
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&indication_icon)?));
} else {
let _ = tray.set_icon_as_template(true);
}
}
#[cfg(not(target_os = "macos"))]
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&indication_icon)?));
let switch_map = {
@ -191,40 +236,113 @@ fn create_tray_menu(
let use_zh = { Config::verge().latest().language == Some("zh".into()) };
let version = VERSION.get().unwrap();
let rule_mode_text = if mode == "rule" {
"✓ 规则模式"
} else {
"规则模式"
};
let open_window = &MenuItem::with_id(
app_handle,
"open_window",
t!("Dashboard", "打开面板", use_zh),
true,
None::<&str>,
)
.unwrap();
let global_mode_text = if mode == "global" {
"✓ 全局模式"
} else {
"全局模式"
};
let rule_mode = &CheckMenuItem::with_id(
app_handle,
"rule_mode",
t!("Rule Mode", "规则模式", use_zh),
true,
mode == "rule",
None::<&str>,
)
.unwrap();
let direct_mode_text = if mode == "direct" {
"✓ 直连模式"
} else {
"直连模式"
};
let global_mode = &CheckMenuItem::with_id(
app_handle,
"global_mode",
t!("Global Mode", "全局模式", use_zh),
true,
mode == "global",
None::<&str>,
)
.unwrap();
let system_proxy_text = if system_proxy_enabled {
"✓ 系统代理"
} else {
"系统代理"
};
let direct_mode = &CheckMenuItem::with_id(
app_handle,
"direct_mode",
t!("Direct Mode", "直连模式", use_zh),
true,
mode == "direct",
None::<&str>,
)
.unwrap();
let tun_mode_text = if tun_mode_enabled {
"✓ Tun 模式"
} else {
"Tun 模式"
};
let system_proxy = &CheckMenuItem::with_id(
app_handle,
"system_proxy",
t!("System Proxy", "系统代理", use_zh),
true,
system_proxy_enabled,
None::<&str>,
)
.unwrap();
let tun_mode = &CheckMenuItem::with_id(
app_handle,
"tun_mode",
t!("TUN Mode", "Tun模式", use_zh),
true,
tun_mode_enabled,
None::<&str>,
)
.unwrap();
let copy_env = &MenuItem::with_id(
app_handle,
"copy_env",
t!("Copy Env", "复制环境变量", use_zh),
true,
None::<&str>,
)
.unwrap();
let open_app_dir = &MenuItem::with_id(
app_handle,
"open_app_dir",
t!("Conf Dir", "配置目录", use_zh),
true,
None::<&str>,
)
.unwrap();
let open_core_dir = &MenuItem::with_id(
app_handle,
"open_core_dir",
t!("Core Dir", "内核目录", use_zh),
true,
None::<&str>,
)
.unwrap();
let open_logs_dir = &MenuItem::with_id(
app_handle,
"open_logs_dir",
t!("Logs Dir", "日志目录", use_zh),
true,
None::<&str>,
)
.unwrap();
let open_dir = &Submenu::with_id_and_items(
app_handle,
"open_dir",
t!("Open Dir", "打开目录", use_zh),
true,
&[open_app_dir, open_core_dir, open_logs_dir],
)
.unwrap();
let restart_clash = &MenuItem::with_id(
app_handle,
"restart_clash",
t!("Restart App", "重启 Clash", use_zh),
t!("Restart Clash Core", "重启Clash内核", use_zh),
true,
None::<&str>,
)
@ -233,7 +351,7 @@ fn create_tray_menu(
let restart_app = &MenuItem::with_id(
app_handle,
"restart_app",
t!("Restart App", "重启应用", use_zh),
t!("Restart App", "重启App", use_zh),
true,
None::<&str>,
)
@ -248,131 +366,67 @@ fn create_tray_menu(
)
.unwrap();
let menu = tauri::menu::MenuBuilder::new(app_handle)
.item(
&MenuItem::with_id(
app_handle,
"open_window",
t!("Dashboard", "打开面板", use_zh),
true,
None::<&str>,
)
.unwrap(),
)
.item(&PredefinedMenuItem::separator(app_handle).unwrap())
.item(
&MenuItem::with_id(
app_handle,
"rule_mode",
t!("Rule Mode", rule_mode_text, use_zh),
true,
None::<&str>,
)
.unwrap(),
)
.item(
&MenuItem::with_id(
app_handle,
"global_mode",
t!("Global Mode", global_mode_text, use_zh),
true,
None::<&str>,
)
.unwrap(),
)
.item(
&MenuItem::with_id(
app_handle,
"direct_mode",
t!("Direct Mode", direct_mode_text, use_zh),
true,
None::<&str>,
)
.unwrap(),
)
.item(&PredefinedMenuItem::separator(app_handle).unwrap())
.item(
&MenuItem::with_id(
app_handle,
"system_proxy",
t!("System Proxy", system_proxy_text, use_zh),
true,
None::<&str>,
)
.unwrap(),
)
.item(
&MenuItem::with_id(
app_handle,
"tun_mode",
t!("TUN Mode", tun_mode_text, use_zh),
true,
None::<&str>,
)
.unwrap(),
)
.item(
&MenuItem::with_id(
app_handle,
"copy_env",
t!("Copy Env", "复制环境变量", use_zh),
true,
None::<&str>,
)
.unwrap(),
)
.item(
&MenuItem::with_id(
app_handle,
"open_dir",
t!("Open Dir", "打开目录", use_zh),
true,
None::<&str>,
)
.unwrap(),
)
.item(
&Submenu::with_id_and_items(
let more = &Submenu::with_id_and_items(
app_handle,
"more",
t!("More", "更多", use_zh),
true,
&[restart_clash, restart_app, app_version],
)
.unwrap(),
)
.item(&PredefinedMenuItem::separator(app_handle).unwrap())
.item(
&MenuItem::with_id(
.unwrap();
let quit = &MenuItem::with_id(
app_handle,
"quit",
t!("Quit", "退出", use_zh),
true,
Some("CmdOrControl+Q"),
)
.unwrap(),
)
.build()
.unwrap();
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
let menu = tauri::menu::MenuBuilder::new(app_handle)
.items(&[
open_window,
separator,
rule_mode,
global_mode,
direct_mode,
separator,
system_proxy,
tun_mode,
copy_env,
open_dir,
more,
separator,
quit,
])
.build()
.unwrap();
Ok(menu)
}
fn on_menu_event(app_handle: &AppHandle, event: MenuEvent) {
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
match event.id.as_ref() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5];
println!("change mode to: {}", mode);
feat::change_clash_mode(mode.into());
}
"open_window" => resolve::create_window(app_handle),
"open_window" => resolve::create_window(),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"copy_env" => feat::copy_clash_env(app_handle),
"open_dir" => crate::log_err!(cmds::open_app_dir()),
"copy_env" => feat::copy_clash_env(),
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
"restart_clash" => feat::restart_clash_core(),
"restart_app" => tauri::process::restart(&app_handle.env()),
"quit" => cmds::exit_app(app_handle.clone()),
"restart_app" => feat::restart_app(),
"quit" => {
println!("quit");
feat::quit(Some(0));
}
_ => {}
}
}

View File

@ -1,7 +1,7 @@
use serde_yaml::{Mapping, Value};
use std::collections::HashSet;
pub const HANDLE_FIELDS: [&str; 11] = [
pub const HANDLE_FIELDS: [&str; 12] = [
"mode",
"redir-port",
"tproxy-port",
@ -13,6 +13,7 @@ pub const HANDLE_FIELDS: [&str; 11] = [
"ipv6",
"external-controller",
"secret",
"unified-delay",
];
pub const DEFAULT_FIELDS: [&str; 5] = [

View File

@ -54,9 +54,7 @@ fn test_merge() -> anyhow::Result<()> {
let merge = serde_yaml::from_str::<Mapping>(merge)?;
let config = serde_yaml::from_str::<Mapping>(config)?;
let result = serde_yaml::to_string(&use_merge(merge, config))?;
println!("{result}");
let _ = serde_yaml::to_string(&use_merge(merge, config))?;
Ok(())
}

View File

@ -103,9 +103,7 @@ fn test_script() {
let config = serde_yaml::from_str(config).unwrap();
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
let config_str = serde_yaml::to_string(&config).unwrap();
println!("{config_str}");
let _ = serde_yaml::to_string(&config).unwrap();
dbg!(results);
}

View File

@ -12,7 +12,7 @@ pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
let append = seq_map.append;
let delete = seq_map.delete;
let origin_seq = config.get(&name).map_or(Sequence::default(), |val| {
let origin_seq = config.get(name).map_or(Sequence::default(), |val| {
val.as_sequence().unwrap_or(&Sequence::default()).clone()
});
let mut seq = origin_seq.clone();
@ -23,7 +23,7 @@ pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
if let Some(name) = if item.is_string() {
Some(item)
} else {
item.get("name").map(|y| y.clone())
item.get("name").cloned()
} {
delete_names.push(name.clone());
}
@ -34,7 +34,7 @@ pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
} else {
x.get("name")
} {
!delete_names.contains(&x_name)
!delete_names.contains(x_name)
} else {
true
}
@ -51,5 +51,5 @@ pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
let mut config = config.clone();
config.insert(Value::from(name), Value::from(seq));
return config;
config
}

View File

@ -1,4 +1,3 @@
use crate::{core::service, log_err};
use serde_yaml::{Mapping, Value};
macro_rules! revise {
@ -9,6 +8,7 @@ macro_rules! revise {
}
// if key not exists then append value
#[allow(unused_macros)]
macro_rules! append {
($map: expr, $key: expr, $val: expr) => {
let ret_key = Value::String($key.into());
@ -21,58 +21,33 @@ macro_rules! append {
pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key);
if !enable && tun_val.is_none() {
return config;
}
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
revise!(tun_val, "enable", enable);
revise!(config, "tun", tun_val);
if enable {
log_err!(service::set_dns_by_service().await);
use_dns_for_tun(config)
} else {
log_err!(service::unset_dns_by_service().await);
config
}
}
fn use_dns_for_tun(mut config: Mapping) -> Mapping {
let dns_key = Value::from("dns");
let dns_val = config.get(&dns_key);
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
// 开启tun将同时开启dns
if enable {
revise!(dns_val, "enable", true);
revise!(dns_val, "ipv6", true);
revise!(dns_val, "enhanced-mode", "fake-ip");
revise!(dns_val, "fake-ip-range", "10.96.0.0/16");
#[cfg(target_os = "macos")]
{
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("10.96.0.2".to_string()).await;
}
} else {
revise!(dns_val, "enhanced-mode", "redir-host");
#[cfg(target_os = "macos")]
crate::utils::resolve::restore_public_dns().await;
}
append!(dns_val, "enhanced-mode", "fake-ip");
append!(dns_val, "fake-ip-range", "198.18.0.1/16");
append!(
dns_val,
"nameserver",
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
);
append!(dns_val, "fallback", vec![] as Vec<&str>);
#[cfg(target_os = "windows")]
append!(
dns_val,
"fake-ip-filter",
vec![
"dns.msftncsi.com",
"www.msftncsi.com",
"www.msftconnecttest.com"
]
);
revise!(tun_val, "enable", enable);
revise!(config, "tun", tun_val);
revise!(config, "dns", dns_val);
config
}

View File

@ -7,31 +7,31 @@
use crate::config::*;
use crate::core::*;
use crate::log_err;
use crate::utils::dirs::app_home_dir;
use crate::utils::resolve;
use anyhow::{bail, Result};
use reqwest_dav::list_cmd::ListFile;
use serde_yaml::{Mapping, Value};
use tauri::{AppHandle, Manager};
use std::fs;
use tauri::Manager;
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
// 打开面板
pub fn open_or_close_dashboard() {
let handle = handle::Handle::global();
let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
if let Some(window) = app_handle.get_webview_window("main") {
if let Some(window) = handle::Handle::global().get_window() {
if let Ok(true) = window.is_focused() {
let _ = window.close();
let _ = window.hide();
return;
}
}
resolve::create_window(app_handle);
}
resolve::create_window();
}
// 重启clash
pub fn restart_clash_core() {
tauri::async_runtime::spawn(async {
match CoreManager::global().run_core().await {
match CoreManager::global().restart_core().await {
Ok(_) => {
handle::Handle::refresh_clash();
handle::Handle::notice_message("set_config::ok", "ok");
@ -44,6 +44,19 @@ pub fn restart_clash_core() {
});
}
pub fn restart_app() {
tauri::async_runtime::spawn_blocking(|| {
tauri::async_runtime::block_on(async {
log_err!(CoreManager::global().stop_core().await);
});
resolve::resolve_reset();
let app_handle = handle::Handle::global().app_handle().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
let _ = app_handle.save_window_state(StateFlags::default());
tauri::process::restart(&app_handle.env());
});
}
// 切换模式 rule/global/direct/script mode
pub fn change_clash_mode(mode: String) {
let mut mapping = Mapping::new();
@ -103,6 +116,22 @@ pub fn toggle_tun_mode() {
});
}
pub fn quit(code: Option<i32>) {
let app_handle = handle::Handle::global().app_handle().unwrap();
handle::Handle::global().set_is_exiting();
resolve::resolve_reset();
log_err!(handle::Handle::global().get_window().unwrap().close());
match app_handle.save_window_state(StateFlags::all()) {
Ok(_) => {
log::info!(target: "app", "window state saved successfully");
}
Err(e) => {
log::error!(target: "app", "failed to save window state: {}", e);
}
};
app_handle.exit(code.unwrap_or(0));
}
/// 修改clash的订阅
pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone());
@ -111,15 +140,16 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
// 激活订阅
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
Config::generate().await?;
CoreManager::global().run_core().await?;
CoreManager::global().restart_core().await?;
handle::Handle::refresh_clash();
}
} else {
if patch.get("mode").is_some() {
log_err!(handle::Handle::update_systray_part());
}
Config::runtime().latest().patch_config(patch);
update_core_config(false).await?;
}
<Result<()>>::Ok(())
};
@ -140,6 +170,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
/// 一般都是一个个的修改
pub async fn patch_verge(patch: IVerge) -> Result<()> {
Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch;
let system_proxy = patch.enable_system_proxy;
@ -150,6 +181,8 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let mixed_port = patch.verge_mixed_port;
#[cfg(target_os = "macos")]
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;
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
let tun_tray_icon = patch.tun_tray_icon;
@ -165,34 +198,26 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let socks_port = patch.verge_socks_port;
let http_enabled = patch.verge_http_enabled;
let http_port = patch.verge_port;
let res = {
let service_mode = patch.enable_service_mode;
let mut generated = false;
if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
if !generated {
Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
} else if tun_mode.is_some() {
update_core_config().await?;
let res: std::result::Result<(), anyhow::Error> = {
let mut should_restart_core = false;
let mut should_update_clash_config = false;
let mut should_update_launch = false;
let mut should_update_sysproxy = false;
let mut should_update_systray_part = false;
if tun_mode.is_some() {
should_update_clash_config = true;
}
#[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() || redir_port.is_some() {
if !generated {
Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
should_restart_core = true;
}
#[cfg(target_os = "linux")]
if tproxy_enabled.is_some() || tproxy_port.is_some() {
if !generated {
Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
should_restart_core = true;
}
if socks_enabled.is_some()
|| http_enabled.is_some()
@ -200,13 +225,10 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|| http_port.is_some()
|| mixed_port.is_some()
{
if !generated {
Config::generate().await?;
CoreManager::global().run_core().await?;
}
should_restart_core = true;
}
if auto_launch.is_some() {
sysopt::Sysopt::global().update_launch()?;
should_update_launch = true;
}
if system_proxy.is_some()
|| proxy_bypass.is_some()
@ -214,30 +236,39 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|| pac.is_some()
|| pac_content.is_some()
{
sysopt::Sysopt::global().update_sysproxy()?;
sysopt::Sysopt::global().guard_proxy();
should_update_sysproxy = true;
}
if let Some(true) = patch.enable_proxy_guard {
sysopt::Sysopt::global().guard_proxy();
if language.is_some()
|| system_proxy.is_some()
|| tun_mode.is_some()
|| common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
|| tray_icon.is_some()
{
should_update_systray_part = true;
}
if should_restart_core {
Config::generate().await?;
CoreManager::global().restart_core().await?;
}
if should_update_clash_config {
update_core_config(false).await?;
}
if should_update_launch {
sysopt::Sysopt::global().update_launch()?;
}
if should_update_sysproxy {
sysopt::Sysopt::global().update_sysproxy().await?;
}
if let Some(hotkeys) = patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys)?;
}
if language.is_some() {
handle::Handle::update_systray()?;
} else if system_proxy.is_some()
|| tun_mode.is_some()
|| common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
{
handle::Handle::update_systray_part()?;
}
#[cfg(target_os = "macos")]
if tray_icon.is_some() {
if should_update_systray_part {
handle::Handle::update_systray_part()?;
}
@ -247,6 +278,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
Ok(()) => {
Config::verge().apply();
Config::verge().data().save_file()?;
Ok(())
}
Err(err) => {
@ -288,29 +320,34 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
};
if should_update {
update_core_config().await?;
update_core_config(true).await?;
}
Ok(())
}
/// 更新订阅
async fn update_core_config() -> Result<()> {
async fn update_core_config(notice: bool) -> Result<()> {
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
if notice {
handle::Handle::notice_message("set_config::ok", "ok");
}
Ok(())
}
Err(err) => {
if notice {
handle::Handle::notice_message("set_config::error", format!("{err}"));
}
Err(err)
}
}
}
/// copy env variable
pub fn copy_clash_env(app_handle: &AppHandle) {
pub fn copy_clash_env() {
let app_handle = handle::Handle::global().app_handle().unwrap();
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
let http_proxy = format!("http://127.0.0.1:{}", port);
let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
@ -387,3 +424,60 @@ pub async fn test_delay(url: String) -> Result<u32> {
}
}
}
pub async fn create_backup_and_upload_webdav() -> Result<()> {
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
log::error!(target: "app", "Failed to create backup: {:#?}", err);
err
})?;
if let Err(err) = backup::WebDavClient::global()
.upload(temp_file_path.clone(), file_name)
.await
{
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
return Err(err);
}
if let Err(err) = std::fs::remove_file(&temp_file_path) {
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
}
Ok(())
}
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
backup::WebDavClient::global().list().await.map_err(|err| {
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
err
})
}
pub async fn delete_webdav_backup(filename: String) -> Result<()> {
backup::WebDavClient::global()
.delete(filename)
.await
.map_err(|err| {
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
err
})
}
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let backup_storage_path = app_home_dir().unwrap().join(&filename);
backup::WebDavClient::global()
.download(filename, backup_storage_path.clone())
.await
.map_err(|err| {
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
err
})?;
// extract zip file
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
zip.extract(app_home_dir()?)?;
// 最后删除临时文件
fs::remove_file(backup_storage_path)?;
Ok(())
}

202
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,202 @@
mod cmds;
mod config;
mod core;
mod enhance;
mod feat;
mod utils;
use crate::core::hotkey;
use crate::utils::{resolve, resolve::resolve_scheme, server};
#[cfg(target_os = "macos")]
use tauri::Listener;
use tauri_plugin_autostart::MacosLauncher;
pub fn run() {
// 单例检测
let app_exists: bool = tauri::async_runtime::block_on(async move {
if server::check_singleton().await.is_err() {
println!("app exists");
true
} else {
false
}
});
if app_exists {
return;
}
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
#[cfg(debug_assertions)]
let devtools = tauri_plugin_devtools::init();
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
None,
))
.plugin(tauri_plugin_window_state::Builder::new().build())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.setup(|app| {
#[cfg(target_os = "linux")]
{
use tauri_plugin_deep_link::DeepLinkExt;
log_err!(app.deep_link().register_all());
}
#[cfg(target_os = "macos")]
{
app.listen("deep-link://new-url", |event| {
tauri::async_runtime::spawn(async move {
let payload = event.payload();
log_err!(resolve_scheme(payload.to_owned()).await);
});
});
}
tauri::async_runtime::block_on(async move {
resolve::resolve_setup(app).await;
#[cfg(not(target_os = "macos"))]
{
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
log_err!(resolve_scheme(argvs[1].to_owned()).await);
}
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
// common
cmds::get_sys_proxy,
cmds::get_auto_proxy,
cmds::open_app_dir,
cmds::open_logs_dir,
cmds::open_web_url,
cmds::open_core_dir,
cmds::get_portable_flag,
cmds::get_network_interfaces,
// cmds::kill_sidecar,
cmds::restart_core,
cmds::restart_app,
// clash
cmds::get_clash_info,
cmds::patch_clash_config,
cmds::change_clash_core,
cmds::get_runtime_config,
cmds::get_runtime_yaml,
cmds::get_runtime_exists,
cmds::get_runtime_logs,
cmds::uwp::invoke_uwp_tool,
cmds::copy_clash_env,
// verge
cmds::get_verge_config,
cmds::patch_verge_config,
cmds::test_delay,
cmds::get_app_dir,
cmds::copy_icon_file,
cmds::download_icon_cache,
cmds::open_devtools,
cmds::exit_app,
cmds::get_network_interfaces_info,
// cmds::update_hotkeys,
// profile
cmds::get_profiles,
cmds::enhance_profiles,
cmds::patch_profiles_config,
cmds::view_profile,
cmds::patch_profile,
cmds::create_profile,
cmds::import_profile,
cmds::reorder_profile,
cmds::update_profile,
cmds::delete_profile,
cmds::read_profile_file,
cmds::save_profile_file,
// clash api
cmds::clash_api_get_proxy_delay,
// backup
cmds::create_webdav_backup,
cmds::save_webdav_config,
cmds::list_webdav_backup,
cmds::delete_webdav_backup,
cmds::restore_webdav_backup,
]);
#[cfg(debug_assertions)]
{
builder = builder.plugin(devtools);
}
let app = builder
.build(tauri::generate_context!())
.expect("error while running tauri application");
app.run(|_, e| match e {
tauri::RunEvent::ExitRequested { api, code, .. } => {
if code.is_none() {
api.prevent_exit();
return;
}
}
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
if core::handle::Handle::global().is_exiting() {
return;
}
println!("closing window...");
api.prevent_close();
let window = core::handle::Handle::global().get_window().unwrap();
let _ = window.hide();
}
tauri::WindowEvent::Focused(true) => {
#[cfg(target_os = "macos")]
{
log_err!(hotkey::Hotkey::global().register("CMD+Q", "quit"));
}
#[cfg(not(target_os = "macos"))]
{
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit"));
};
}
tauri::WindowEvent::Focused(false) => {
#[cfg(target_os = "macos")]
{
log_err!(hotkey::Hotkey::global().unregister("CMD+Q"));
}
#[cfg(not(target_os = "macos"))]
{
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
};
}
tauri::WindowEvent::Destroyed => {
#[cfg(target_os = "macos")]
{
log_err!(hotkey::Hotkey::global().unregister("CMD+Q"));
}
#[cfg(not(target_os = "macos"))]
{
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
};
}
_ => {}
}
}
}
_ => {}
});
}

141
src-tauri/src/main.rs Normal file → Executable file
View File

@ -1,139 +1,4 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod cmds;
mod config;
mod core;
mod enhance;
mod feat;
mod utils;
use crate::utils::{resolve, server};
fn main() -> std::io::Result<()> {
// 单例检测
let app_exists: bool = tauri::async_runtime::block_on(async move {
if server::check_singleton().await.is_err() {
println!("app exists");
true
} else {
false
}
});
if app_exists {
return Ok(());
}
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
#[cfg(debug_assertions)]
let devtools = tauri_plugin_devtools::init();
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.setup(|app| {
tauri::async_runtime::block_on(async move {
resolve::resolve_setup(app).await;
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
// common
cmds::get_sys_proxy,
cmds::get_auto_proxy,
cmds::open_app_dir,
cmds::open_logs_dir,
cmds::open_web_url,
cmds::open_core_dir,
cmds::get_portable_flag,
cmds::get_network_interfaces,
// cmds::kill_sidecar,
cmds::restart_sidecar,
// clash
cmds::get_clash_info,
cmds::get_clash_logs,
cmds::patch_clash_config,
cmds::change_clash_core,
cmds::get_runtime_config,
cmds::get_runtime_yaml,
cmds::get_runtime_exists,
cmds::get_runtime_logs,
cmds::uwp::invoke_uwp_tool,
cmds::copy_clash_env,
// verge
cmds::get_verge_config,
cmds::patch_verge_config,
cmds::test_delay,
cmds::get_app_dir,
cmds::copy_icon_file,
cmds::download_icon_cache,
cmds::open_devtools,
cmds::exit_app,
cmds::get_network_interfaces_info,
// cmds::update_hotkeys,
// profile
cmds::get_profiles,
cmds::enhance_profiles,
cmds::patch_profiles_config,
cmds::view_profile,
cmds::patch_profile,
cmds::create_profile,
cmds::import_profile,
cmds::reorder_profile,
cmds::update_profile,
cmds::delete_profile,
cmds::read_profile_file,
cmds::save_profile_file,
// service mode
cmds::service::check_service,
cmds::service::install_service,
cmds::service::uninstall_service,
// clash api
cmds::clash_api_get_proxy_delay
]);
#[cfg(debug_assertions)]
{
builder = builder.plugin(devtools);
}
let app = builder
.build(tauri::generate_context!())
.expect("error while running tauri application");
app.run(|app_handle, e| match e {
tauri::RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
}
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::Destroyed => {
let _ = resolve::save_window_size_position(app_handle, true);
}
tauri::WindowEvent::CloseRequested { .. } => {
let _ = resolve::save_window_size_position(app_handle, true);
}
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
let _ = resolve::save_window_size_position(app_handle, false);
}
_ => {}
}
}
}
_ => {}
});
Ok(())
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

View File

@ -1,19 +1,25 @@
use crate::core::handle;
use anyhow::Result;
use once_cell::sync::OnceCell;
use std::fs;
use std::path::PathBuf;
use tauri::Manager;
#[cfg(not(feature = "verge-dev"))]
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
#[cfg(not(feature = "verge-dev"))]
pub static BACKUP_DIR: &str = "clash-verge-rev-backup";
#[cfg(feature = "verge-dev")]
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
#[cfg(feature = "verge-dev")]
pub static BACKUP_DIR: &str = "clash-verge-rev-backup-dev";
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
static CLASH_CONFIG: &str = "config.yaml";
static VERGE_CONFIG: &str = "verge.yaml";
static PROFILE_YAML: &str = "profiles.yaml";
pub static CLASH_CONFIG: &str = "config.yaml";
pub static VERGE_CONFIG: &str = "verge.yaml";
pub static PROFILE_YAML: &str = "profiles.yaml";
/// init portable flag
pub fn init_portable_flag() -> Result<()> {
@ -44,40 +50,27 @@ pub fn app_home_dir() -> Result<PathBuf> {
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID));
}
let app_handle = handle::Handle::global().app_handle().unwrap();
let handle = handle::Handle::global();
let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
match app_handle.path().data_dir() {
Ok(dir) => {
return Ok(dir.join(APP_ID));
}
Ok(dir) => Ok(dir.join(APP_ID)),
Err(e) => {
log::error!("Failed to get the app home directory: {}", e);
return Err(anyhow::anyhow!("Failed to get the app homedirectory"));
log::error!(target:"app", "Failed to get the app home directory: {}", e);
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
}
}
}
Err(anyhow::anyhow!("failed to get the app home dir"))
}
/// get the resources dir
pub fn app_resources_dir() -> Result<PathBuf> {
let handle = handle::Handle::global();
let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
let app_handle = handle::Handle::global().app_handle().unwrap();
match app_handle.path().resource_dir() {
Ok(dir) => {
return Ok(dir.join("resources"));
}
Ok(dir) => Ok(dir.join("resources")),
Err(e) => {
log::error!("Failed to get the resource directory: {}", e);
return Err(anyhow::anyhow!("Failed to get the resource directory"));
log::error!(target:"app", "Failed to get the resource directory: {}", e);
Err(anyhow::anyhow!("Failed to get the resource directory"))
}
}
};
};
Err(anyhow::anyhow!("failed to get the resource dir"))
}
/// profiles dir
@ -102,11 +95,7 @@ pub fn profiles_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join(PROFILE_YAML))
}
pub fn clash_pid_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join("clash.pid"))
}
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "macos")]
pub fn service_path() -> Result<PathBuf> {
Ok(app_resources_dir()?.join("clash-verge-service"))
}
@ -137,3 +126,27 @@ pub fn path_to_str(path: &PathBuf) -> Result<&str> {
.ok_or(anyhow::anyhow!("failed to get path from {:?}", path))?;
Ok(path_str)
}
pub fn get_encryption_key() -> Result<Vec<u8>> {
let app_dir = app_home_dir()?;
let key_path = app_dir.join(".encryption_key");
if key_path.exists() {
// Read existing key
fs::read(&key_path).map_err(|e| anyhow::anyhow!("Failed to read encryption key: {}", e))
} else {
// Generate and save new key
let mut key = vec![0u8; 32];
getrandom::getrandom(&mut key)?;
// Ensure directory exists
if let Some(parent) = key_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| anyhow::anyhow!("Failed to create key directory: {}", e))?;
}
// Save key
fs::write(&key_path, &key)
.map_err(|e| anyhow::anyhow!("Failed to save encryption key: {}", e))?;
Ok(key)
}
}

View File

@ -0,0 +1,40 @@
use crate::log_err;
use anyhow;
use std::{
backtrace::{Backtrace, BacktraceStatus},
thread,
};
pub fn redirect_panic_to_log() {
std::panic::set_hook(Box::new(move |panic_info| {
let thread = thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>");
let payload = panic_info.payload();
let payload = if let Some(s) = payload.downcast_ref::<&str>() {
&**s
} else if let Some(s) = payload.downcast_ref::<String>() {
s
} else {
&format!("{:?}", payload)
};
let location = panic_info
.location()
.map(|l| l.to_string())
.unwrap_or("unknown location".to_string());
let backtrace = Backtrace::capture();
let backtrace = if backtrace.status() == BacktraceStatus::Captured {
&format!("stack backtrace:\n{}", backtrace)
} else {
"note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace"
};
let err: Result<(), anyhow::Error> = Err(anyhow::anyhow!(format!(
"thread '{}' panicked at {}:\n{}\n{}",
thread_name, location, payload, backtrace
)));
log_err!(err);
}));
}

View File

@ -101,10 +101,38 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
/// open file
/// use vscode by default
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
app.shell().open(path.to_string_lossy(), None).unwrap();
#[cfg(target_os = "macos")]
let code = "Visual Studio Code";
#[cfg(not(target_os = "macos"))]
let code = "code";
if let Err(err) = open::with(&path.as_os_str(), code) {
log::error!(target: "app", "Can not open file with VS code, {}", err);
// default open
app.shell().open(path.to_string_lossy(), None)?;
};
Ok(())
}
#[cfg(target_os = "linux")]
pub fn linux_elevator() -> String {
use std::process::Command;
match Command::new("which").arg("pkexec").output() {
Ok(output) => {
if !output.stdout.is_empty() {
// Convert the output to a string slice
if let Ok(path) = std::str::from_utf8(&output.stdout) {
path.trim().to_string()
} else {
"sudo".to_string()
}
} else {
"sudo".to_string()
}
}
Err(_) => "sudo".to_string(),
}
}
#[macro_export]
macro_rules! error {
($result: expr) => {

View File

@ -1,4 +1,5 @@
use crate::config::*;
use crate::core::handle;
use crate::utils::{dirs, help};
use anyhow::Result;
use chrono::{Local, TimeZone};
@ -10,7 +11,6 @@ use log4rs::encode::pattern::PatternEncoder;
use std::fs::{self, DirEntry};
use std::path::PathBuf;
use std::str::FromStr;
use tauri::AppHandle;
use tauri_plugin_shell::ShellExt;
/// initialize this instance's log file
@ -44,22 +44,10 @@ fn init_log() -> Result<()> {
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
#[cfg(feature = "verge-dev")]
{
logger_builder = logger_builder.appenders(["file", "stdout"]);
if log_more {
root_builder = root_builder.appenders(["file", "stdout"]);
} else {
root_builder = root_builder.appenders(["stdout"]);
}
}
#[cfg(not(feature = "verge-dev"))]
{
logger_builder = logger_builder.appenders(["file"]);
if log_more {
root_builder = root_builder.appenders(["file"]);
}
}
let (config, _) = log4rs::config::Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
@ -207,8 +195,10 @@ pub fn init_resources() -> Result<()> {
#[cfg(target_os = "windows")]
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "macos")]
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
#[cfg(target_os = "linux")]
let file_list: [&str; 0] = [];
// copy the resource file
// if the source file is newer than the destination file, copy it over
@ -216,12 +206,13 @@ pub fn init_resources() -> Result<()> {
let src_path = res_dir.join(file);
let dest_path = app_dir.join(file);
let test_dest_path = test_dir.join(file);
log::debug!(target: "app", "src_path: {src_path:?}, dest_path: {dest_path:?}");
let handle_copy = |dest: &PathBuf| {
match fs::copy(&src_path, dest) {
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
Err(err) => {
log::error!(target: "app", "failed to copy resources '{file}', {err}")
log::error!(target: "app", "failed to copy resources '{file}' to '{dest:?}', {err}")
}
};
};
@ -297,7 +288,9 @@ pub fn init_scheme() -> Result<()> {
Ok(())
}
pub async fn startup_script(app_handle: &AppHandle) -> Result<()> {
pub async fn startup_script() -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let script_path = {
let verge = Config::verge();
let verge = verge.latest();
@ -330,7 +323,7 @@ pub async fn startup_script(app_handle: &AppHandle) -> Result<()> {
app_handle
.shell()
.command(shell_type)
.current_dir(working_dir.to_path_buf())
.current_dir(working_dir)
.args(&[script_path])
.output()
.await?;

View File

@ -1,4 +1,5 @@
pub mod dirs;
pub mod error;
pub mod help;
pub mod init;
pub mod resolve;

View File

@ -1,12 +1,16 @@
use crate::cmds::import_profile;
use crate::config::IVerge;
use crate::{config::Config, core::*, utils::init, utils::server};
use crate::{log_err, trace_err};
use anyhow::Result;
use crate::utils::error;
use crate::{config::Config, config::PrfItem, core::*, utils::init, utils::server};
use crate::{log_err, trace_err, wrap_err};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use percent_encoding::percent_decode_str;
use serde_yaml::Mapping;
use std::net::TcpListener;
use tauri::{App, AppHandle, Manager};
use tauri::{App, Manager};
use tauri_plugin_window_state::{StateFlags, WindowExt};
use url::Url;
//#[cfg(not(target_os = "linux"))]
// use window_shadows::set_shadow;
use tauri_plugin_notification::NotificationExt;
@ -32,98 +36,100 @@ pub fn find_unused_port() -> Result<u16> {
/// handle something when start app
pub async fn resolve_setup(app: &mut App) {
error::redirect_panic_to_log();
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
let version = app.package_info().version.to_string();
handle::Handle::global().init(app.app_handle());
VERSION.get_or_init(|| version.clone());
log_err!(init::init_config());
log_err!(init::init_resources());
log_err!(init::init_scheme());
log_err!(init::startup_script(app.app_handle()).await);
log_err!(init::startup_script().await);
// 处理随机端口
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
let mut port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
if enable_random_port {
port = find_unused_port().unwrap_or(
Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port()),
);
}
Config::verge().data().patch_config(IVerge {
verge_mixed_port: Some(port),
..IVerge::default()
});
let _ = Config::verge().data().save_file();
let mut mapping = Mapping::new();
mapping.insert("mixed-port".into(), port.into());
Config::clash().data().patch_config(mapping);
let _ = Config::clash().data().save_config();
log_err!(resolve_random_port_config());
// 启动核心
log::trace!("init config");
log::trace!(target:"app", "init config");
log_err!(Config::init_config().await);
log::trace!("launch core");
log_err!(CoreManager::global().init(app.app_handle()));
if service::check_service().await.is_err() {
match service::reinstall_service().await {
Ok(_) => {
log::info!(target:"app", "install service susccess.");
#[cfg(not(target_os = "macos"))]
std::thread::sleep(std::time::Duration::from_millis(1000));
#[cfg(target_os = "macos")]
{
let mut service_runing = false;
for _ in 0..40 {
if service::check_service().await.is_ok() {
service_runing = true;
break;
} else {
log::warn!(target: "app", "service not runing, sleep 500ms and check again.");
std::thread::sleep(std::time::Duration::from_millis(500));
}
}
if !service_runing {
log::error!(target: "app", "service not runing. exit");
app.app_handle().exit(-2);
}
}
}
Err(e) => {
log::error!(target: "app", "{e:?}");
app.app_handle().exit(-1);
}
}
}
log::trace!(target: "app", "launch core");
log_err!(CoreManager::global().init().await);
// setup a simple http server for singleton
log::trace!("launch embed server");
server::embed_server(app.app_handle());
log::trace!(target: "app", "launch embed server");
server::embed_server();
log::trace!("init system tray");
log_err!(tray::Tray::update_systray(&app.app_handle()));
log::trace!(target: "app", "init system tray");
log_err!(tray::Tray::create_systray());
let silent_start = { Config::verge().data().enable_silent_start };
if !silent_start.unwrap_or(false) {
create_window(&app.app_handle());
create_window();
}
log_err!(sysopt::Sysopt::global().init_launch());
log_err!(sysopt::Sysopt::global().init_sysproxy());
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
log_err!(handle::Handle::update_systray_part());
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
log_err!(hotkey::Hotkey::global().init());
log_err!(timer::Timer::global().init());
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
let param = argvs[1].as_str();
if param.starts_with("clash:") {
log_err!(resolve_scheme(argvs[1].to_owned()).await);
}
}
}
/// reset system proxy
pub fn resolve_reset() {
log_err!(sysopt::Sysopt::global().reset_sysproxy());
tauri::async_runtime::block_on(async move {
log_err!(sysopt::Sysopt::global().reset_sysproxy().await);
log_err!(CoreManager::global().stop_core().await);
log_err!(service::unset_dns_by_service().await);
#[cfg(target_os = "macos")]
restore_public_dns().await;
});
}
/// create main window
pub fn create_window(app_handle: &AppHandle) {
if let Some(window) = app_handle.get_webview_window("main") {
trace_err!(window.unminimize(), "set win unminimize");
pub fn create_window() {
let app_handle = handle::Handle::global().app_handle().unwrap();
if let Some(window) = handle::Handle::global().get_window() {
trace_err!(window.show(), "set win visible");
trace_err!(window.set_focus(), "set win focus");
return;
}
let mut builder = tauri::WebviewWindowBuilder::new(
app_handle,
let builder = tauri::WebviewWindowBuilder::new(
&app_handle,
"main".to_string(),
tauri::WebviewUrl::App("index.html".into()),
)
@ -132,121 +138,98 @@ pub fn create_window(app_handle: &AppHandle) {
.fullscreen(false)
.min_inner_size(600.0, 520.0);
match Config::verge().latest().window_size_position.clone() {
Some(size_pos) if size_pos.len() == 4 => {
let size = (size_pos[0], size_pos[1]);
let pos = (size_pos[2], size_pos[3]);
let w = size.0.clamp(600.0, f64::INFINITY);
let h = size.1.clamp(520.0, f64::INFINITY);
builder = builder.inner_size(w, h).position(pos.0, pos.1);
}
_ => {
#[cfg(target_os = "windows")]
{
builder = builder.inner_size(800.0, 636.0).center();
}
#[cfg(target_os = "macos")]
{
builder = builder.inner_size(800.0, 642.0).center();
}
#[cfg(target_os = "linux")]
{
builder = builder.inner_size(800.0, 642.0).center();
}
}
};
#[cfg(target_os = "windows")]
let window = builder
.decorations(false)
.maximizable(true)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true)
.visible(false)
.build();
.build().unwrap();
#[cfg(target_os = "macos")]
let window = builder
.decorations(true)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.build();
.build()
.unwrap();
#[cfg(target_os = "linux")]
let window = builder.decorations(false).transparent(true).build();
let window = builder
.decorations(false)
.transparent(true)
.build()
.unwrap();
match window {
Ok(win) => {
let is_maximized = Config::verge()
.latest()
.window_is_maximized
.unwrap_or(false);
log::trace!("try to calculate the monitor size");
let center = (|| -> Result<bool> {
let mut center = false;
let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?;
let size = monitor.size();
let pos = win.outer_position()?;
match window.restore_state(StateFlags::all()) {
Ok(_) => {
log::info!(target: "app", "window state restored successfully");
}
Err(e) => {
log::error!(target: "app", "failed to restore window state: {}", e);
#[cfg(target_os = "windows")]
window
.set_size(tauri::Size::Physical(tauri::PhysicalSize {
width: 800,
height: 636,
}))
.unwrap();
if pos.x < -400
|| pos.x > (size.width - 200) as i32
|| pos.y < -200
|| pos.y > (size.height - 200) as i32
{
center = true;
#[cfg(not(target_os = "windows"))]
window
.set_size(tauri::Size::Physical(tauri::PhysicalSize {
width: 800,
height: 642,
}))
.unwrap();
}
Ok(center)
})();
if center.unwrap_or(true) {
trace_err!(win.center(), "set win center");
}
// #[cfg(not(target_os = "linux"))]
// trace_err!(set_shadow(&win, true), "set win shadow");
if is_maximized {
trace_err!(win.maximize(), "set win maximize");
}
}
Err(_) => {
log::error!("failed to create window");
}
}
}
/// save window size and position
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
let verge = Config::verge();
let mut verge = verge.latest();
if save_to_file {
verge.save_file()?;
}
let win = app_handle
.get_webview_window("main")
.ok_or(anyhow::anyhow!("failed to get window"))?;
let scale = win.scale_factor()?;
let size = win.inner_size()?;
let size = size.to_logical::<f64>(scale);
let pos = win.outer_position()?;
let pos = pos.to_logical::<f64>(scale);
let is_maximized = win.is_maximized()?;
verge.window_is_maximized = Some(is_maximized);
if !is_maximized && size.width >= 600.0 && size.height >= 520.0 {
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
}
Ok(())
};
}
pub async fn resolve_scheme(param: String) -> Result<()> {
let url = param
.trim_start_matches("clash://install-config/?url=")
.trim_start_matches("clash://install-config?url=");
log::info!(target:"app", "received deep link: {}", param);
let handle = handle::Handle::global();
let app_handle = handle.app_handle.lock().clone();
if let Some(app_handle) = app_handle.as_ref() {
match import_profile(url.to_string(), None).await {
Ok(_) => {
let app_handle = handle::Handle::global().app_handle().unwrap();
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()
};
// 解析 URL
let link_parsed = match Url::parse(param_str) {
Ok(url) => url,
Err(e) => {
bail!("failed to parse deep link: {:?}, param: {:?}", e, param);
}
};
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
let name = link_parsed
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.into_owned());
let encode_url = link_parsed
.query_pairs()
.find(|(key, _)| key == "url")
.map(|(_, value)| value.into_owned());
match encode_url {
Some(url) => {
let url = percent_decode_str(url.as_ref())
.decode_utf8_lossy()
.to_string();
create_window();
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = item.uid.clone().unwrap();
let _ = wrap_err!(Config::profiles().data().append_item(item));
app_handle
.notification()
.builder()
@ -254,6 +237,8 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
.body("Import profile success")
.show()
.unwrap();
handle::Handle::notice_message("import_sub_url::ok", uid);
}
Err(e) => {
app_handle
@ -263,9 +248,116 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
.body(format!("Import profile failed: {e}"))
.show()
.unwrap();
log::error!("Import profile failed: {e}");
handle::Handle::notice_message("import_sub_url::error", e.to_string());
bail!("Failed to add subscriptions: {e}");
}
}
}
None => bail!("failed to get profile url"),
}
}
Ok(())
}
fn resolve_random_port_config() -> Result<()> {
let verge_config = Config::verge();
let clash_config = Config::clash();
let enable_random_port = verge_config.latest().enable_random_port.unwrap_or(false);
let default_port = verge_config
.latest()
.verge_mixed_port
.unwrap_or(clash_config.data().get_mixed_port());
let port = if enable_random_port {
find_unused_port().unwrap_or(default_port)
} else {
default_port
};
verge_config.data().patch_config(IVerge {
verge_mixed_port: Some(port),
..IVerge::default()
});
verge_config.data().save_file()?;
let mut mapping = Mapping::new();
mapping.insert("mixed-port".into(), port.into());
clash_config.data().patch_config(mapping);
clash_config.data().save_config()?;
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn set_public_dns(dns_server: String) {
use crate::core::handle;
use crate::utils::dirs;
use tauri_plugin_shell::ShellExt;
let app_handle = handle::Handle::global().app_handle().unwrap();
log::info!(target: "app", "try to set system dns");
let resource_dir = dirs::app_resources_dir().unwrap();
let script = resource_dir.join("set_dns.sh");
if !script.exists() {
log::error!(target: "app", "set_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script, dns_server])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "set system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "set system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "set system dns failed: {err}");
}
}
}
#[cfg(target_os = "macos")]
pub async fn restore_public_dns() {
use crate::core::handle;
use crate::utils::dirs;
use tauri_plugin_shell::ShellExt;
let app_handle = handle::Handle::global().app_handle().unwrap();
log::info!(target: "app", "try to unset system dns");
let resource_dir = dirs::app_resources_dir().unwrap();
let script = resource_dir.join("unset_dns.sh");
if !script.exists() {
log::error!(target: "app", "unset_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "unset system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "unset system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "unset system dns failed: {err}");
}
}
}

View File

@ -2,11 +2,10 @@ extern crate warp;
use super::resolve;
use crate::config::{Config, IVerge, DEFAULT_PAC};
use crate::log_err;
use anyhow::{bail, Result};
use port_scanner::local_port_available;
use std::convert::Infallible;
use tauri::AppHandle;
use warp::http::StatusCode;
use warp::Filter;
#[derive(serde::Deserialize, Debug)]
@ -17,36 +16,24 @@ struct QueryParam {
/// check whether there is already exists
pub async fn check_singleton() -> Result<()> {
let port = IVerge::get_singleton_port();
if !local_port_available(port) {
let resp = reqwest::get(format!("http://127.0.0.1:{port}/commands/ping"))
.await?
.text()
.await?;
if &resp == "ok" {
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
#[cfg(not(target_os = "macos"))]
{
let param = argvs[1].as_str();
if param.starts_with("clash:") {
reqwest::get(format!(
let _ = reqwest::get(format!(
"http://127.0.0.1:{port}/commands/scheme?param={param}"
))
.await?
.text()
.await?;
.await;
}
}
} else {
reqwest::get(format!("http://127.0.0.1:{port}/commands/visible"))
.await?
.text()
.await?;
let _ = reqwest::get(format!("http://127.0.0.1:{port}/commands/visible")).await;
}
bail!("app exists");
}
log::error!("failed to setup singleton listen server");
Ok(())
bail!("app exists");
} else {
Ok(())
}
@ -54,15 +41,12 @@ pub async fn check_singleton() -> Result<()> {
/// The embed server only be used to implement singleton process
/// maybe it can be used as pac server later
pub fn embed_server(app_handle: &AppHandle) {
pub fn embed_server() {
let port = IVerge::get_singleton_port();
let handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
let ping = warp::path!("commands" / "ping").map(move || "ok");
let visible = warp::path!("commands" / "visible").map(move || {
resolve::create_window(&handle);
resolve::create_window();
"ok"
});
@ -82,21 +66,15 @@ pub fn embed_server(app_handle: &AppHandle) {
.body(content)
.unwrap_or_default()
});
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
log_err!(resolve::resolve_scheme(query.param).await);
Ok("ok")
}
let scheme = warp::path!("commands" / "scheme")
.and(warp::query::<QueryParam>())
.and_then(scheme_handler);
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
let result = resolve::resolve_scheme(query.param).await;
Ok(match result {
Ok(_) => warp::reply::with_status("Ok", StatusCode::OK),
Err(_) => {
warp::reply::with_status("Internal Error", StatusCode::INTERNAL_SERVER_ERROR)
}
})
}
let commands = ping.or(visible).or(pac).or(scheme);
let commands = visible.or(scheme).or(pac);
warp::serve(commands).run(([127, 0, 0, 1], port)).await;
});
}

12
src-tauri/tauri.conf.json Normal file → Executable file
View File

@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
"longDescription": "A Clash Meta GUI based on tauri.",
"longDescription": "Clash Verge Rev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@ -15,7 +15,7 @@
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"copyright": "GNU General Public License v3.0",
"category": "DeveloperTool",
"shortDescription": "A Clash Meta GUI based on tauri.",
"shortDescription": "Clash Verge Rev",
"createUpdaterArtifacts": "v1Compatible"
},
"build": {
@ -34,6 +34,11 @@
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
]
},
"deep-link": {
"desktop": {
"schemes": ["clash", "clash-verge"]
}
}
},
"app": {
@ -44,7 +49,6 @@
"enable": true
},
"csp": null
},
"windows": []
}
}
}

View File

@ -1,24 +1,36 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": {
"targets": ["deb", "rpm"],
"linux": {
"deb": {
"depends": ["openssl"],
"desktopTemplate": "./template/clash-verge.desktop",
"desktopTemplate": "./packages/linux/clash-verge.desktop",
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
"replaces": ["clash-verge"]
"replaces": ["clash-verge"],
"postInstallScript": "./packages/linux/post-install.sh",
"preRemoveScript": "./packages/linux/pre-remove.sh"
},
"rpm": {
"depends": ["openssl"],
"desktopTemplate": "./template/clash-verge.desktop",
"desktopTemplate": "./packages/linux/clash-verge.desktop",
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
"obsoletes": ["clash-verge"]
}
"obsoletes": ["clash-verge"],
"postInstallScript": "./packages/linux/post-install.sh",
"preRemoveScript": "./packages/linux/pre-remove.sh"
}
},
"externalBin": [
"./resources/clash-verge-service",
"./resources/install-service",
"./resources/uninstall-service",
"./sidecar/verge-mihomo",
"./sidecar/verge-mihomo-alpha"
]
},
"app": {
"trayIcon": {
"iconPath": "icons/tray-icon.ico"

View File

@ -1,7 +1,9 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": {
"targets": ["app", "dmg"],
"resources": ["resources"],
"macOS": {
"frameworks": [],
"minimumSystemVersion": "10.15",

View File

@ -1,5 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": {
"targets": ["nsis"],
"windows": {
@ -15,14 +16,16 @@
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"installMode": "perMachine",
"template": "./template/installer.nsi"
"template": "./packages/windows/installer.nsi"
}
}
},
"app": {
"trayIcon": {
"iconPath": "icons/tray-icon.ico",
"iconAsTemplate": true
}
"iconAsTemplate": true,
"menuOnLeftClick": false
},
"windows": []
}
}

View File

@ -16,7 +16,7 @@
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"installMode": "perMachine",
"template": "./template/installer.nsi"
"template": "./packages/windows/installer.nsi"
}
}
},

View File

@ -16,7 +16,7 @@
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"installMode": "perMachine",
"template": "./template/installer.nsi"
"template": "./packages/windows/installer.nsi"
}
}
},

View File

@ -16,7 +16,7 @@
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"installMode": "perMachine",
"template": "./template/installer.nsi"
"template": "./packages/windows/installer.nsi"
}
}
},

View File

@ -1,3 +1,7 @@
@use "./layout.scss";
@use "./page.scss";
@use "./font.scss";
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
@ -43,10 +47,6 @@ body {
overflow: hidden;
}
@import "./layout.scss";
@import "./page.scss";
@import "./font.scss";
// @media (prefers-color-scheme: dark) {
// :root {
// background-color: rgba(18, 18, 18, 1);

View File

@ -10,7 +10,7 @@ type Props = {
};
export const BaseFieldset: React.FC<Props> = (props: Props) => {
const Fieldset = styled(Box)(() => ({
const Fieldset = styled(Box)<{ component?: string }>(() => ({
position: "relative",
border: "1px solid #bbb",
borderRadius: "5px",

View File

@ -0,0 +1,33 @@
import React from "react";
import { Box, CircularProgress } from "@mui/material";
export interface BaseLoadingOverlayProps {
isLoading: boolean;
}
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
isLoading,
}) => {
if (!isLoading) return null;
return (
<Box
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(255, 255, 255, 0.7)",
zIndex: 1000,
}}
>
<CircularProgress />
</Box>
);
};
export default BaseLoadingOverlay;

View File

@ -1,7 +1,7 @@
import React, { ReactNode } from "react";
import { Typography } from "@mui/material";
import { BaseErrorBoundary } from "./base-error-boundary";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { useTheme } from "@mui/material/styles";
interface Props {
title?: React.ReactNode; // the page title
@ -13,7 +13,7 @@ interface Props {
export const BasePage: React.FC<Props> = (props) => {
const { title, header, contentStyle, full, children } = props;
const { theme } = useCustomTheme();
const theme = useTheme();
const isDark = theme.palette.mode === "dark";

View File

@ -7,20 +7,19 @@ import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
export type SearchState = {
text: string;
matchCase: boolean;
matchWholeWord: boolean;
useRegularExpression: boolean;
};
type SearchProps = {
placeholder?: string;
matchCase?: boolean;
matchWholeWord?: boolean;
useRegularExpression?: boolean;
onSearch: (
match: (content: string) => boolean,
state: {
text: string;
matchCase: boolean;
matchWholeWord: boolean;
useRegularExpression: boolean;
}
) => void;
onSearch: (match: (content: string) => boolean, state: SearchState) => void;
};
export const BaseSearchBox = styled((props: SearchProps) => {
@ -28,10 +27,10 @@ export const BaseSearchBox = styled((props: SearchProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
const [matchWholeWord, setMatchWholeWord] = useState(
props.matchWholeWord ?? false
props.matchWholeWord ?? false,
);
const [useRegularExpression, setUseRegularExpression] = useState(
props.useRegularExpression ?? false
props.useRegularExpression ?? false,
);
const [errorMessage, setErrorMessage] = useState("");
@ -60,7 +59,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
matchCase,
matchWholeWord,
useRegularExpression,
}
},
);
};

View File

@ -5,3 +5,4 @@ export { BaseLoading } from "./base-loading";
export { BaseErrorBoundary } from "./base-error-boundary";
export { Notice } from "./base-notice";
export { Switch } from "./base-switch";
export { BaseLoadingOverlay } from "./base-loading-overlay";

View File

@ -30,6 +30,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
open={open}
onClose={onClose}
sx={{ maxWidth: "520px" }}
message={
detail ? (
<InnerConnectionDetail data={detail} onClose={onClose} />
@ -37,7 +38,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
}
/>
);
}
},
);
interface InnerProps {

View File

@ -8,7 +8,6 @@ import {
import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
import { useLogData } from "@/hooks/use-log-data";
import { useVisibility } from "@/hooks/use-visibility";
import parseTraffic from "@/utils/parse-traffic";
import useSWRSubscription from "swr/subscription";
@ -39,11 +38,6 @@ export const LayoutTraffic = () => {
return () => {};
}, [isDebug]);
// https://swr.vercel.app/docs/subscription#deduplication
// useSWRSubscription auto deduplicates to one subscription per key per entire app
// So we can simply invoke it here acting as preconnect
useLogData();
const { data: traffic = { up: 0, down: 0 } } = useSWRSubscription<
ITrafficItem,
any,
@ -65,7 +59,7 @@ export const LayoutTraffic = () => {
this.close();
next(event, { up: 0, down: 0 });
},
}
},
);
return () => {
@ -75,7 +69,7 @@ export const LayoutTraffic = () => {
{
fallbackData: { up: 0, down: 0 },
keepPreviousData: true,
}
},
);
/* --------- meta memory information --------- */
@ -102,7 +96,7 @@ export const LayoutTraffic = () => {
this.close();
next(event, { inuse: 0 });
},
}
},
);
return () => {
@ -112,7 +106,7 @@ export const LayoutTraffic = () => {
{
fallbackData: { inuse: 0 },
keepPreviousData: true,
}
},
);
const [up, upUnit] = parseTraffic(traffic.up);

View File

@ -0,0 +1,35 @@
import { IconButton, Fade } from "@mui/material";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
interface Props {
onClick: () => void;
show: boolean;
}
export const ScrollTopButton = ({ onClick, show }: Props) => {
return (
<Fade in={show}>
<IconButton
onClick={onClick}
sx={{
position: "absolute",
bottom: "20px",
right: "20px",
backgroundColor: (theme) =>
theme.palette.mode === "dark"
? "rgba(255,255,255,0.1)"
: "rgba(0,0,0,0.1)",
"&:hover": {
backgroundColor: (theme) =>
theme.palette.mode === "dark"
? "rgba(255,255,255,0.2)"
: "rgba(0,0,0,0.2)",
},
visibility: show ? "visible" : "hidden",
}}
>
<KeyboardArrowUpIcon />
</IconButton>
</Fade>
);
};

View File

@ -4,6 +4,7 @@ import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { useSetThemeMode, useThemeMode } from "@/services/states";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { useVerge } from "@/hooks/use-verge";
import { useTheme } from "@mui/material/styles";
const appWindow = getCurrentWebviewWindow();
/**
@ -103,7 +104,7 @@ export const useCustomTheme = () => {
rootEle.style.setProperty("--primary-main", theme.palette.primary.main);
rootEle.style.setProperty(
"--background-color-alpha",
alpha(theme.palette.primary.main, 0.1)
alpha(theme.palette.primary.main, 0.1),
);
// inject css

View File

@ -1,4 +1,5 @@
import { styled, Box } from "@mui/material";
import { SearchState } from "@/components/base/base-search-box";
const Item = styled(Box)(({ theme: { palette, typography } }) => ({
padding: "8px 0",
@ -32,14 +33,54 @@ const Item = styled(Box)(({ theme: { palette, typography } }) => ({
color: palette.text.primary,
overflowWrap: "anywhere",
},
"& .highlight": {
backgroundColor: palette.mode === "dark" ? "#ffeb3b40" : "#ffeb3b90",
borderRadius: 2,
padding: "0 2px",
},
}));
interface Props {
value: ILogItem;
searchState?: SearchState;
}
const LogItem = (props: Props) => {
const { value } = props;
const LogItem = ({ value, searchState }: Props) => {
const renderHighlightText = (text: string) => {
if (!searchState?.text.trim()) return text;
try {
const searchText = searchState.text;
let pattern: string;
if (searchState.useRegularExpression) {
try {
new RegExp(searchText);
pattern = searchText;
} catch {
pattern = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
} else {
const escaped = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
pattern = searchState.matchWholeWord ? `\\b${escaped}\\b` : escaped;
}
const flags = searchState.matchCase ? "g" : "gi";
const parts = text.split(new RegExp(`(${pattern})`, flags));
return parts.map((part, index) => {
return index % 2 === 1 ? (
<span key={index} className="highlight">
{part}
</span>
) : (
part
);
});
} catch {
return text;
}
};
return (
<Item>
@ -50,7 +91,7 @@ const LogItem = (props: Props) => {
</span>
</div>
<div>
<span className="data">{value.payload}</span>
<span className="data">{renderHighlightText(value.payload)}</span>
</div>
</Item>
);

View File

@ -34,7 +34,7 @@ export const ProviderButton = () => {
const hasProvider = Object.keys(data || {}).length > 0;
const [updating, setUpdating] = useState(
Object.keys(data || {}).map(() => false)
Object.keys(data || {}).map(() => false),
);
const setUpdatingAt = (status: boolean, index: number) => {
@ -107,7 +107,7 @@ export const ProviderButton = () => {
const expire = sub?.Expire || 0;
const progress = Math.min(
Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
100
100,
);
return (
<>
@ -190,7 +190,7 @@ export const ProviderButton = () => {
</>
);
};
const TypeBox = styled(Box)(({ theme }) => ({
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.secondary.main, 0.5),
@ -202,7 +202,8 @@ const TypeBox = styled(Box)(({ theme }) => ({
lineHeight: 1.25,
}));
const StyledTypeBox = styled(Box)(({ theme }) => ({
const StyledTypeBox = styled(Box)<{ component?: React.ElementType }>(
({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.primary.main, 0.5),
@ -212,7 +213,8 @@ const StyledTypeBox = styled(Box)(({ theme }) => ({
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));
}),
);
const boxStyle = {
height: 26,

View File

@ -1,4 +1,4 @@
import { useRef } from "react";
import { useRef, useState, useEffect } from "react";
import { useLockFn } from "ahooks";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import {
@ -15,6 +15,7 @@ import { useRenderList } from "./use-render-list";
import { ProxyRender } from "./proxy-render";
import delayManager from "@/services/delay";
import { useTranslation } from "react-i18next";
import { ScrollTopButton } from "../layout/scroll-top-button";
interface Props {
mode: string;
@ -32,6 +33,22 @@ export const ProxyGroups = (props: Props) => {
const virtuosoRef = useRef<VirtuosoHandle>(null);
const [showScrollTop, setShowScrollTop] = useState(false);
// 添加滚动处理函数
const handleScroll = (e: any) => {
const scrollTop = e.target.scrollTop;
setShowScrollTop(scrollTop > 100);
};
// 滚动到顶部
const scrollToTop = () => {
virtuosoRef.current?.scrollTo?.({
top: 0,
behavior: "smooth",
});
};
// 切换分组的节点代理
const handleChangeProxy = useLockFn(
async (group: IProxyGroupItem, proxy: IProxyItem) => {
@ -57,7 +74,7 @@ export const ProxyGroups = (props: Props) => {
if (!current.selected) current.selected = [];
const index = current.selected.findIndex(
(item) => item.name === group.name
(item) => item.name === group.name,
);
if (index < 0) {
@ -66,14 +83,14 @@ export const ProxyGroups = (props: Props) => {
current.selected[index] = { name, now: proxy.name };
}
await patchCurrent({ selected: current.selected });
}
},
);
// 测全部延迟
const handleCheckAll = useLockFn(async (groupName: string) => {
const proxies = renderList
.filter(
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4)
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4),
)
.flatMap((e) => e.proxyCol || e.proxy!)
.filter(Boolean);
@ -82,7 +99,7 @@ export const ProxyGroups = (props: Props) => {
if (providers.size) {
Promise.allSettled(
[...providers].map((p) => providerHealthCheck(p))
[...providers].map((p) => providerHealthCheck(p)),
).then(() => onProxies());
}
@ -105,7 +122,7 @@ export const ProxyGroups = (props: Props) => {
(e) =>
e.group?.name === name &&
((e.type === 2 && e.proxy?.name === now) ||
(e.type === 4 && e.proxyCol?.some((p) => p.name === now)))
(e.type === 4 && e.proxyCol?.some((p) => p.name === now))),
);
if (index >= 0) {
@ -122,12 +139,19 @@ export const ProxyGroups = (props: Props) => {
}
return (
<div style={{ position: "relative", height: "100%" }}>
<Virtuoso
ref={virtuosoRef}
style={{ height: "calc(100% - 16px)" }}
totalCount={renderList.length}
increaseViewportBy={256}
scrollerRef={(ref) => {
if (ref) {
ref.addEventListener("scroll", handleScroll);
}
}}
itemContent={(index) => (
<>
<ProxyRender
key={renderList[index].key}
item={renderList[index]}
@ -137,7 +161,11 @@ export const ProxyGroups = (props: Props) => {
onHeadState={onHeadState}
onChangeProxy={handleChangeProxy}
/>
</>
)}
/>
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
</div>
);
};

View File

@ -160,6 +160,16 @@ export const ProxyItemMini = (props: Props) => {
TFO
</TypeBox>
)}
{proxy.mptcp && (
<TypeBox color="text.secondary" component="span">
MPTCP
</TypeBox>
)}
{proxy.smux && (
<TypeBox color="text.secondary" component="span">
SMUX
</TypeBox>
)}
</Box>
)}
</Box>
@ -239,7 +249,9 @@ const Widget = styled(Box)(({ theme: { typography } }) => ({
borderRadius: "4px",
}));
const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({
const TypeBox = styled(Box, {
shouldForwardProp: (prop) => prop !== "component",
})<{ component?: React.ElementType }>(({ theme: { palette, typography } }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: "text.secondary",

View File

@ -31,7 +31,7 @@ const Widget = styled(Box)(() => ({
borderRadius: "4px",
}));
const TypeBox = styled(Box)(({ theme }) => ({
const TypeBox = styled("span")(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.text.secondary, 0.36),
@ -121,14 +121,14 @@ export const ProxyItem = (props: Props) => {
{showType && proxy.now && ` - ${proxy.now}`}
</Box>
{showType && !!proxy.provider && (
<TypeBox component="span">{proxy.provider}</TypeBox>
<TypeBox>{proxy.provider}</TypeBox>
)}
{showType && <TypeBox component="span">{proxy.type}</TypeBox>}
{showType && proxy.udp && <TypeBox component="span">UDP</TypeBox>}
{showType && proxy.xudp && (
<TypeBox component="span">XUDP</TypeBox>
)}
{showType && proxy.tfo && <TypeBox component="span">TFO</TypeBox>}
{showType && <TypeBox>{proxy.type}</TypeBox>}
{showType && proxy.udp && <TypeBox>UDP</TypeBox>}
{showType && proxy.xudp && <TypeBox>XUDP</TypeBox>}
{showType && proxy.tfo && <TypeBox>TFO</TypeBox>}
{showType && proxy.mptcp && <TypeBox>MPTCP</TypeBox>}
{showType && proxy.smux && <TypeBox>SMUX</TypeBox>}
</>
}
/>

View File

@ -59,7 +59,7 @@ export const ProxyRender = (props: RenderProps) => {
return url.substring(url.lastIndexOf("/") + 1);
}
if (type === 0 && !group.hidden) {
if (type === 0) {
return (
<ListItemButton
dense
@ -125,7 +125,7 @@ export const ProxyRender = (props: RenderProps) => {
);
}
if (type === 1 && !group.hidden) {
if (type === 1) {
return (
<ProxyHead
sx={{ pl: 2, pr: 3, mt: indent ? 1 : 0.5, mb: 1 }}
@ -139,7 +139,7 @@ export const ProxyRender = (props: RenderProps) => {
);
}
if (type === 2 && !group.hidden) {
if (type === 2) {
return (
<ProxyItem
group={group}
@ -152,7 +152,7 @@ export const ProxyRender = (props: RenderProps) => {
);
}
if (type === 3 && !group.hidden) {
if (type === 3) {
return (
<Box
sx={{
@ -170,7 +170,7 @@ export const ProxyRender = (props: RenderProps) => {
);
}
if (type === 4 && !group.hidden) {
if (type === 4) {
const proxyColItemsMemo = useMemo(() => {
return proxyCol?.map((proxy) => (
<ProxyItemMini

View File

@ -25,7 +25,7 @@ export const useRenderList = (mode: string) => {
const { data: proxiesData, mutate: mutateProxies } = useSWR(
"getProxies",
getProxies,
{ refreshInterval: 45000 }
{ refreshInterval: 45000 },
);
const { verge } = useVerge();
@ -78,7 +78,7 @@ export const useRenderList = (mode: string) => {
group.all,
group.name,
headState.filterText,
headState.sortType
headState.sortType,
);
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
@ -97,7 +97,7 @@ export const useRenderList = (mode: string) => {
headState,
col,
proxyCol,
}))
})),
);
}
@ -108,14 +108,14 @@ export const useRenderList = (mode: string) => {
group,
proxy,
headState,
}))
})),
);
}
return ret;
});
if (!useRule) return retList.slice(1);
return retList;
return retList.filter((item) => item.group.hidden === false);
}, [headStates, proxiesData, mode, col]);
return {

View File

@ -32,7 +32,7 @@ export const ProviderButton = () => {
const hasProvider = Object.keys(data || {}).length > 0;
const [updating, setUpdating] = useState(
Object.keys(data || {}).map(() => false)
Object.keys(data || {}).map(() => false),
);
const setUpdatingAt = (status: boolean, index: number) => {
@ -162,7 +162,9 @@ export const ProviderButton = () => {
</>
);
};
const TypeBox = styled(Box)(({ theme }) => ({
const TypeBox = styled(Box, {
shouldForwardProp: (prop) => prop !== "component",
})<{ component?: React.ElementType }>(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.secondary.main, 0.5),
@ -174,7 +176,9 @@ const TypeBox = styled(Box)(({ theme }) => ({
lineHeight: 1.25,
}));
const StyledTypeBox = styled(Box)(({ theme }) => ({
const StyledTypeBox = styled(Box, {
shouldForwardProp: (prop) => prop !== "component",
})<{ component?: React.ElementType }>(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.primary.main, 0.5),

View File

@ -0,0 +1,245 @@
import { useState, useRef, memo, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { useVerge } from "@/hooks/use-verge";
import { Notice } from "@/components/base";
import { isValidUrl } from "@/utils/helper";
import { useLockFn } from "ahooks";
import {
TextField,
Button,
Grid2,
Box,
Stack,
IconButton,
InputAdornment,
} from "@mui/material";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds";
export interface BackupConfigViewerProps {
onBackupSuccess: () => Promise<void>;
onSaveSuccess: () => Promise<void>;
onRefresh: () => Promise<void>;
onInit: () => Promise<void>;
setLoading: (loading: boolean) => void;
}
export const BackupConfigViewer = memo(
({
onBackupSuccess,
onSaveSuccess,
onRefresh,
onInit,
setLoading,
}: BackupConfigViewerProps) => {
const { t } = useTranslation();
const { verge } = useVerge();
const { webdav_url, webdav_username, webdav_password } = verge || {};
const [showPassword, setShowPassword] = useState(false);
const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const urlRef = useRef<HTMLInputElement>(null);
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
defaultValues: {
url: webdav_url,
username: webdav_username,
password: webdav_password,
},
});
const url = watch("url");
const username = watch("username");
const password = watch("password");
const webdavChanged =
webdav_url !== url ||
webdav_username !== username ||
webdav_password !== password;
console.log(
"webdavChanged",
webdavChanged,
webdav_url,
webdav_username,
webdav_password,
);
const handleClickShowPassword = () => {
setShowPassword((prev) => !prev);
};
useEffect(() => {
if (webdav_url && webdav_username && webdav_password) {
onInit();
}
}, []);
const checkForm = () => {
const username = usernameRef.current?.value;
const password = passwordRef.current?.value;
const url = urlRef.current?.value;
if (!url) {
urlRef.current?.focus();
Notice.error(t("WebDAV URL Required"));
throw new Error(t("WebDAV URL Required"));
} else if (!isValidUrl(url)) {
urlRef.current?.focus();
Notice.error(t("Invalid WebDAV URL"));
throw new Error(t("Invalid WebDAV URL"));
}
if (!username) {
usernameRef.current?.focus();
Notice.error(t("WebDAV URL Required"));
throw new Error(t("Username Required"));
}
if (!password) {
passwordRef.current?.focus();
Notice.error(t("WebDAV URL Required"));
throw new Error(t("Password Required"));
}
};
const save = useLockFn(async (data: IWebDavConfig) => {
checkForm();
try {
setLoading(true);
await saveWebdavConfig(
data.url.trim(),
data.username.trim(),
data.password,
).then(() => {
Notice.success(t("WebDAV Config Saved"));
onSaveSuccess();
});
} catch (error) {
Notice.error(t("WebDAV Config Save Failed", { error }), 3000);
} finally {
setLoading(false);
}
});
const handleBackup = useLockFn(async () => {
checkForm();
try {
setLoading(true);
await createWebdavBackup().then(async () => {
await onBackupSuccess();
Notice.success(t("Backup Created"));
});
} catch (error) {
Notice.error(t("Backup Failed", { error }));
} finally {
setLoading(false);
}
});
return (
<form onSubmit={(e) => e.preventDefault()}>
<Grid2 container spacing={2}>
<Grid2 size={{ xs: 12, sm: 9 }}>
<Grid2 container spacing={2}>
<Grid2 size={{ xs: 12 }}>
<TextField
fullWidth
label={t("WebDAV Server URL")}
variant="outlined"
size="small"
{...register("url")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={urlRef}
/>
</Grid2>
<Grid2 size={{ xs: 6 }}>
<TextField
label={t("Username")}
variant="outlined"
size="small"
{...register("username")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={usernameRef}
/>
</Grid2>
<Grid2 size={{ xs: 6 }}>
<TextField
label={t("Password")}
type={showPassword ? "text" : "password"}
variant="outlined"
size="small"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={passwordRef}
{...register("password")}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
},
}}
/>
</Grid2>
</Grid2>
</Grid2>
<Grid2 size={{ xs: 12, sm: 3 }}>
<Stack
direction="column"
justifyContent="space-between"
alignItems="stretch"
sx={{ height: "100%" }}
>
{webdavChanged ||
webdav_url === undefined ||
webdav_username === undefined ||
webdav_password === undefined ? (
<Button
variant="contained"
color={"primary"}
sx={{ height: "100%" }}
type="button"
onClick={handleSubmit(save)}
>
{t("Save")}
</Button>
) : (
<>
<Button
variant="contained"
color="success"
onClick={handleBackup}
type="button"
size="large"
>
{t("Backup")}
</Button>
<Button
variant="outlined"
onClick={onRefresh}
type="button"
size="large"
>
{t("Refresh")}
</Button>
</>
)}
</Stack>
</Grid2>
</Grid2>
</form>
);
},
);

View File

@ -0,0 +1,266 @@
import { SVGProps, memo } from "react";
import {
Box,
Paper,
IconButton,
Divider,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TablePagination,
} from "@mui/material";
import { Notice } from "@/components/base";
import { Typography } from "@mui/material";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { Dayjs } from "dayjs";
import {
deleteWebdavBackup,
restoreWebDavBackup,
restartApp,
} from "@/services/cmds";
import DeleteIcon from "@mui/icons-material/Delete";
import RestoreIcon from "@mui/icons-material/Restore";
export type BackupFile = IWebDavFile & {
platform: string;
backup_time: Dayjs;
allow_apply: boolean;
};
export const DEFAULT_ROWS_PER_PAGE = 5;
export interface BackupTableViewerProps {
datasource: BackupFile[];
page: number;
onPageChange: (
event: React.MouseEvent<HTMLButtonElement> | null,
page: number,
) => void;
total: number;
onRefresh: () => Promise<void>;
}
export const BackupTableViewer = memo(
({
datasource,
page,
onPageChange,
total,
onRefresh,
}: BackupTableViewerProps) => {
const { t } = useTranslation();
const handleDelete = useLockFn(async (filename: string) => {
await deleteWebdavBackup(filename);
await onRefresh();
});
const handleRestore = useLockFn(async (filename: string) => {
await restoreWebDavBackup(filename).then(() => {
Notice.success(t("Restore Success, App will restart in 1s"));
});
await restartApp();
});
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Filename")}</TableCell>
<TableCell>{t("Backup Time")}</TableCell>
<TableCell align="right">{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{datasource.length > 0 ? (
datasource?.map((file, index) => (
<TableRow key={index}>
<TableCell component="th" scope="row">
{file.platform === "windows" ? (
<WindowsIcon className="h-full w-full" />
) : file.platform === "linux" ? (
<LinuxIcon className="h-full w-full" />
) : (
<MacIcon className="h-full w-full" />
)}
{file.filename}
</TableCell>
<TableCell align="center">
{file.backup_time.fromNow()}
</TableCell>
<TableCell align="right">
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<IconButton
color="secondary"
aria-label={t("Delete")}
size="small"
title={t("Delete Backup")}
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await window.confirm(
t("Confirm to delete this backup file?"),
);
if (confirmed) {
await handleDelete(file.filename);
}
}}
>
<DeleteIcon />
</IconButton>
<Divider
orientation="vertical"
flexItem
sx={{ mx: 1, height: 24 }}
/>
<IconButton
color="primary"
aria-label={t("Restore")}
size="small"
title={t("Restore Backup")}
disabled={!file.allow_apply}
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await window.confirm(
t("Confirm to restore this backup file?"),
);
if (confirmed) {
await handleRestore(file.filename);
}
}}
>
<RestoreIcon />
</IconButton>
</Box>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={3} align="center">
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: 150,
}}
>
<Typography
variant="body1"
color="textSecondary"
align="center"
>
{t("No Backups")}
</Typography>
</Box>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[]}
component="div"
count={total}
rowsPerPage={DEFAULT_ROWS_PER_PAGE}
page={page}
onPageChange={onPageChange}
labelRowsPerPage={t("Rows per page")}
/>
</TableContainer>
);
},
);
function LinuxIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 48 48"
{...props}
>
<path
fill="#ECEFF1"
d="m20.1 16.2l.1 2.3l-1.6 3l-2.5 4.9l-.5 4.1l1.8 5.8l4.1 2.3h6.2l5.8-4.4l2.6-6.9l-6-7.3l-1.7-4.1z"
/>
<path
fill="#263238"
d="M34.3 21.9c-1.6-2.3-2.9-3.7-3.6-6.6s.2-2.1-.4-4.6c-.3-1.3-.8-2.2-1.3-2.9c-.6-.7-1.3-1.1-1.7-1.2c-.9-.5-3-1.3-5.6.1c-2.7 1.4-2.4 4.4-1.9 10.5c0 .4-.1.9-.3 1.3c-.4.9-1.1 1.7-1.7 2.4c-.7 1-1.4 2-1.9 3.1c-1.2 2.3-2.3 5.2-2 6.3c.5-.1 6.8 9.5 6.8 9.7c.4-.1 2.1-.1 3.6-.1c2.1-.1 3.3-.2 5 .2c0-.3-.1-.6-.1-.9c0-.6.1-1.1.2-1.8c.1-.5.2-1 .3-1.6c-1 .9-2.8 1.9-4.5 2.2c-1.5.3-4-.2-5.2-1.7c.1 0 .3 0 .4-.1c.3-.1.6-.2.7-.4c.3-.5.1-1-.1-1.3s-1.7-1.4-2.4-2s-1.1-.9-1.5-1.3l-.8-.8c-.2-.2-.3-.4-.4-.5c-.2-.5-.3-1.1-.2-1.9c.1-1.1.5-2 1-3c.2-.4.7-1.2.7-1.2s-1.7 4.2-.8 5.5c0 0 .1-1.3.5-2.6c.3-.9.8-2.2 1.4-2.9s2.1-3.3 2.2-4.9c0-.7.1-1.4.1-1.9c-.4-.4 6.6-1.4 7-.3c.1.4 1.5 4 2.3 5.9c.4.9.9 1.7 1.2 2.7c.3 1.1.5 2.6.5 4.1c0 .3 0 .8-.1 1.3c.2 0 4.1-4.2-.5-7.7c0 0 2.8 1.3 2.9 3.9c.1 2.1-.8 3.8-1 4.1c.1 0 2.1.9 2.2.9c.4 0 1.2-.3 1.2-.3c.1-.3.4-1.1.4-1.4c.7-2.3-1-6-2.6-8.3"
/>
<g fill="#ECEFF1" transform="translate(0 -2)">
<ellipse cx="21.6" cy="15.3" rx="1.3" ry="2" />
<ellipse cx="26.1" cy="15.2" rx="1.7" ry="2.3" />
</g>
<g fill="#212121" transform="translate(0 -2)">
<ellipse
cx="21.7"
cy="15.5"
rx="1.2"
ry=".7"
transform="rotate(-97.204 21.677 15.542)"
/>
<ellipse cx="26" cy="15.6" rx="1" ry="1.3" />
</g>
<path
fill="#FFC107"
d="M39.3 35.6c-.4-.2-1.1-.5-1.7-1.4c-.3-.5-.2-1.9-.7-2.5c-.3-.4-.7-.2-.8-.2c-.9.2-3 1.6-4.4 0c-.2-.2-.5-.5-1-.5s-.7.2-.9.6s-.2.7-.2 1.7c0 .8 0 1.7-.1 2.4c-.2 1.7-.5 2.7-.5 3.7c0 1.1.3 1.8.7 2.1c.3.3.8.5 1.9.5s1.8-.4 2.5-1.1c.5-.5.9-.7 2.3-1.7c1.1-.7 2.8-1.6 3.1-1.9c.2-.2.5-.3.5-.9c0-.5-.4-.7-.7-.8m-20.1.3c-1-1.6-1.1-1.9-1.8-2.9c-.6-1-1.9-2.9-2.7-2.9c-.6 0-.9.3-1.3.7s-.8 1.3-1.5 1.8c-.6.5-2.3.4-2.7 1s.4 1.5.4 3c0 .6-.5 1-.6 1.4c-.1.5-.2.8 0 1.2c.4.6.9.8 4.3 1.5c1.8.4 3.5 1.4 4.6 1.5s3 0 3-2.7c.1-1.6-.8-2-1.7-3.6m1.9-18.1c-.6-.4-1.1-.8-1.1-1.4s.4-.8 1-1.3c.1-.1 1.2-1.1 2.3-1.1s2.4.7 2.9.9c.9.2 1.8.4 1.7 1.1c-.1 1-.2 1.2-1.2 1.7c-.7.2-2 1.3-2.9 1.3c-.4 0-1 0-1.4-.1c-.3-.1-.8-.6-1.3-1.1"
/>
<path
fill="#634703"
d="M20.9 17c.2.2.5.4.8.5c.2.1.5.2.5.2h.9c.5 0 1.2-.2 1.9-.6c.7-.3.8-.5 1.3-.7c.5-.3 1-.6.8-.7s-.4 0-1.1.4c-.6.4-1.1.6-1.7.9c-.3.1-.7.3-1 .3h-.9c-.3 0-.5-.1-.8-.2c-.2-.1-.3-.2-.4-.2c-.2-.1-.6-.5-.8-.6c0 0-.2 0-.1.1zm3-2.2c.1.2.3.2.4.3s.2.1.2.1c.1-.1 0-.3-.1-.3c0-.2-.5-.2-.5-.1m-1.6.2c0 .1.2.2.2.1c.1-.1.2-.2.3-.2c.2-.1.1-.2-.2-.2c-.2.1-.2.2-.3.3"
/>
<path
fill="#455A64"
d="M32 32.7v.3c.2.4.7.5 1.1.5c.6 0 1.2-.4 1.5-.8c0-.1.1-.2.2-.3c.2-.3.3-.5.4-.6c0 0-.1-.1-.1-.2c-.1-.2-.4-.4-.8-.5c-.3-.1-.8-.2-1-.2c-.9-.1-1.4.2-1.7.5c0 0 .1 0 .1.1c.2.2.3.4.3.7c.1.2 0 .3 0 .5"
/>
</svg>
);
}
function WindowsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 16 16"
{...props}
>
<path
fill="#0284c7"
d="M6.555 1.375L0 2.237v5.45h6.555zM0 13.795l6.555.933V8.313H0zm7.278-5.4l.026 6.378L16 16V8.395zM16 0L7.33 1.244v6.414H16z"
/>
</svg>
);
}
function MacIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 26 26"
{...props}
>
<path
fill="#000"
d="M23.934 18.947c-.598 1.324-.884 1.916-1.652 3.086c-1.073 1.634-2.588 3.673-4.461 3.687c-1.666.014-2.096-1.087-4.357-1.069c-2.261.011-2.732 1.089-4.4 1.072c-1.873-.017-3.307-1.854-4.381-3.485c-3.003-4.575-3.32-9.937-1.464-12.79C4.532 7.425 6.61 6.237 8.561 6.237c1.987 0 3.236 1.092 4.879 1.092c1.594 0 2.565-1.095 4.863-1.095c1.738 0 3.576.947 4.889 2.581c-4.296 2.354-3.598 8.49.742 10.132M16.559 4.408c.836-1.073 1.47-2.587 1.24-4.131c-1.364.093-2.959.964-3.891 2.092c-.844 1.027-1.544 2.553-1.271 4.029c1.488.048 3.028-.839 3.922-1.99"
/>
</svg>
);
}

View File

@ -0,0 +1,144 @@
import {
forwardRef,
useImperativeHandle,
useState,
useCallback,
useEffect,
} from "react";
import { useTranslation } from "react-i18next";
import { BaseDialog, DialogRef } from "@/components/base";
import getSystem from "@/utils/get-system";
import { BaseLoadingOverlay } from "@/components/base";
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import {
BackupTableViewer,
BackupFile,
DEFAULT_ROWS_PER_PAGE,
} from "./backup-table-viewer";
import { BackupConfigViewer } from "./backup-config-viewer";
import { Box, Paper, Divider } from "@mui/material";
import { listWebDavBackup } from "@/services/cmds";
dayjs.extend(customParseFormat);
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
const [dataSource, setDataSource] = useState<BackupFile[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(0);
const OS = getSystem();
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
},
close: () => setOpen(false),
}));
// Handle page change
const handleChangePage = useCallback(
(_: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
setPage(page);
},
[],
);
const fetchAndSetBackupFiles = async () => {
try {
setIsLoading(true);
const files = await getAllBackupFiles();
setBackupFiles(files);
setTotal(files.length);
} catch (error) {
setBackupFiles([]);
setTotal(0);
console.error(error);
// Notice.error(t("Failed to fetch backup files"));
} finally {
setIsLoading(false);
}
};
const getAllBackupFiles = async () => {
const files = await listWebDavBackup();
return files
.map((file) => {
const platform = file.filename.split("-")[0];
const fileBackupTimeStr = file.filename.match(FILENAME_PATTERN)!;
if (fileBackupTimeStr === null) {
return null;
}
const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT);
const allowApply = OS === platform;
return {
...file,
platform,
backup_time: backupTime,
allow_apply: allowApply,
} as BackupFile;
})
.filter((item) => item !== null)
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
};
useEffect(() => {
setDataSource(
backupFiles.slice(
page * DEFAULT_ROWS_PER_PAGE,
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE,
),
);
}, [page, backupFiles]);
return (
<BaseDialog
open={open}
title={t("Backup Setting")}
contentSx={{ width: 600, maxHeight: 800 }}
okBtn={t("")}
cancelBtn={t("Close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
disableOk
>
<Box sx={{ maxWidth: 800 }}>
<BaseLoadingOverlay isLoading={isLoading} />
<Paper elevation={2} sx={{ padding: 2 }}>
<BackupConfigViewer
setLoading={setIsLoading}
onBackupSuccess={async () => {
fetchAndSetBackupFiles();
}}
onSaveSuccess={async () => {
fetchAndSetBackupFiles();
}}
onRefresh={async () => {
fetchAndSetBackupFiles();
}}
onInit={async () => {
fetchAndSetBackupFiles();
}}
/>
<Divider sx={{ marginY: 2 }} />
<BackupTableViewer
datasource={dataSource}
page={page}
onPageChange={handleChangePage}
total={total}
onRefresh={fetchAndSetBackupFiles}
/>
</Paper>
</Box>
</BaseDialog>
);
});

View File

@ -17,7 +17,7 @@ import {
ListItemButton,
ListItemText,
} from "@mui/material";
import { changeClashCore, restartSidecar } from "@/services/cmds";
import { changeClashCore, restartCore } from "@/services/cmds";
import { closeAllConnections, upgradeCore } from "@/services/api";
const VALID_CORE = [
@ -59,7 +59,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const onRestart = useLockFn(async () => {
try {
await restartSidecar();
await restartCore();
Notice.success(t(`Clash Core Restarted`), 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());

View File

@ -41,7 +41,6 @@ export function GuardState<T>(props: Props<T>) {
childProps[onChangeProps] = async (...args: any[]) => {
// 多次操作无效
if (lockRef.current) return;
lockRef.current = true;
try {

View File

@ -196,8 +196,8 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
},
],
});
if (selected?.path.length) {
await copyIconFile(`${selected.path}`, "common");
if (selected) {
await copyIconFile(`${selected}`, "common");
await initIconPath();
onChangeData({ common_tray_icon: true });
patchVerge({ common_tray_icon: true });
@ -242,8 +242,8 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
},
],
});
if (selected?.path.length) {
await copyIconFile(`${selected.path}`, "sysproxy");
if (selected) {
await copyIconFile(`${selected}`, "sysproxy");
await initIconPath();
onChangeData({ sysproxy_tray_icon: true });
patchVerge({ sysproxy_tray_icon: true });
@ -281,13 +281,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
multiple: false,
filters: [
{
name: "Tray Icon Image",
name: "Tun Icon Image",
extensions: ["png", "ico"],
},
],
});
if (selected?.path.length) {
await copyIconFile(`${selected.path}`, "tun");
if (selected) {
await copyIconFile(`${selected}`, "tun");
await initIconPath();
onChangeData({ tun_tray_icon: true });
patchVerge({ tun_tray_icon: true });

View File

@ -205,7 +205,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
spellCheck="false"
sx={{ width: 250, marginLeft: "auto" }}
value={values.defaultLatencyTest}
placeholder="http://1.1.1.1"
placeholder="http://cp.cloudflare.com/generate_204"
onChange={(e) =>
setValues((v) => ({ ...v, defaultLatencyTest: e.target.value }))
}

View File

@ -1,134 +0,0 @@
import { KeyedMutator } from "swr";
import { useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { installService, uninstallService } from "@/services/cmds";
import { Notice } from "@/components/base";
import { LoadingButton } from "@mui/lab";
import { PasswordInput } from "./password-input";
import getSystem from "@/utils/get-system";
interface Props {
status: "active" | "installed" | "unknown" | "uninstall";
mutate: KeyedMutator<"active" | "installed" | "unknown" | "uninstall">;
patchVerge: (value: Partial<IVergeConfig>) => Promise<void>;
onChangeData: (patch: Partial<IVergeConfig>) => void;
}
export const ServiceSwitcher = (props: Props) => {
const { status, mutate, patchVerge, onChangeData } = props;
const isWindows = getSystem() === "windows";
const isActive = status === "active";
const isInstalled = status === "installed";
const isUninstall = status === "uninstall" || status === "unknown";
const { t } = useTranslation();
const [serviceLoading, setServiceLoading] = useState(false);
const [uninstallServiceLoaing, setUninstallServiceLoading] = useState(false);
const [openInstall, setOpenInstall] = useState(false);
const [openUninstall, setOpenUninstall] = useState(false);
async function install(passwd: string) {
try {
setOpenInstall(false);
await installService(passwd);
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Installed Successfully"));
setServiceLoading(false);
} catch (err: any) {
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.error(err.message || err.toString());
setServiceLoading(false);
}
}
async function uninstall(passwd: string) {
try {
setOpenUninstall(false);
await uninstallService(passwd);
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Uninstalled Successfully"));
setUninstallServiceLoading(false);
} catch (err: any) {
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.error(err.message || err.toString());
setUninstallServiceLoading(false);
}
}
const onInstallOrEnableService = useLockFn(async () => {
setServiceLoading(true);
if (isUninstall) {
// install service
if (isWindows) {
await install("");
} else {
setOpenInstall(true);
}
} else {
try {
// enable or disable service
await patchVerge({ enable_service_mode: !isActive });
onChangeData({ enable_service_mode: !isActive });
await mutate();
setTimeout(() => {
mutate();
}, 2000);
setServiceLoading(false);
} catch (err: any) {
await mutate();
Notice.error(err.message || err.toString());
setServiceLoading(false);
}
}
});
const onUninstallService = useLockFn(async () => {
setUninstallServiceLoading(true);
if (isWindows) {
await uninstall("");
} else {
setOpenUninstall(true);
}
});
return (
<>
{openInstall && <PasswordInput onConfirm={install} />}
{openUninstall && <PasswordInput onConfirm={uninstall} />}
<LoadingButton
size="small"
variant={isUninstall ? "outlined" : "contained"}
onClick={onInstallOrEnableService}
loading={serviceLoading}
>
{isActive ? t("Disable") : isInstalled ? t("Enable") : t("Install")}
</LoadingButton>
{isInstalled && (
<LoadingButton
size="small"
variant="outlined"
color="error"
sx={{ ml: 1 }}
onClick={onUninstallService}
loading={uninstallServiceLoaing}
>
{t("Uninstall")}
</LoadingButton>
)}
</>
);
};

View File

@ -28,7 +28,7 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
setTheme({ ...theme_setting } || {});
setTheme({ ...theme_setting });
},
close: () => setOpen(false),
}));

View File

@ -7,10 +7,11 @@ import { relaunch } from "@tauri-apps/plugin-process";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { useUpdateState, useSetUpdateState } from "@/services/states";
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
import { Event, UnlistenFn } from "@tauri-apps/api/event";
import { portableFlag } from "@/pages/_layout";
import { open as openUrl } from "@tauri-apps/plugin-shell";
import ReactMarkdown from "react-markdown";
import { useListen } from "@/hooks/use-listen";
let eventListener: UnlistenFn | null = null;
@ -21,6 +22,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const updateState = useUpdateState();
const setUpdateState = useSetUpdateState();
const { addListener } = useListen();
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
errorRetryCount: 2,
@ -66,7 +68,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
if (eventListener !== null) {
eventListener();
}
eventListener = await listen(
eventListener = await addListener(
"tauri://update-download-progress",
(e: Event<any>) => {
setTotal(e.payload.contentLength);
@ -74,7 +76,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
setDownloaded((a) => {
return a + e.payload.chunkLength;
});
}
},
);
try {
await updateInfo.install();
@ -98,7 +100,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
size="small"
onClick={() => {
openUrl(
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`,
);
}}
>

View File

@ -1,7 +1,6 @@
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { TextField, Select, MenuItem, Typography } from "@mui/material";
import {
SettingsRounded,
ShuffleRounded,
@ -34,7 +33,12 @@ const SettingClash = ({ onError }: Props) => {
const { clash, version, mutateClash, patchClash } = useClash();
const { verge, mutateVerge, patchVerge } = useVerge();
const { ipv6, "allow-lan": allowLan, "log-level": logLevel } = clash ?? {};
const {
ipv6,
"allow-lan": allowLan,
"log-level": logLevel,
"unified-delay": unifiedDelay,
} = clash ?? {};
const { enable_random_port = false, verge_mixed_port } = verge ?? {};
@ -106,10 +110,36 @@ const SettingClash = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem label={t("Log Level")}>
<SettingItem
label={t("Unified Delay")}
extra={
<TooltipIcon
title={t("Unified Delay Info")}
sx={{ opacity: "0.7" }}
/>
}
>
<GuardState
value={unifiedDelay ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ "unified-delay": e })}
onGuard={(e) => patchClash({ "unified-delay": e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem
label={t("Log Level")}
extra={
<TooltipIcon title={t("Log Level Info")} sx={{ opacity: "0.7" }} />
}
>
<GuardState
// clash premium 2022.08.26 值为warn
value={logLevel === "warn" ? "warning" : logLevel ?? "info"}
value={logLevel === "warn" ? "warning" : (logLevel ?? "info")}
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ "log-level": e })}
@ -135,7 +165,7 @@ const SettingClash = ({ onError }: Props) => {
onClick={() => {
Notice.success(
t("Restart Application to Apply Modifications"),
1000
1000,
);
onChangeVerge({ enable_random_port: !enable_random_port });
patchVerge({ enable_random_port: !enable_random_port });

View File

@ -2,12 +2,10 @@ import useSWR from "swr";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { SettingsRounded } from "@mui/icons-material";
import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import { DialogRef, Notice, Switch } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp";
import { GuardState } from "./mods/guard-state";
import { ServiceSwitcher } from "./mods/service-switcher";
import { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
@ -20,16 +18,6 @@ const SettingSystem = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, mutateVerge, patchVerge } = useVerge();
// service mode
const { data: serviceStatus, mutate: mutateServiceStatus } = useSWR(
"checkService",
checkService,
{
revalidateIfStale: false,
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
}
);
const sysproxyRef = useRef<DialogRef>(null);
const tunRef = useRef<DialogRef>(null);
@ -67,34 +55,15 @@ const SettingSystem = ({ onError }: Props) => {
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => {
if (serviceStatus !== "active") {
onChangeData({ enable_tun_mode: false });
} else {
onChangeData({ enable_tun_mode: e });
}
}}
onGuard={(e) => {
if (serviceStatus !== "active" && e) {
Notice.error(t("Please Enable Service Mode"));
return Promise.resolve();
} else {
return patchVerge({ enable_tun_mode: e });
}
}}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem label={t("Service Mode")}>
<ServiceSwitcher
status={serviceStatus ?? "unknown"}
mutate={mutateServiceStatus}
patchVerge={patchVerge}
onChangeData={onChangeData}
/>
</SettingItem>
<SettingItem
label={t("System Proxy")}
extra={
@ -134,7 +103,9 @@ const SettingSystem = ({ onError }: Props) => {
<SettingItem
label={t("Silent Start")}
extra={<TooltipIcon title={t("Silent Start Info")} />}
extra={
<TooltipIcon title={t("Silent Start Info")} sx={{ opacity: "0.7" }} />
}
>
<GuardState
value={enable_silent_start ?? false}

View File

@ -23,6 +23,7 @@ import { ThemeViewer } from "./mods/theme-viewer";
import { GuardState } from "./mods/guard-state";
import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer";
import { BackupViewer } from "./mods/backup-viewer";
import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
@ -52,6 +53,7 @@ const SettingVerge = ({ onError }: Props) => {
const themeRef = useRef<DialogRef>(null);
const layoutRef = useRef<DialogRef>(null);
const updateRef = useRef<DialogRef>(null);
const backupRef = useRef<DialogRef>(null);
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
@ -83,6 +85,7 @@ const SettingVerge = ({ onError }: Props) => {
<MiscViewer ref={miscRef} />
<LayoutViewer ref={layoutRef} />
<UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} />
<SettingItem label={t("Language")}>
<GuardState
@ -194,9 +197,9 @@ const SettingVerge = ({ onError }: Props) => {
},
],
});
if (selected?.path.length) {
onChangeData({ startup_script: `${selected.path}` });
patchVerge({ startup_script: `${selected.path}` });
if (selected) {
onChangeData({ startup_script: `${selected}` });
patchVerge({ startup_script: `${selected}` });
}
}}
>
@ -238,12 +241,23 @@ const SettingVerge = ({ onError }: Props) => {
label={t("Hotkey Setting")}
/>
<SettingItem
onClick={() => backupRef.current?.open()}
label={t("Backup Setting")}
extra={
<TooltipIcon
title={t("Backup Setting Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem
onClick={() => configRef.current?.open()}
label={t("Runtime Config")}
/>
<SettingItem onClick={openAppDir} label={t("Open App Dir")} />
<SettingItem onClick={openAppDir} label={t("Open Conf Dir")} />
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />

View File

@ -18,9 +18,9 @@ import { Notice } from "@/components/base";
import { TestBox } from "./test-box";
import delayManager from "@/services/delay";
import { cmdTestDelay, downloadIconCache } from "@/services/cmds";
import { listen, UnlistenFn } from "@tauri-apps/api/event";
import { UnlistenFn } from "@tauri-apps/api/event";
import { convertFileSrc } from "@tauri-apps/api/core";
import { useListen } from "@/hooks/use-listen";
interface Props {
id: string;
itemData: IVergeTestItem;
@ -47,6 +47,7 @@ export const TestItem = (props: Props) => {
const [delay, setDelay] = useState(-1);
const { uid, name, icon, url } = itemData;
const [iconCachePath, setIconCachePath] = useState("");
const { addListener } = useListen();
useEffect(() => {
initIconCachePath();
@ -91,7 +92,7 @@ export const TestItem = (props: Props) => {
const listenTsetEvent = async () => {
eventListener();
eventListener = await listen("verge://test-all", () => {
eventListener = await addListener("verge://test-all", () => {
onDelay();
});
};

Some files were not shown because too many files have changed in this diff Show More