Compare commits

...
145 Commits
Author SHA1 Message Date
MystiPanda f59be465ea Release 1.5.2 2024-02-21 00:04:11 +08:00
MystiPanda 27305317ce chore: Auto Update config.yml 2024-02-20 23:54:02 +08:00
MystiPanda 8378320e50 feat: Support Tun Config (#416) 2024-02-20 23:27:03 +08:00
MystiPanda 86a69952db chore: Update patch file 2024-02-20 18:39:36 +08:00
MystiPanda e212ebfdb9 fix: Allow program run with administrator to start at startup 2024-02-20 18:35:39 +08:00
MystiPanda f235125d48 chore: add tooltip for tun mode 2024-02-19 18:10:10 +08:00
MystiPanda 8efa815eb9 feat: Support custom delay timeout (#397) 2024-02-18 11:11:22 +08:00
MystiPanda e54d42576b build: Try restart windows service after install (#395) 2024-02-18 10:19:54 +08:00
MystiPanda 1e18539862 fix: Merge profile unexpect behavior 2024-02-17 15:58:52 +08:00
MystiPanda 673fdf4721 ci: Update Release Note 2024-02-15 20:05:00 +08:00
MystiPanda 57d6bfba51 ci: change tag 2024-02-15 19:45:37 +08:00
MystiPanda 8fa23aecda chore: update ci script 2024-02-15 19:14:35 +08:00
MystiPanda f9c10533e8 chore: Change CI Name 2024-02-15 18:45:13 +08:00
MystiPanda c891c3723e ci: Support alpha package 2024-02-15 18:43:20 +08:00
MystiPanda a0625ef2e7 chore: remove 32bit package 2024-02-15 18:22:15 +08:00
MystiPanda b2774abb54 chore: default show proxy detials 2024-02-15 18:17:21 +08:00
wonfen fa089bbc40 doc: remove 32bit package 2024-02-12 02:25:18 +08:00
MystiPanda 8437d1763f fix: build error 2024-02-11 20:51:00 +08:00
MystiPanda 0532b0f9df Release v1.5.1 2024-02-11 20:32:25 +08:00
MystiPanda 5f8eb853f0 style: clear script 2024-02-11 20:03:14 +08:00
MystiPanda 9125a85382 fix: Custom GLOBAL group display error 2024-02-11 18:27:27 +08:00
MystiPanda 5b3dc44c72 feat: Show proxies count for provider 2024-02-10 13:13:27 +08:00
MystiPanda ee5147f111 feat: Save window maximize state 2024-02-10 12:51:30 +08:00
MystiPanda f67137fd5e style: change config name
#350
2024-02-08 10:23:54 +08:00
MystiPanda b67db4a896 fix: Script profile invalid
#347
2024-02-06 08:55:44 +08:00
MystiPanda 067018828c Release 1.5.0 2024-02-06 00:41:12 +08:00
MystiPanda e990530ada refactor: Remove clash field filter 2024-02-05 17:28:36 +08:00
wonfen 45f863a72a chore: enable clash field TLS fingerprint 2024-02-04 13:07:25 +08:00
MystiPanda 519febbeb2 fix: Try to fix traffic parse error
#337
2024-02-04 10:24:37 +08:00
wonfen 854b1a4caf chore: update changelog 2024-02-03 16:14:26 +08:00
MystiPanda 08eb95e73a fix: Copy Env Type Select Error 2024-02-02 17:57:56 +08:00
MystiPanda 66f8fbf08c chore: typo 2024-02-02 17:47:28 +08:00
MystiPanda 86a30bbb29 chore: Update Changelog 2024-02-02 17:34:15 +08:00
MystiPanda 966c453c27 fix: prevent_exit 2024-02-02 17:26:31 +08:00
MystiPanda c7bcaf0193 Release 1.4.11 2024-02-02 16:56:39 +08:00
MystiPanda 0ac91e36a0 fix: exit_app event 2024-02-02 16:32:19 +08:00
MystiPanda 6f1299ce9e chore: fix styles 2024-02-02 16:15:23 +08:00
MystiPanda 83ca29d649 chore: Remove prevent_close 2024-02-02 15:57:03 +08:00
MystiPanda d0206044dc fix: Fix Nisi Error 2024-02-02 15:53:28 +08:00
ycjcl868 d4623e4175 chore: unused clash core (#284) 2024-01-23 11:22:15 +08:00
MystiPanda 68ef03377d chore: fix style 2024-01-21 13:49:07 +08:00
MystiPanda d3f9d3d033 revert: some style 2024-01-21 13:49:07 +08:00
wonfen a6ccd04471 Sytle: UI improvement 2024-01-20 16:05:01 +08:00
Lai Zn 0fd8174e77 fix: Solve the confliction issues on auto updater failure (#273) 2024-01-20 13:03:48 +08:00
MystiPanda ebc63f479a revert: Support both registry and api for windows sysproxy 2024-01-20 12:16:46 +08:00
MystiPanda 570c39c20a build: Update lock file 2024-01-18 22:34:14 +08:00
MystiPanda 1d57257cf5 Release v1.4.10 2024-01-18 22:26:23 +08:00
MystiPanda 82d7baee0b feat: Add exit button on setting page 2024-01-18 22:19:14 +08:00
MystiPanda 1d91e1f1f7 build: Update Depends 2024-01-18 16:26:38 +08:00
MystiPanda 15e8894614 feat: Support Custom Start Page 2024-01-18 15:37:02 +08:00
Lai Zn 0ae96918e9 feat: Use url path name as fallback subscription name (#255) 2024-01-18 14:36:37 +08:00
MystiPanda e970880059 feat: Show SubInfo for Proxy Provider
#211
2024-01-18 14:26:57 +08:00
Lai Zn 69ae86aba8 fix: make port change set to system proxy immediately (#256) 2024-01-18 09:37:46 +08:00
MystiPanda 8142e1be49 fix: use nanoid to compatible with old devices 2024-01-18 01:16:39 +08:00
MystiPanda cbc7612451 feat: Optimizing Provider Support 2024-01-18 01:02:30 +08:00
Kiri 09e6a7b7cc feat: Add support for loong64 (#246) 2024-01-17 22:30:51 +08:00
Lai Zn 082e35668a docs: Add guidelines for windows development (#250) 2024-01-17 19:02:39 +08:00
MystiPanda c9e78c837b fix: Do not use proxy when test on tun mode 2024-01-17 18:08:09 +08:00
MystiPanda 2d171db672 Release v1.4.9 2024-01-17 15:43:15 +08:00
MystiPanda a8b11abec8 feat: Support Startup Script 2024-01-17 15:06:16 +08:00
MystiPanda b08333dccd feat: Support proxy group icon 2024-01-17 13:32:56 +08:00
MystiPanda 98bad48971 fix: Fix connection table sort error
#108
2024-01-17 12:54:54 +08:00
MystiPanda 53375bb536 chore: Update I18n 2024-01-17 11:30:19 +08:00
MystiPanda 88798093e1 fix: csp error 2024-01-17 11:08:14 +08:00
MystiPanda 45a28751af feat: Add Test Page 2024-01-17 11:02:17 +08:00
MystiPanda 1670c44464 fix: fix column width 2024-01-16 19:02:38 +08:00
MystiPanda a286ac85dc Release v1.4.8 2024-01-16 15:36:04 +08:00
MystiPanda 1606adc0ab feat: Support both registry and api for windows sysproxy 2024-01-16 15:11:53 +08:00
MystiPanda b4ce557d92 fix: Can not use specify update time when create profile 2024-01-16 10:29:04 +08:00
MystiPanda 2ba7fff8d4 refactor: rm theme blur 2024-01-15 17:13:55 +08:00
MystiPanda 98efc93610 Revert Use Tauri Http Api 2024-01-15 10:18:04 +08:00
MystiPanda 2680507eae Revert Use Tauri Websocket 2024-01-15 10:17:00 +08:00
MystiPanda df72392ad2 fix: Fix connections sort issue and add total traffic info 2024-01-15 00:46:19 +08:00
MystiPanda a6acb15a00 refactor: Use Tauri Http API 2024-01-14 19:35:03 +08:00
MystiPanda ba7242a815 refactor: Use Tauri WebSocket 2024-01-14 17:30:18 +08:00
MystiPanda 99740c1324 fix patch error 2024-01-11 14:47:25 +08:00
MystiPanda c04b20d1fa Release v1.4.7 2024-01-11 13:19:05 +08:00
MystiPanda e127067878 feat: Disable updater for portable 2024-01-11 12:44:30 +08:00
MystiPanda 6f03b72368 feat: Support hide group
#214
2024-01-11 12:34:05 +08:00
Morris Li 7bd3ee9340 Fix expired Tauri domain (#222) 2024-01-11 10:17:56 +08:00
MystiPanda c94db606e5 build: Use old sysproxy 2024-01-10 19:24:14 +08:00
MystiPanda 4d2474226b refactor: cargo clippy 2024-01-10 17:36:35 +08:00
MystiPanda 1ffc4f538b refactor: Optimizing the implementation of Linux URL Scheme registration 2024-01-10 16:34:35 +08:00
MystiPanda 6702ac957b fix: resolve scheme error 2024-01-10 15:37:40 +08:00
MystiPanda ba42b2e77d fix: linux build error 2024-01-10 15:22:08 +08:00
MystiPanda fba18ca40a feat: Support URL Scheme for MacOS 2024-01-10 14:04:06 +08:00
MystiPanda 3a2a7a1476 feat: Support URL Scheme for Linux 2024-01-10 13:03:34 +08:00
MystiPanda 3c6d6b90bc build: fix macos build script 2024-01-09 22:49:11 +08:00
MystiPanda 47dc9ee304 fix patch error 2024-01-09 22:25:25 +08:00
MystiPanda 0045bf206c Release v1.4.6 2024-01-09 22:17:29 +08:00
MystiPanda 669a1a6953 feat: Support URL Scheme for Windows
#165
2024-01-09 21:57:06 +08:00
MystiPanda e28452cc7b feat: Optimize control button style 2024-01-09 16:04:56 +08:00
MystiPanda 5ceac03db1 feat: Add pin button 2024-01-09 15:13:59 +08:00
MystiPanda 96215e5950 fix: Fix some compile error 2024-01-09 14:52:43 +08:00
wonfen 7a030b9224 Style: UI improvement & 1.4.6 ready 2024-01-09 13:57:53 +08:00
wonfen e743478a4d chore: UI adjustment 2024-01-08 19:32:21 +08:00
MystiPanda bc29c80d44 chore: Use Latest and compatible core 2023-12-23 12:09:19 +08:00
MystiPanda 82b8a474d7 fix: Cargo clippy 2023-12-21 16:49:21 +08:00
MystiPanda 99851b297d fix: Get filename error
#165
2023-12-19 19:52:13 +08:00
MystiPanda e3a500e12c fix: portable flag 2023-12-15 21:39:34 +08:00
MystiPanda 378666e3cc chore: update service url 2023-12-15 20:38:17 +08:00
MystiPanda b65ad1ebd7 fix: user-agent version error 2023-12-15 15:18:01 +08:00
MystiPanda 933a821b5f fix: Subinfo parse error 2023-12-15 11:35:10 +08:00
MystiPanda 159337ddbd Release 1.4.5 2023-12-14 17:40:48 +08:00
MystiPanda 2e25a4333a fix: Window control button icon issue
#136
2023-12-14 14:59:55 +08:00
MystiPanda 64fafb0795 chore: Remove unsafe function 2023-12-14 13:56:51 +08:00
MystiPanda 5927e0bb3b fix: icon size 2023-12-14 13:20:01 +08:00
MystiPanda e21596db83 feat: Update MacOS tray icon 2023-12-14 13:11:46 +08:00
MystiPanda 8a608c3c3e chore: Optimize service path 2023-12-14 13:03:52 +08:00
MystiPanda 76f9db8516 chore: Optimize upgrade process 2023-12-14 11:24:26 +08:00
MystiPanda 90f4809b7c chore: default enable clash field filter 2023-12-14 10:49:58 +08:00
MystiPanda 3ff1af9fd6 chore: Delete unnecessary drag area 2023-12-13 11:09:19 +08:00
MystiPanda 711dd520f7 chore: Change service path 2023-12-13 10:59:07 +08:00
MystiPanda 99503c836a fix: Save wrong window size 2023-12-12 16:42:19 +08:00
MystiPanda 2c8bc51862 Revert "fix: Change PID file path"
This reverts commit d64bdf02de.
2023-12-12 15:59:00 +08:00
MystiPanda 5e7aa15232 fix: Can't switch env type 2023-12-11 10:51:59 +08:00
MystiPanda d0761869b6 build: fix update issue 2023-12-10 22:24:46 +08:00
MystiPanda bb99b89228 build: fix patch 2023-12-10 21:40:57 +08:00
MystiPanda 7b88beb0b5 v1.4.4 2023-12-10 21:36:22 +08:00
MystiPanda 82c630bd0e feat: Support windows aarch64 (#112) 2023-12-10 20:45:27 +08:00
MystiPanda bb4f11e1b8 feat: Patch for windows aarch64 2023-12-10 19:50:01 +08:00
MystiPanda 3182d20b7c build: Update Depends 2023-12-10 17:14:48 +08:00
MystiPanda a6f15e2474 chore: Hide script mode for alpha core 2023-12-10 16:31:55 +08:00
MystiPanda 0c3f03680c Support upgrade alpha core 2023-12-10 15:57:10 +08:00
MystiPanda a66e8dfc9f feat: Support update geodata
#37
2023-12-10 15:22:04 +08:00
zclkkk 556232aac4 chore: Allow user to choose where to install (#110) 2023-12-10 09:50:43 +08:00
MystiPanda 7fcfaadd91 chore: Remove script mode 2023-12-09 17:04:03 +08:00
MystiPanda 18d388e1e2 docs: Update ISSUE_TEMPLATE 2023-12-09 12:00:01 +08:00
MystiPanda 7de6622d74 feat: Support different tray icon for macos 2023-12-09 11:22:02 +08:00
MystiPanda 9af1f90990 feat: Support different tray icon for linux 2023-12-09 11:03:19 +08:00
Pylogmon 15ab43963d feat: Optimize copy environment variable logic (#106) 2023-12-08 22:16:42 +08:00
MystiPanda 6b7465a4b0 perf: Improves window creation speed 2023-12-08 13:10:35 +08:00
MystiPanda d64bdf02de fix: Change PID file path
#99
2023-12-08 11:59:40 +08:00
MystiPanda 4a2c94993a docs: Update README 2023-12-07 16:49:59 +08:00
MystiPanda 86c318d86b feat: Add AppImage for x86 linux 2023-12-07 16:38:39 +08:00
MystiPanda bffc1ad41d ci: fix build script 2023-12-07 16:17:34 +08:00
MystiPanda 2a685c116f chore: Fix build error 2023-12-07 16:02:29 +08:00
MystiPanda 0e271d2924 ci: Fix Linux CI Script 2023-12-07 15:57:53 +08:00
MystiPanda 7127c4d590 ci: Fix Linux Build Script 2023-12-07 15:34:49 +08:00
MystiPanda a82929996b chore: change default port to 7897 2023-12-07 14:52:14 +08:00
MystiPanda 3f01f52c7b refactor: fix depends 2023-12-07 14:44:44 +08:00
MystiPanda 9c94494622 refactor: Remove unnecessary changes 2023-12-07 13:43:53 +08:00
MystiPanda c8a508f35c Revert "chore: change default port to 7897"
This reverts commit 4ec73ef1c3.
2023-12-07 13:32:49 +08:00
wonfen 4ec73ef1c3 chore: change default port to 7897 2023-12-06 03:37:16 +08:00
MystiPanda 3465b79e5b docs: Update README 2023-12-05 17:02:54 +08:00
116 changed files with 6082 additions and 3210 deletions
-33
View File
@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Information**
- OS: [e.g. macOS]
- Clash Verge Version: [e.g. 1.3.4]
- Clash Core: [e.g. Clash or Clash Meta]
**Additional context**
Add any other context about the problem here.
+45
View File
@@ -0,0 +1,45 @@
name: Bug report
description: Create a report to help us improve
title: "[BUG]"
labels: ["bug"]
body:
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: Steps to reproduce the behavior.
validations:
required: true
- type: dropdown
attributes:
label: Platform
options:
- Windows
- Linux
- MacOS
validations:
required: true
- type: input
attributes:
label: System Version
placeholder: "e.g. macOS 10.15.7"
validations:
required: true
- type: input
attributes:
label: Software Version
placeholder: "e.g. 1.4.3"
validations:
required: true
- type: textarea
attributes:
label: Log
description: "Log file content or screenshot"
- type: textarea
attributes:
label: Additional Information
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,27 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature]"
labels: ["enhancement"]
body:
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
+4
View File
@@ -0,0 +1,4 @@
FROM rust:buster
COPY entrypoint.sh /entrypoint.sh
RUN chmod a+x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
+14
View File
@@ -0,0 +1,14 @@
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 }}
+8
View File
@@ -0,0 +1,8 @@
pnpm install
pnpm check $INPUT_TARGET
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
pnpm build --target $INPUT_TARGET
else
pnpm build --target $INPUT_TARGET -b deb
fi
+37
View File
@@ -0,0 +1,37 @@
#!/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"
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
apt-get update
apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
dpkg --add-architecture arm64
apt-get update
apt-get install -y libncurses6:arm64 libtinfo6:arm64 linux-libc-dev:arm64 libncursesw6:arm64 libssl3:arm64 libcups2:arm64
apt-get install -y --no-install-recommends g++-aarch64-linux-gnu libc6-dev-arm64-cross libssl-dev:arm64 libwebkit2gtk-4.0-dev:arm64 libgtk-3-dev:arm64 patchelf:arm64 librsvg2-dev:arm64 libayatana-appindicator3-dev:arm64
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
export PKG_CONFIG_ALLOW_CROSS=1
elif [ "$INPUT_TARGET" = "armv7-unknown-linux-gnueabihf" ]; then
dpkg --add-architecture armhf
apt-get update
apt-get install -y libncurses6:armhf libtinfo6:armhf linux-libc-dev:armhf libncursesw6:armhf libssl3:armhf libcups2:armhf
apt-get install -y --no-install-recommends g++-arm-linux-gnueabihf libc6-dev-armhf-cross libssl-dev:armhf libwebkit2gtk-4.0-dev:armhf libgtk-3-dev:armhf patchelf:armhf librsvg2-dev:armhf libayatana-appindicator3-dev:armhf
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
export CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc
export CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig
export PKG_CONFIG_ALLOW_CROSS=1
else
echo "Unknown target: $INPUT_TARGET" && exit 1
fi
bash .github/build-for-linux/build.sh
+169
View File
@@ -0,0 +1,169 @@
name: Alpha Build
on:
workflow_dispatch:
push:
branches: [main]
tags-ignore: [updater, alpha]
permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
alpha:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-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
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Apply Patch
if: matrix.target == 'aarch64-pc-windows-msvc'
run: |
git config --global user.email "clash-verge-rev@github.io"
git config --global user.name "clash-verge-rev"
git am patches/support-windows-aarch64.patch
- name: Init Submodule
if: matrix.target == 'aarch64-pc-windows-msvc'
run: git submodule update --init --recursive
- 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
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: alpha
releaseName: "Clash Verge Rev Alpha"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: pnpm portable ${{ matrix.target }} --alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
alpha-for-linux:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Build for Linux
uses: ./.github/build-for-linux
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
target: ${{ matrix.target }}
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Upload Release
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
update_tag:
name: Update tag
runs-on: ubuntu-latest
needs: [alpha, alpha-for-linux]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set Env
run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
shell: bash
- name: Update Tag
uses: richardsimko/update-tag@v1
with:
tag_name: alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
cat > release.txt << 'EOF'
## Clash Verge Rev Alpha
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
-101
View File
@@ -1,101 +0,0 @@
name: Compatible CI
on:
workflow_dispatch:
# push:
# tags:
# - v**
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
build:
strategy:
fail-fast: false
matrix:
targets:
- tag: macOS-10.15
os: macos-10.15
- tag: Ubuntu18
os: ubuntu-18.04
- tag: Ubuntu22
os: ubuntu-22.04
runs-on: ${{ matrix.targets.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: 16
# - name: Install Dependencies (ubuntu18 only)
# if: matrix.targets.os == 'ubuntu-18.04'
# run: |
# sudo apt-get update
# sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev libayatana-appindicator3-dev
- name: Install Dependencies (ubuntu22 only)
if: startsWith(matrix.targets.os, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Get yarn cache dir path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install and check
run: |
yarn install --network-timeout 1000000
yarn run check
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: ${{ matrix.targets.tag }}
releaseName: "Compatible For ${{ matrix.targets.tag }}"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: false
# - name: Portable Bundle
# if: matrix.os == 'windows-latest'
# # rebuild with env settings
# run: |
# yarn build
# yarn run portable
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
# TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
# VITE_WIN_PORTABLE: 1
-97
View File
@@ -1,97 +0,0 @@
name: Dev Build
on:
workflow_dispatch:
push:
branches:
- dev
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
release:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Delete current release assets
if: startsWith(matrix.os, 'ubuntu-')
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: dev
fail-if-no-assets: false
fail-if-no-release: false
assets: |
*.zip
*.gz
*.AppImage
*.deb
*.dmg
*.msi
*.sig
*.exe
*.json
- name: Install Dependencies (Ubuntu Only)
if: startsWith(matrix.os, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
- name: Pnpm install and check
run: |
pnpm i
pnpm check
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: dev
releaseName: "Clash Verge Dev"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
tauriScript: pnpm
args: -f default-meta
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: |
pnpm portable
env:
TAG_NAME: dev
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_WIN_PORTABLE: 1
-106
View File
@@ -1,106 +0,0 @@
name: Meta CI
on:
workflow_dispatch:
push:
tags:
- v**
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
release:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Delete current release assets
if: matrix.os == 'ubuntu-latest'
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: meta
fail-if-no-assets: false
fail-if-no-release: false
assets: |
*.zip
*.gz
*.AppImage
*.deb
*.dmg
*.msi
*.sig
- name: Install Dependencies (ubuntu only)
if: startsWith(matrix.os, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
- name: Get yarn cache dir path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install and check
run: |
yarn install --network-timeout 1000000
yarn run check
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: meta
releaseName: "Clash Verge Meta"
releaseBody: ""
releaseDraft: false
prerelease: true
args: -f default-meta
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: |
yarn build -f default-meta
yarn run portable
env:
TAG_NAME: meta
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_WIN_PORTABLE: 1
+65 -98
View File
@@ -2,9 +2,6 @@ name: Release Build
on:
workflow_dispatch:
push:
tags:
- v**
permissions: write-all
env:
CARGO_INCREMENTAL: 0
@@ -19,31 +16,33 @@ jobs:
- 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
target: aarch64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
# - os: ubuntu-latest
# target: armv7-unknown-linux-gnueabihf
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Apply Patch
if: matrix.target == 'aarch64-pc-windows-msvc'
run: |
git config --global user.email "clash-verge-rev@github.io"
git config --global user.name "clash-verge-rev"
git am patches/support-windows-aarch64.patch
- name: Init Submodule
if: matrix.target == 'aarch64-pc-windows-msvc'
run: git submodule update --init --recursive
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }}
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
@@ -61,43 +60,12 @@ jobs:
version: 8
run_install: false
- name: Install Dependencies (Ubuntu Only)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'x86_64')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libayatana-appindicator3-dev librsvg2-dev patchelf openssl
- name: Install Dependencies (Ubuntu Only)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'aarch64')
run: |
sed 's/mirror+file:\/etc\/apt\/apt-mirrors.txt/[arch-=amd64] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | sudo tee /etc/apt/sources.list.d/ports.list
sudo sed -i 's/mirror+file/[arch=amd64] mirror+file/g' /etc/apt/sources.list
cat /etc/apt/sources.list
cat /etc/apt/sources.list.d/ports.list
sudo dpkg --add-architecture arm64
sudo apt-get update
sudo apt-get install -y libncurses6:arm64 libtinfo6:arm64 linux-libc-dev:arm64 libncursesw6:arm64 libssl3:arm64 libcups2:arm64
sudo apt-get install -y --no-install-recommends g++-aarch64-linux-gnu libc6-dev-arm64-cross libssl-dev:arm64 libwebkit2gtk-4.0-dev:arm64 libgtk-3-dev:arm64 patchelf:arm64 librsvg2-dev:arm64 libayatana-appindicator3-dev:arm64
- name: Install Dependencies (Ubuntu Only)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'armv7')
run: |
sed 's/mirror+file:\/etc\/apt\/apt-mirrors.txt/[arch-=amd64] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | sudo tee /etc/apt/sources.list.d/ports.list
sudo sed -i 's/mirror+file/[arch=amd64] mirror+file/g' /etc/apt/sources.list
cat /etc/apt/sources.list
cat /etc/apt/sources.list.d/ports.list
sudo dpkg --add-architecture armhf
sudo apt-get update
sudo apt-get install -y libncurses6:armhf libtinfo6:armhf linux-libc-dev:armhf libncursesw6:armhf libssl3:armhf libcups2:armhf
sudo apt-get install -y --no-install-recommends g++-arm-linux-gnueabihf libc6-dev-armhf-cross libssl-dev:armhf libwebkit2gtk-4.0-dev:armhf libgtk-3-dev:armhf patchelf:armhf librsvg2-dev:armhf libayatana-appindicator3-dev:armhf
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Tauri build
if: startsWith(matrix.os, 'windows') || startsWith(matrix.os,'macos') || startsWith(matrix.target,'x86_64')
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -105,75 +73,74 @@ jobs:
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: v__VERSION__
releaseName: "Clash Verge v__VERSION__"
releaseName: "Clash Verge Rev v__VERSION__"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: false
tauriScript: pnpm
args: -f default-meta --target ${{ matrix.target }}
- name: Tauri build (Ubuntu Arm64)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'aarch64')
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++
PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig
PKG_CONFIG_ALLOW_CROSS: 1
with:
tagName: v__VERSION__
releaseName: "Clash Verge v__VERSION__"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: false
tauriScript: pnpm
args: -f default-meta --target ${{ matrix.target }} -b deb,updater
- name: Tauri build (Ubuntu Armv7)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'armv7')
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
CC_armv7_unknown_linux_gnueabihf: arm-linux-gnueabihf-gcc
CXX_armv7_unknown_linux_gnueabihf: arm-linux-gnueabihf-g++
PKG_CONFIG_PATH: /usr/lib/arm-linux-gnueabihf/pkgconfig
PKG_CONFIG_ALLOW_CROSS: 1
with:
tagName: v__VERSION__
releaseName: "Clash Verge v__VERSION__"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: false
tauriScript: pnpm
args: -f default-meta --target ${{ matrix.target }} -b deb,updater
args: --target ${{ matrix.target }}
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: |
pnpm portable ${{ matrix.target }}
run: pnpm portable ${{ matrix.target }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_WIN_PORTABLE: 1
release-update:
needs: [release]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
release-for-linux:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
- name: Checkout Repository
uses: actions/checkout@v4
- name: Build for Linux
uses: ./.github/build-for-linux
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
target: ${{ matrix.target }}
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Upload Release
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
release-update:
runs-on: ubuntu-latest
needs: [release, release-for-linux]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: "20"
-75
View File
@@ -1,75 +0,0 @@
name: Test CI
on:
workflow_dispatch:
inputs:
os:
description: "Runs on OS"
required: true
default: windows-latest
type: choice
options:
- windows-latest
- ubuntu-latest
- macos-latest
- ubuntu-18.04
- ubuntu-20.04
- ubuntu-22.04
- macos-10.15
- macos-11
- macos-12
- windows-2019
- windows-2022
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
release:
runs-on: ${{ github.event.inputs.os }}
steps:
- name: System Version
run: |
echo ${{ github.event.inputs.os }}
- name: Checkout repository
uses: actions/checkout@v4
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "16"
cache: "yarn"
- name: Install Dependencies (ubuntu only)
if: startsWith(github.event.inputs.os, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Yarn install and check
run: |
yarn install --network-timeout 1000000 --frozen-lockfile
yarn run check
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: alpha
releaseName: "Clash Verge Alpha"
releaseBody: "Alpha Version (include debug)"
releaseDraft: false
includeUpdaterJson: false
+67
View File
@@ -0,0 +1,67 @@
# CONTRIBUTING
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
## Development Setup
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
### Prerequisites
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
### Setup for Windows Users
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:
```shell
pnpm i
```
### Download the Clash Binary
You have two options for downloading the clash binary:
- Automatically download it via the provided script:
```shell
pnpm run check
# Use '--force' to force update to the latest version
# pnpm run check --force
```
- Manually download it from the [Clash Meta release](https://github.com/MetaCubeX/Clash.Meta/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
### Run the Development Server
To run the development server, use the following command:
```shell
pnpm dev
# If an app instance already exists, use a different command
pnpm dev:diff
```
### Build the Project
If you want to build the project, use:
```shell
pnpm build
```
## Contributing Your Changes
Once you have made your changes:
1. Fork the repository.
2. Create a new branch for your feature or bug fix.
3. Commit your changes with clear and concise commit messages.
4. Push your branch to your fork and submit a pull request to our repository.
We appreciate your contributions and look forward to your active participation in our project!
+23 -44
View File
@@ -9,10 +9,28 @@
A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
</h3>
## Install
Click on the corresponding link below to download the installation package. Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
[[Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/Clash.Verge_1.5.2_x64-setup.exe)]
[[Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/Clash.Verge_1.5.2_arm64-setup.exe)]
[[macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/Clash.Verge_1.5.2_x64.dmg)]
[[macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/Clash.Verge_1.5.2_aarch64.dmg)]
[[Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/clash-verge_1.5.2_amd64.AppImage)]
[[Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/clash-verge_1.5.2_amd64.deb)]
[[Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/clash-verge_1.5.2_arm64.deb)]
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
## Features
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/clash-verge-rev/clash-verge-rev/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97)
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://clash-verge-rev.github.io)
- Simple UI and supports custom theme color.
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
- System proxy setting and guard.
@@ -37,24 +55,6 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
![preview](./docs/preview.gif)
## Install
Download from [release](https://github.com/clash-verge-rev/clash-verge-rev/releases). Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
- [Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.2/Clash.Verge_1.4.2_x64-setup.exe)
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.2/Clash.Verge_1.4.2_x86-setup.exe)
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.2/Clash.Verge_1.4.2_x64.dmg)
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.2/Clash.Verge_1.4.2_aarch64.dmg)
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.2/clash-verge_1.4.2_amd64.AppImage)
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.2/clash-verge_1.4.2_amd64.deb)
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.2/clash-verge_1.4.2_arm64.deb)
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
### FAQ
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
@@ -63,34 +63,14 @@ open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/C
## Development
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
To run the development server, execute the following commands after all prerequisites for **Tauri** are installed:
```shell
pnpm i
```
Then download the clash binary... Or you can download it from [clash meta release](https://github.com/MetaCubeX/Clash.Meta/releases) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
```shell
# force update to latest version
# pnpm run check --force
pnpm run check
```
Then run
```shell
pnpm dev
# run it in another way if app instance exists
pnpm dev:diff
```
Or you can build it
```shell
pnpm build
```
## Todos
@@ -109,11 +89,10 @@ Issue and PR welcome!
Clash Verge rev was based on or inspired by these projects and so on:
- [keiko233/clash-nyanpasu](https://github.com/keiko233/clash-nyanpasu): A Clash Verge variant.
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): A Clash GUI based on tauri. Supports Windows, macOS and Linux.
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.
- [MetaCubeX/Clash.Meta](https://github.com/MetaCubeX/Clash.Meta): A rule-based tunnel in Go.
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): A rule-based tunnel in Go.
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash.
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
+176
View File
@@ -1,3 +1,179 @@
## v1.5.2
### Features
- 支持自定义延迟测试超时时间
- 优化 Tun 相关设置
### Bugs Fixes
- Merge 操作出错
- 安装后重启服务
- 修复管理员权限启动时开机启动失效的问题
---
## v1.5.1
### Features
- 保存窗口最大化状态
- Proxy Provider 显示数量
- 不再提供 32 位安装包(因为 32 位经常出现各种奇怪问题,比如 tun 模式无法开启;现在系统也几乎没有 32 位了)
### Bugs Fixes
- 优化设置项名称
- 自定义 GLOBAL 代理组时代理组显示错误的问题
---
## v1.5.0
### Features
- 删除 Clash 字段过滤功能
- 添加 socks 端口和 http 端口设置
- 升级内核到 1.18.1
### Bugs Fixes
- 修复 32 位版本无法显示流量信息的问题
---
## v1.4.11
### Break Changes
- 此版本更改了 Windows 安装包安装模式,需要卸载后手动安装,否则无法安装到正确位置
### Features
- 优化了系统代理开启的代码,解决了稀有场景下代理开启卡顿的问题
- 添加 MacOS 下的 debug 日志,以便日后调试稀有场景下 MacOS 下无法开启系统代理的问题
- MacOS 关闭 GUI 时同步杀除后台 GUI [#306](https://github.com/clash-verge-rev/clash-verge-rev/issues/306)
### Bugs Fixes
- 解决自动更新时文件占用问题
- 解决稀有场景下系统代理开启失败的问题
- 删除冗余内核代码
---
## v1.4.10
### Features
- 设置中添加退出按钮
- 支持自定义软件启动页
- 在 Proxy Provider 页面展示订阅信息
- 优化 Provider 支持
### Bugs Fixes
- 更改端口时立即重设系统代理
- 网站测试超时错误
---
## v1.4.9
### Features
- 支持启动时运行脚本
- 支持代理组显示图标
- 新增测试页面
### Bugs Fixes
- 连接页面时间排序错误
- 连接页面表格宽度优化
---
## v1.4.8
### Features
- 连接页面总流量显示
### Bugs Fixes
- 连接页面数据排序错误
- 新建订阅时设置更新间隔无效
- Windows 拨号网络无法设置系统代理
- Windows 开启/关闭系统代理延迟(使用注册表即可)
- 删除无效的背景模糊选项
---
## v1.4.7
### Features
- Windows 便携版禁用应用内更新
- 支持代理组 Hidden 选项
- 支持 URL Scheme(MacOS & Linux)
---
## v1.4.6
### Features
- 更新 Clash Meta(mihomo) 内核到 v1.18.0
- 支持 URL Scheme(暂时仅支持 Windows)
- 添加窗口置顶按钮
- UI 优化调整
### Bugs Fixes
- 修复一些编译错误
- 获取订阅名称错误
- 订阅信息解析错误
---
## v1.4.5
### Features
- 更新 MacOS 托盘图标样式(@gxx2778 贡献)
### Bugs Fixes
- Windows 下更新时无法覆盖`clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
- 窗口最大化按钮变化问题
- 窗口尺寸保存错误问题
- 复制环境变量类型无法切换问题
- 某些情况下闪退的问题
- 某些订阅无法导入的问题
---
## v1.4.4
### Features
- 支持 Windows aarch64(arm64) 版本
- 支持一键更新 GeoData
- 支持一键更新 Alpha 内核
- MacOS 支持在系统代理时显示不同的托盘图标
- Linux 支持在系统代理时显示不同的托盘图标
- 优化复制环境变量逻辑
### Bugs Fixes
- 修改 PID 文件的路径
### Performance
- 优化创建窗口的速度
---
## v1.4.3
### Break Changes
+23 -25
View File
@@ -1,19 +1,17 @@
{
"name": "clash-verge",
"version": "1.4.3",
"license": "GPL-3.0",
"version": "1.5.2",
"license": "GPL-3.0-only",
"scripts": {
"dev": "tauri dev -f default-meta",
"dev": "tauri dev",
"dev:diff": "tauri dev -f verge-dev",
"build": "tauri build -f default-meta",
"build": "tauri build",
"tauri": "tauri",
"web:dev": "vite",
"web:build": "tsc && vite build",
"web:serve": "vite preview",
"aarch": "node scripts/aarch.mjs",
"check": "node scripts/check.mjs",
"updater": "node scripts/updater.mjs",
"publish": "node scripts/publish.mjs",
"portable": "node scripts/portable.mjs",
"prepare": "husky install"
},
@@ -21,26 +19,27 @@
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.1",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.14.19",
"@mui/icons-material": "^5.15.5",
"@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.14.19",
"@mui/x-data-grid": "^6.18.2",
"@tauri-apps/api": "^1.5.1",
"@mui/material": "^5.15.5",
"@mui/x-data-grid": "^6.18.7",
"@tauri-apps/api": "^1.5.3",
"ahooks": "^3.7.8",
"axios": "^1.6.2",
"axios": "^1.6.5",
"dayjs": "1.11.5",
"i18next": "^23.7.7",
"i18next": "^23.7.16",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.34.1",
"nanoid": "^5.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.48.2",
"react-hook-form": "^7.49.3",
"react-i18next": "^13.5.0",
"react-router-dom": "^6.20.0",
"react-router-dom": "^6.21.2",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.6.2",
"recoil": "^0.7.7",
@@ -50,15 +49,14 @@
},
"devDependencies": {
"@actions/github": "^5.1.1",
"@tauri-apps/cli": "^1.5.6",
"@tauri-apps/cli": "^1.5.9",
"@types/fs-extra": "^9.0.13",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.14.202",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.17",
"@types/react-transition-group": "^4.4.9",
"@vitejs/plugin-react": "^4.2.0",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/react-transition-group": "^4.4.10",
"@vitejs/plugin-react": "^4.2.1",
"adm-zip": "^0.5.10",
"cross-env": "^7.0.3",
"fs-extra": "^11.2.0",
@@ -66,10 +64,10 @@
"husky": "^7.0.4",
"node-fetch": "^3.3.2",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"sass": "^1.69.5",
"typescript": "^5.3.2",
"vite": "^4.5.0",
"pretty-quick": "^3.3.1",
"sass": "^1.70.0",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
},
+195
View File
@@ -0,0 +1,195 @@
From 871c9a6d1ed014c93da2436a437df03734e9f76c Mon Sep 17 00:00:00 2001
From: MystiPanda <mystipanda@proton.me>
Date: Sun, 10 Dec 2023 19:47:45 +0800
Subject: [PATCH] feat: Support windows aarch64
---
.gitmodules | 3 +
src-tauri/Cargo.toml | 2 +-
src-tauri/quick-rs | 1 +
src-tauri/src/enhance/script.rs | 130 +++++++++++++++++++-------------
4 files changed, 81 insertions(+), 55 deletions(-)
create mode 100644 .gitmodules
create mode 160000 src-tauri/quick-rs
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..2eda7e4
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "src-tauri/quick-rs"]
+ path = src-tauri/quick-rs
+ url = https://github.com/clash-verge-rev/quick-rs.git
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 2f1a3be..d67f6ed 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -25,7 +25,6 @@ log4rs = "1"
nanoid = "0.4"
chrono = "0.4"
sysinfo = "0.30"
-rquickjs = "0.3" # 高版本不支持 Linux aarch64
serde_json = "1.0"
serde_yaml = "0.9"
once_cell = "1.18"
@@ -33,6 +32,7 @@ port_scanner = "0.1.5"
delay_timer = "0.11.5"
parking_lot = "0.12"
percent-encoding = "2.3.1"
+quick-rs = { path = "quick-rs" }
window-shadows = { version = "0.2" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
diff --git a/src-tauri/quick-rs b/src-tauri/quick-rs
new file mode 160000
index 0000000..78277c4
--- /dev/null
+++ b/src-tauri/quick-rs
@@ -0,0 +1 @@
+Subproject commit 78277c4509c64f18c0fc5c9f2b84671de7c83343
diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs
index 30a922f..d47dc33 100644
--- a/src-tauri/src/enhance/script.rs
+++ b/src-tauri/src/enhance/script.rs
@@ -3,61 +3,83 @@ use anyhow::Result;
use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
- use rquickjs::{function::Func, Context, Runtime};
- use std::sync::{Arc, Mutex};
-
- let runtime = Runtime::new().unwrap();
- let context = Context::full(&runtime).unwrap();
- let outputs = Arc::new(Mutex::new(vec![]));
-
- let copy_outputs = outputs.clone();
- let result = context.with(|ctx| -> Result<Mapping> {
- ctx.globals().set(
- "__verge_log__",
- Func::from(move |level: String, data: String| {
- let mut out = copy_outputs.lock().unwrap();
- out.push((level, data));
- }),
- )?;
-
- ctx.eval(
- r#"var console = Object.freeze({
- log(data){__verge_log__("log",JSON.stringify(data))},
- info(data){__verge_log__("info",JSON.stringify(data))},
- error(data){__verge_log__("error",JSON.stringify(data))},
- debug(data){__verge_log__("debug",JSON.stringify(data))},
- });"#,
- )?;
-
- let config = use_lowercase(config.clone());
- let config_str = serde_json::to_string(&config)?;
-
- let code = format!(
- r#"try{{
+ use quick_rs::{context::Context, function::Function, module::Module, runtime::Runtime};
+
+ let config = use_lowercase(config.clone());
+ let config_str = serde_json::to_string(&config)?;
+
+ let runtime = Runtime::new();
+ let context = Context::from(&runtime);
+
+ let code = format!(
+ r#"
+ let output = [];
+
+ function __verge_log__(type, data) {{
+ output.push([type, data]);
+ }}
+
+ var console = Object.freeze({{
+ log(data) {{ __verge_log__("log", JSON.stringify(data)) }},
+ info(data) {{ __verge_log__("info", JSON.stringify(data)) }},
+ error(data) {{ __verge_log__("error", JSON.stringify(data)) }},
+ debug(data) {{ __verge_log__("debug", JSON.stringify(data)) }},
+ }});
+
{script};
- JSON.stringify(main({config_str})||'')
- }} catch(err) {{
- `__error_flag__ ${{err.toString()}}`
- }}"#
- );
- let result: String = ctx.eval(code.as_str())?;
- if result.starts_with("__error_flag__") {
- anyhow::bail!(result[15..].to_owned());
- }
- if result == "\"\"" {
- anyhow::bail!("main function should return object");
- }
- Ok(serde_json::from_str::<Mapping>(result.as_str())?)
- });
-
- let mut out = outputs.lock().unwrap();
- match result {
- Ok(config) => Ok((use_lowercase(config), out.to_vec())),
- Err(err) => {
- out.push(("exception".into(), err.to_string()));
- Ok((config, out.to_vec()))
- }
- }
+
+ export function _main(){{
+ try{{
+ let result = JSON.stringify(main({config_str})||"");
+ return JSON.stringify({{result, output}});
+ }} catch(err) {{
+ output.push(["exception", err.toString()]);
+ return JSON.stringify({{result: "__error__", output}});
+ }}
+ }}
+ "#
+ );
+ let value = context.eval_module(&code, "_main")?;
+ let module = Module::new(value)?;
+ let value = module.get("_main")?;
+ let function = Function::new(value)?;
+ let value = function.call(vec![])?;
+ let result = serde_json::from_str::<serde_json::Value>(&value.to_string()?)?;
+ result
+ .as_object()
+ .map(|obj| {
+ let result = obj.get("result").unwrap().as_str().unwrap();
+ let output = obj.get("output").unwrap();
+
+ let mut out = output
+ .as_array()
+ .unwrap()
+ .iter()
+ .map(|item| {
+ let item = item.as_array().unwrap();
+ (
+ item[0].as_str().unwrap().into(),
+ item[1].as_str().unwrap().into(),
+ )
+ })
+ .collect::<Vec<_>>();
+ if result.is_empty() {
+ anyhow::bail!("main function should return object");
+ }
+ if result == "__error__" {
+ return Ok((config, out.to_vec()));
+ }
+ let result = serde_json::from_str::<Mapping>(result);
+
+ match result {
+ Ok(config) => Ok((use_lowercase(config), out.to_vec())),
+ Err(err) => {
+ out.push(("exception".into(), err.to_string()));
+ Ok((config, out.to_vec()))
+ }
+ }
+ })
+ .unwrap_or_else(|| anyhow::bail!("Unknown result"))
}
#[test]
--
2.43.0.windows.1
+651 -591
View File
File diff suppressed because it is too large Load Diff
-119
View File
@@ -1,119 +0,0 @@
/**
* Build and upload assets
* for macOS(aarch)
*/
import fs from "fs-extra";
import path from "path";
import { exit } from "process";
import { execSync } from "child_process";
import { createRequire } from "module";
import { getOctokit, context } from "@actions/github";
// to `meta` tag
const META = process.argv.includes("--meta");
// to `alpha` tag
const ALPHA = process.argv.includes("--alpha");
const require = createRequire(import.meta.url);
async function resolve() {
if (!process.env.GITHUB_TOKEN) {
throw new Error("GITHUB_TOKEN is required");
}
if (!process.env.GITHUB_REPOSITORY) {
throw new Error("GITHUB_REPOSITORY is required");
}
if (!process.env.TAURI_PRIVATE_KEY) {
throw new Error("TAURI_PRIVATE_KEY is required");
}
if (!process.env.TAURI_KEY_PASSWORD) {
throw new Error("TAURI_KEY_PASSWORD is required");
}
const { version } = require("../package.json");
const tag = META ? "meta" : ALPHA ? "alpha" : `v${version}`;
const buildCmd = META ? `pnpm build -f default-meta` : `pnpm build`;
console.log(`[INFO]: Upload to tag "${tag}"`);
console.log(`[INFO]: Building app. "${buildCmd}"`);
execSync(buildCmd);
const cwd = process.cwd();
const bundlePath = path.join(cwd, "src-tauri/target/release/bundle");
const join = (p) => path.join(bundlePath, p);
const appPathList = [
join("macos/Clash Verge.aarch64.app.tar.gz"),
join("macos/Clash Verge.aarch64.app.tar.gz.sig"),
];
for (const appPath of appPathList) {
if (fs.pathExistsSync(appPath)) {
fs.removeSync(appPath);
}
}
fs.copyFileSync(join("macos/Clash Verge.app.tar.gz"), appPathList[0]);
fs.copyFileSync(join("macos/Clash Verge.app.tar.gz.sig"), appPathList[1]);
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
const { data: release } = await github.rest.repos.getReleaseByTag({
...options,
tag,
});
if (!release.id) throw new Error("failed to find the release");
await uploadAssets(release.id, [
join(`dmg/Clash Verge_${version}_aarch64.dmg`),
...appPathList,
]);
}
// From tauri-apps/tauri-action
// https://github.com/tauri-apps/tauri-action/blob/dev/packages/action/src/upload-release-assets.ts
async function uploadAssets(releaseId, assets) {
const github = getOctokit(process.env.GITHUB_TOKEN);
// Determine content-length for header to upload asset
const contentLength = (filePath) => fs.statSync(filePath).size;
for (const assetPath of assets) {
const headers = {
"content-type": "application/zip",
"content-length": contentLength(assetPath),
};
const ext = path.extname(assetPath);
const filename = path.basename(assetPath).replace(ext, "");
const assetName = path.dirname(assetPath).includes(`target${path.sep}debug`)
? `${filename}-debug${ext}`
: `${filename}${ext}`;
console.log(`[INFO]: Uploading ${assetName}...`);
try {
await github.rest.repos.uploadReleaseAsset({
headers,
name: assetName,
data: fs.readFileSync(assetPath),
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
});
} catch (error) {
console.log(error.message);
}
}
}
if (process.platform === "darwin" && process.arch === "arm64") {
resolve();
} else {
console.error("invalid");
exit(1);
}
+93 -16
View File
@@ -13,23 +13,23 @@ const FORCE = process.argv.includes("--force");
const PLATFORM_MAP = {
"x86_64-pc-windows-msvc": "win32",
"i686-pc-windows-msvc": "win32",
"aarch64-pc-windows-msvc": "win32",
"x86_64-apple-darwin": "darwin",
"aarch64-apple-darwin": "darwin",
"x86_64-unknown-linux-gnu": "linux",
"aarch64-unknown-linux-gnu": "linux",
"armv7-unknown-linux-gnueabihf": "linux",
"loongarch64-unknown-linux-gnu": "linux",
};
const ARCH_MAP = {
"x86_64-pc-windows-msvc": "x64",
"i686-pc-windows-msvc": "ia32",
"aarch64-pc-windows-msvc": "arm64",
"x86_64-apple-darwin": "x64",
"aarch64-apple-darwin": "arm64",
"x86_64-unknown-linux-gnu": "x64",
"aarch64-unknown-linux-gnu": "arm64",
"armv7-unknown-linux-gnueabihf": "arm",
"loongarch64-unknown-linux-gnu": "loong64",
};
const arg1 = process.argv.slice(2)[0];
@@ -46,50 +46,93 @@ const SIDECAR_HOST = target
.match(/(?<=host: ).+(?=\s*)/g)[0];
/* ======= clash meta alpha======= */
const VERSION_URL =
const META_ALPHA_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
let META_ALPHA_VERSION;
const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64-compatible",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-loong64": "mihomo-linux-loong64",
};
// Fetch the latest release version from the version.txt file
async function getLatestVersion() {
// Fetch the latest alpha release version from the version.txt file
async function getLatestAlphaVersion() {
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
}
try {
const response = await fetch(VERSION_URL, { method: "GET" });
const response = await fetch(META_ALPHA_VERSION_URL, {
...options,
method: "GET",
});
let v = await response.text();
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest release version: ${META_ALPHA_VERSION}`);
console.log(`Latest alpha version: ${META_ALPHA_VERSION}`);
} catch (error) {
console.error("Error fetching latest release version:", error.message);
console.error("Error fetching latest alpha version:", error.message);
process.exit(1);
}
}
/* ======= clash meta stable ======= */
const META_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
let META_VERSION = "v1.17.0";
let META_VERSION;
const META_MAP = {
"win32-x64": "mihomo-windows-amd64-compatible",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-loong64": "mihomo-linux-loong64",
};
// Fetch the latest release version from the version.txt file
async function getLatestReleaseVersion() {
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
}
try {
const response = await fetch(META_VERSION_URL, {
...options,
method: "GET",
});
let v = await response.text();
META_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest release version: ${META_VERSION}`);
} catch (error) {
console.error("Error fetching latest release version:", error.message);
process.exit(1);
}
}
/*
* check available
*/
@@ -266,11 +309,43 @@ async function downloadFile(url, path) {
console.log(`[INFO]: download finished "${url}"`);
}
// SimpleSC.dll
const resolvePlugin = async () => {
const url =
"https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip";
const tempDir = path.join(TEMP_DIR, "SimpleSC");
const tempZip = path.join(
tempDir,
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip"
);
const tempDll = path.join(tempDir, "SimpleSC.dll");
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
await fs.mkdirp(pluginDir);
await fs.mkdirp(tempDir);
if (!FORCE && (await fs.pathExists(pluginPath))) return;
try {
if (!(await fs.pathExists(tempZip))) {
await downloadFile(url, tempZip);
}
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: "SimpleSC" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
await fs.copyFile(tempDll, pluginPath);
console.log(`[INFO]: "SimpleSC" unzip finished`);
} finally {
await fs.remove(tempDir);
}
};
/**
* main
*/
const SERVICE_URL =
"https://github.com/zzzgydi/clash-verge-service/releases/download/latest";
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () =>
resolveResource({
@@ -312,15 +387,17 @@ const tasks = [
// { name: "clash", func: resolveClash, retry: 5 },
{
name: "clash-meta-alpha",
func: () => getLatestVersion().then(() => resolveSidecar(clashMetaAlpha())),
func: () =>
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
retry: 5,
},
{
name: "clash-meta",
func: () => resolveSidecar(clashMeta()),
func: () =>
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5,
},
// { name: "wintun", func: resolveWintun, retry: 5, winOnly: true },
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5, winOnly: true },
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
+16 -5
View File
@@ -5,10 +5,11 @@ import { createRequire } from "module";
import { getOctokit, context } from "@actions/github";
const target = process.argv.slice(2)[0];
const alpha = process.argv.slice(2)[1];
const ARCH_MAP = {
"i686-pc-windows-msvc": "x86",
"x86_64-pc-windows-msvc": "x64",
"aarch64-pc-windows-msvc": "arm64",
};
/// Script for ci
@@ -31,7 +32,6 @@ async function resolvePortable() {
const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
// zip.addLocalFile(path.join(releaseDir, "clash.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-meta-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
@@ -53,14 +53,25 @@ async function resolvePortable() {
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
console.log("[INFO]: upload to ", process.env.TAG_NAME || `v${version}`);
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
console.log("[INFO]: upload to ", tag);
const { data: release } = await github.rest.repos.getReleaseByTag({
...options,
tag: process.env.TAG_NAME || `v${version}`,
tag,
});
let assets = release.assets.filter((x) => {
return x.name === zipFile;
});
if (assets.length > 0) {
let id = assets[0].id;
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: id,
});
}
console.log(release.name);
await github.rest.repos.uploadReleaseAsset({
-53
View File
@@ -1,53 +0,0 @@
import fs from "fs-extra";
import { createRequire } from "module";
import { execSync } from "child_process";
import { resolveUpdateLog } from "./updatelog.mjs";
const require = createRequire(import.meta.url);
// publish
async function resolvePublish() {
const flag = process.argv[2] ?? "patch";
const packageJson = require("../package.json");
const tauriJson = require("../src-tauri/tauri.conf.json");
let [a, b, c] = packageJson.version.split(".").map(Number);
if (flag === "major") {
a += 1;
b = 0;
c = 0;
} else if (flag === "minor") {
b += 1;
c = 0;
} else if (flag === "patch") {
c += 1;
} else throw new Error(`invalid flag "${flag}"`);
const nextVersion = `${a}.${b}.${c}`;
packageJson.version = nextVersion;
tauriJson.package.version = nextVersion;
// 发布更新前先写更新日志
const nextTag = `v${nextVersion}`;
await resolveUpdateLog(nextTag);
await fs.writeFile(
"./package.json",
JSON.stringify(packageJson, undefined, 2)
);
await fs.writeFile(
"./src-tauri/tauri.conf.json",
JSON.stringify(tauriJson, undefined, 2)
);
execSync("git add ./package.json");
execSync("git add ./src-tauri/tauri.conf.json");
execSync(`git commit -m "v${nextVersion}"`);
execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
execSync(`git push`);
execSync(`git push origin v${nextVersion}`);
console.log(`Publish Successfully...`);
}
resolvePublish();
+14 -13
View File
@@ -46,8 +46,9 @@ async function resolveUpdater() {
"darwin-x86_64": { signature: "", url: "" },
"linux-x86_64": { signature: "", url: "" },
"linux-aarch64": { signature: "", url: "" },
"linux-armv7": { signature: "", url: "" },
"windows-x86_64": { signature: "", url: "" },
"windows-i686": { signature: "", url: "" },
"windows-aarch64": { signature: "", url: "" },
},
};
@@ -66,14 +67,14 @@ async function resolveUpdater() {
updateData.platforms["windows-x86_64"].signature = sig;
}
// win32 url
if (name.endsWith("x86-setup.nsis.zip")) {
updateData.platforms["windows-i686"].url = browser_download_url;
// win arm url
if (name.endsWith("arm64-setup.nsis.zip")) {
updateData.platforms["windows-aarch64"].url = browser_download_url;
}
// win32 signature
if (name.endsWith("x86-setup.nsis.zip.sig")) {
// win arm signature
if (name.endsWith("arm64-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-i686"].signature = sig;
updateData.platforms["windows-aarch64"].signature = sig;
}
// darwin url (intel)
@@ -100,22 +101,22 @@ async function resolveUpdater() {
updateData.platforms["darwin-aarch64"].signature = sig;
}
// linux url
if (name.endsWith(".AppImage.tar.gz")) {
// linux x64 url
if (name.endsWith("amd64.AppImage.tar.gz")) {
updateData.platforms.linux.url = browser_download_url;
updateData.platforms["linux-x86_64"].url = browser_download_url;
// 暂时使用x64版本的url和sig,使得可以检查更新,但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].url = browser_download_url;
// updateData.platforms["linux-armv7"].url = browser_download_url;
updateData.platforms["linux-armv7"].url = browser_download_url;
}
// linux signature
if (name.endsWith(".AppImage.tar.gz.sig")) {
// linux x64 signature
if (name.endsWith("amd64.AppImage.tar.gz.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms.linux.signature = sig;
updateData.platforms["linux-x86_64"].signature = sig;
// 暂时使用x64版本的url和sig,使得可以检查更新,但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].signature = sig;
// updateData.platforms["linux-armv7"].signature = sig;
updateData.platforms["linux-armv7"].signature = sig;
}
});
+802 -520
View File
File diff suppressed because it is too large Load Diff
+16 -24
View File
@@ -1,9 +1,9 @@
[package]
name = "clash-verge"
version = "1.4.3"
version = "1.5.2"
description = "clash verge"
authors = ["zzzgydi"]
license = "GPL-3.0"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
edition = "2021"
@@ -14,7 +14,7 @@ tauri-build = { version = "1", features = [] }
[dependencies]
warp = "0.3"
which = "5.0.0"
which = "6.0.0"
anyhow = "1.0"
dirs = "5.0"
open = "5.0"
@@ -24,43 +24,35 @@ dunce = "1.0"
log4rs = "1"
nanoid = "0.4"
chrono = "0.4"
sysinfo = "0.29"
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
rquickjs = "0.3"
sysinfo = "0.30"
rquickjs = "0.3" # 高版本不支持 Linux aarch64
serde_json = "1.0"
serde_yaml = "0.9"
auto-launch = "0.5"
once_cell = "1.18"
port_scanner = "0.1.5"
delay_timer = "0.11"
delay_timer = "0.11.5"
parking_lot = "0.12"
percent-encoding = "2.3.1"
window-shadows = { version = "0.2" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
tauri = { version = "1.5", features = ["clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
window-vibrancy = { version = "0.4.3" }
window-shadows = { version = "0.2" }
percent-encoding = "2.3.1"
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
auto-launch = { git="https://github.com/zzzgydi/auto-launch", branch = "main" }
tauri = { version = "1.5", features = [ "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
[target.'cfg(windows)'.dependencies]
runas = "=1.0.0"
runas = "=1.0.0" # 高版本会返回错误 Status
deelevate = "0.2.0"
winreg = { version = "0.52", features = ["transactions"] }
windows-sys = { version = "0.52", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
[target.'cfg(windows)'.dependencies.tauri]
features = ["global-shortcut-all", "icon-png", "process-all", "shell-all", "system-tray", "updater", "window-all"]
[target.'cfg(linux)'.dependencies.tauri]
features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "native-tls-vendored", "reqwest-native-tls-vendored"]
winreg = "0.52.0"
[target.'cfg(target_os = "linux")'.dependencies]
#openssl
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = []
default-meta = []
[profile.release]
panic = "abort"
+17
View File
@@ -0,0 +1,17 @@
<?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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

+25 -5
View File
@@ -2,14 +2,14 @@ use crate::{
config::*,
core::*,
feat,
utils::{dirs, help},
utils::{dirs, help, resolve},
};
use crate::{ret_err, wrap_err};
use anyhow::{Context, Result};
use serde_yaml::Mapping;
use std::collections::{HashMap, VecDeque};
use sysproxy::Sysproxy;
use tauri::api;
type CmdResult<T = ()> = Result<T, String>;
#[tauri::command]
@@ -219,7 +219,7 @@ pub fn open_app_dir() -> CmdResult<()> {
#[tauri::command]
pub fn open_core_dir() -> CmdResult<()> {
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
let core_dir = core_dir.parent().ok_or(format!("failed to get core dir"))?;
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
wrap_err!(open::that(core_dir))
}
@@ -249,13 +249,33 @@ pub mod uwp {
pub async fn clash_api_get_proxy_delay(
name: String,
url: Option<String>,
timeout: i32,
) -> CmdResult<clash_api::DelayRes> {
match clash_api::get_proxy_delay(name, url).await {
match clash_api::get_proxy_delay(name, url, timeout).await {
Ok(res) => Ok(res),
Err(err) => Err(format!("{}", err.to_string())),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub fn get_portable_flag() -> CmdResult<bool> {
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
}
#[tauri::command]
pub async fn test_delay(url: String) -> CmdResult<u32> {
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
}
#[tauri::command]
pub fn exit_app(app_handle: tauri::AppHandle) {
let _ = resolve::save_window_size_position(&app_handle, true);
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
std::process::exit(0);
}
#[cfg(windows)]
pub mod service {
use super::*;
+88 -36
View File
@@ -12,50 +12,56 @@ pub struct IClashTemp(pub Mapping);
impl IClashTemp {
pub fn new() -> Self {
let template = Self::template();
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
Ok(map) => Self(Self::guard(map)),
Ok(mut map) => {
template.0.keys().for_each(|key| {
if !map.contains_key(key) {
map.insert(key.clone(), template.0.get(key).unwrap().clone());
}
});
Self(Self::guard(map))
}
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
template
}
}
}
pub fn template() -> Self {
let mut map = Mapping::new();
let mut tun = Mapping::new();
tun.insert("stack".into(), "gVisor".into());
tun.insert("device".into(), "Meta".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), true.into());
tun.insert("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53", "tcp://any:53"].into());
tun.insert("mtu".into(), 9000.into());
map.insert(
"mixed-port".into(),
match cfg!(feature = "default-meta") {
false => 7890.into(),
true => 7898.into(),
},
);
map.insert("mixed-port".into(), 7897.into());
map.insert("socks-port".into(), 7898.into());
map.insert("port".into(), 7899.into());
map.insert("log-level".into(), "info".into());
map.insert("allow-lan".into(), false.into());
map.insert("mode".into(), "rule".into());
map.insert(
"external-controller".into(),
match cfg!(feature = "default-meta") {
false => "127.0.0.1:9090".into(),
true => "127.0.0.1:9098".into(),
},
);
map.insert("external-controller".into(), "127.0.0.1:9097".into());
map.insert("secret".into(), "".into());
#[cfg(feature = "default-meta")]
map.insert("unified-delay".into(), true.into());
#[cfg(feature = "default-meta")]
map.insert("tcp-concurrent".into(), true.into());
// map.insert("ipv6".into(), false.into());
map.insert("tun".into(), tun.into());
Self(map)
}
fn guard(mut config: Mapping) -> Mapping {
let port = Self::guard_mixed_port(&config);
let mixed_port = Self::guard_mixed_port(&config);
let socks_port = Self::guard_socks_port(&config);
let port = Self::guard_port(&config);
let ctrl = Self::guard_server_ctrl(&config);
config.insert("mixed-port".into(), port.into());
config.insert("mixed-port".into(), mixed_port.into());
config.insert("socks-port".into(), socks_port.into());
config.insert("port".into(), port.into());
config.insert("external-controller".into(), ctrl.into());
config
}
@@ -78,12 +84,24 @@ impl IClashTemp {
Self::guard_mixed_port(&self.0)
}
#[allow(unused)]
pub fn get_socks_port(&self) -> u16 {
Self::guard_socks_port(&self.0)
}
#[allow(unused)]
pub fn get_port(&self) -> u16 {
Self::guard_port(&self.0)
}
pub fn get_client_info(&self) -> ClashInfo {
let config = &self.0;
ClashInfo {
port: Self::guard_mixed_port(&config),
server: Self::guard_client_ctrl(&config),
mixed_port: Self::guard_mixed_port(config),
socks_port: Self::guard_socks_port(config),
port: Self::guard_port(config),
server: Self::guard_client_ctrl(config),
secret: config.get("secret").and_then(|value| match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Bool(val_bool) => Some(val_bool.to_string()),
@@ -101,9 +119,39 @@ impl IClashTemp {
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7890);
.unwrap_or(7897);
if port == 0 {
port = 7890;
port = 7897;
}
port
}
pub fn guard_socks_port(config: &Mapping) -> u16 {
let mut port = config
.get("socks-port")
.and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7898);
if port == 0 {
port = 7898;
}
port
}
pub fn guard_port(config: &Mapping) -> u16 {
let mut port = config
.get("port")
.and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7899);
if port == 0 {
port = 7899;
}
port
}
@@ -115,7 +163,7 @@ impl IClashTemp {
Some(val_str) => {
let val_str = val_str.trim();
let val = match val_str.starts_with(":") {
let val = match val_str.starts_with(':') {
true => format!("127.0.0.1{val_str}"),
false => val_str.to_owned(),
};
@@ -126,7 +174,7 @@ impl IClashTemp {
}
None => None,
})
.unwrap_or("127.0.0.1:9090".into())
.unwrap_or("127.0.0.1:9097".into())
}
pub fn guard_client_ctrl(config: &Mapping) -> String {
@@ -138,7 +186,7 @@ impl IClashTemp {
}
socket.to_string()
}
Err(_) => "127.0.0.1:9090".into(),
Err(_) => "127.0.0.1:9097".into(),
}
}
}
@@ -146,6 +194,8 @@ impl IClashTemp {
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ClashInfo {
/// clash core port
pub mixed_port: u16,
pub socks_port: u16,
pub port: u16,
/// same as `external-controller`
pub server: String,
@@ -165,7 +215,9 @@ fn test_clash_info() {
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
ClashInfo {
port,
mixed_port: port,
socks_port: 7898,
port: 7899,
server: server.into(),
secret: None,
}
@@ -173,12 +225,12 @@ fn test_clash_info() {
assert_eq!(
IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(),
get_result(7890, "127.0.0.1:9090")
get_result(7897, "127.0.0.1:9097")
);
assert_eq!(get_case("", ""), get_result(7890, "127.0.0.1:9090"));
assert_eq!(get_case("", ""), get_result(7897, "127.0.0.1:9097"));
assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9090"));
assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9097"));
assert_eq!(
get_case(8888, "127.0.0.1:8888"),
@@ -187,7 +239,7 @@ fn test_clash_info() {
assert_eq!(
get_case(8888, " :98888 "),
get_result(8888, "127.0.0.1:9090")
get_result(8888, "127.0.0.1:9097")
);
assert_eq!(
@@ -212,7 +264,7 @@ fn test_clash_info() {
assert_eq!(
get_case(8888, "192.168.1.1:80800"),
get_result(8888, "127.0.0.1:9090")
get_result(8888, "127.0.0.1:9097")
);
}
+58 -66
View File
@@ -1,4 +1,4 @@
use crate::utils::{dirs, help, tmpl};
use crate::utils::{dirs, help, resolve::VERSION, tmpl};
use anyhow::{bail, Context, Result};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
@@ -8,7 +8,7 @@ use sysproxy::Sysproxy;
use super::Config;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PrfItem {
pub uid: Option<String>,
@@ -59,10 +59,10 @@ pub struct PrfSelected {
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
pub struct PrfExtra {
pub upload: usize,
pub download: usize,
pub total: usize,
pub expire: usize,
pub upload: u64,
pub download: u64,
pub total: u64,
pub expire: u64,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
@@ -96,25 +96,7 @@ impl PrfOption {
a.update_interval = b.update_interval.or(a.update_interval);
Some(a)
}
t @ _ => t.0.or(t.1),
}
}
}
impl Default for PrfItem {
fn default() -> Self {
PrfItem {
uid: None,
itype: None,
name: None,
desc: None,
file: None,
url: None,
selected: None,
extra: None,
updated: None,
option: None,
file_data: None,
t => t.0.or(t.1),
}
}
}
@@ -152,7 +134,7 @@ impl PrfItem {
let desc = item.desc.unwrap_or("".into());
PrfItem::from_script(name, desc)
}
typ @ _ => bail!("invalid profile item type \"{typ}\""),
typ => bail!("invalid profile item type \"{typ}\""),
}
}
@@ -188,7 +170,8 @@ impl PrfItem {
let opt_ref = option.as_ref();
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
let update_interval = opt_ref.and_then(|o| o.update_interval);
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
@@ -213,26 +196,26 @@ impl PrfItem {
}
// 使用系统代理
else if with_proxy {
match Sysproxy::get_system_proxy() {
Ok(p @ Sysproxy { enable: true, .. }) => {
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
_ => {}
};
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
}
let version = unsafe { dirs::APP_VERSION };
let version = format!("clash-verge/{version}");
let version = match VERSION.get() {
Some(v) => format!("clash-verge/v{}", v),
None => "clash-verge/unknown".to_string(),
};
builder = builder.user_agent(user_agent.unwrap_or(version));
let resp = builder.build()?.get(url).send().await?;
@@ -248,12 +231,11 @@ impl PrfItem {
let extra = match header.get("Subscription-Userinfo") {
Some(value) => {
let sub_info = value.to_str().unwrap_or("");
Some(PrfExtra {
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
download: help::parse_str(sub_info, "download=").unwrap_or(0),
total: help::parse_str(sub_info, "total=").unwrap_or(0),
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
download: help::parse_str(sub_info, "download").unwrap_or(0),
total: help::parse_str(sub_info, "total").unwrap_or(0),
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
})
}
None => None,
@@ -262,32 +244,42 @@ impl PrfItem {
// parse the Content-Disposition
let filename = match header.get("Content-Disposition") {
Some(value) => {
let filename = value.to_str().unwrap_or("");
match help::parse_str::<String>(filename, "filename=") {
Some(filename) => Some(filename),
None => match help::parse_str::<String>(filename, "filename*=") {
let filename = format!("{value:?}");
let filename = filename.trim_matches('"');
match help::parse_str::<String>(filename, "filename*") {
Some(filename) => {
let iter = percent_encoding::percent_decode(filename.as_bytes());
let filename = iter.decode_utf8().unwrap_or_default();
filename.split("''").last().map(|s| s.to_string())
}
None => match help::parse_str::<String>(filename, "filename") {
Some(filename) => {
let iter = percent_encoding::percent_decode(filename.as_bytes());
let filename = iter.decode_utf8().unwrap_or_default();
filename.split("''").last().map(|s| s.to_string())
let filename = filename.trim_matches('"');
Some(filename.to_string())
}
None => None,
},
}
}
None => None,
None => Some(
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
),
};
// parse the profile-update-interval
let option = match header.get("profile-update-interval") {
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
Ok(val) => Some(PrfOption {
update_interval: Some(val * 60), // hour -> min
..PrfOption::default()
}),
Err(_) => None,
let option = match update_interval {
Some(val) => Some(PrfOption {
update_interval: Some(val),
..PrfOption::default()
}),
None => match header.get("profile-update-interval") {
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
Ok(val) => Some(PrfOption {
update_interval: Some(val * 60), // hour -> min
..PrfOption::default()
}),
Err(_) => None,
},
None => None,
},
None => None,
};
let uid = help::get_uid("r");
+14 -25
View File
@@ -14,9 +14,6 @@ pub struct IProfiles {
/// same as PrfConfig.chain
pub chain: Option<Vec<String>>,
/// record valid fields for clash
pub valid: Option<Vec<String>>,
/// profile list
pub items: Option<Vec<PrfItem>>,
}
@@ -37,13 +34,13 @@ impl IProfiles {
profiles.items = Some(vec![]);
}
// compatible with the old old old version
profiles.items.as_mut().map(|items| {
if let Some(items) = profiles.items.as_mut() {
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d"));
}
}
});
}
profiles
}
Err(err) => {
@@ -55,12 +52,6 @@ impl IProfiles {
pub fn template() -> Self {
Self {
valid: Some(vec![
"dns".into(),
"sub-rules".into(),
"unified-delay".into(),
"tcp-concurrent".into(),
]),
items: Some(vec![]),
..Self::default()
}
@@ -93,10 +84,6 @@ impl IProfiles {
self.chain = Some(chain);
}
if let Some(valid) = patch.valid {
self.valid = Some(valid);
}
Ok(())
}
@@ -152,17 +139,19 @@ impl IProfiles {
self.items = Some(vec![]);
}
self.items.as_mut().map(|items| items.push(item));
if let Some(items) = self.items.as_mut() {
items.push(item)
}
self.save_file()
}
/// reorder items
pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
let mut items = self.items.take().unwrap_or(vec![]);
let mut items = self.items.take().unwrap_or_default();
let mut old_index = None;
let mut new_index = None;
for i in 0..items.len() {
for (i, _) in items.iter().enumerate() {
if items[i].uid == Some(active_id.clone()) {
old_index = Some(i);
}
@@ -182,7 +171,7 @@ impl IProfiles {
/// update the item value
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or(vec![]);
let mut items = self.items.take().unwrap_or_default();
for each in items.iter_mut() {
if each.uid == Some(uid.clone()) {
@@ -255,11 +244,11 @@ impl IProfiles {
let current = self.current.as_ref().unwrap_or(&uid);
let current = current.clone();
let mut items = self.items.take().unwrap_or(vec![]);
let mut items = self.items.take().unwrap_or_default();
let mut index = None;
// get the index
for i in 0..items.len() {
for (i, _) in items.iter().enumerate() {
if items[i].uid == Some(uid.clone()) {
index = Some(i);
break;
@@ -267,19 +256,19 @@ impl IProfiles {
}
if let Some(index) = index {
items.remove(index).file.map(|file| {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
});
}
}
// delete the original uid
if current == uid {
self.current = match items.len() > 0 {
self.current = match !items.is_empty() {
true => items[0].uid.clone(),
false => None,
};
@@ -299,7 +288,7 @@ impl IProfiles {
Some(file) => dirs::app_profiles_dir()?.join(file),
None => bail!("failed to get the file field"),
};
return Ok(help::read_merge_mapping(&file_path)?);
return help::read_merge_mapping(&file_path);
}
bail!("failed to find the current profile \"uid:{current}\"");
}
+17 -3
View File
@@ -1,7 +1,7 @@
use crate::enhance::field::use_keys;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use serde_yaml::{Mapping, Value};
use std::collections::HashMap;
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IRuntime {
pub config: Option<Mapping>,
@@ -16,7 +16,7 @@ impl IRuntime {
Self::default()
}
// 这里只更改 allow-lan | ipv6 | log-level
// 这里只更改 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"]
@@ -26,6 +26,20 @@ impl IRuntime {
config.insert(key.into(), value.clone());
}
});
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())
});
use_keys(&patch_tun).into_iter().for_each(|key| {
if let Some(value) = patch_tun.get(&key).to_owned() {
tun.insert(key.into(), value.clone());
}
});
config.insert("tun".into(), Value::from(tun));
}
}
}
+47 -19
View File
@@ -19,13 +19,17 @@ pub struct IVerge {
/// `light` or `dark` or `system`
pub theme_mode: Option<String>,
/// enable blur mode
/// maybe be able to set the alpha
pub theme_blur: Option<bool>,
/// tray click event
pub tray_event: Option<String>,
/// copy env type
pub env_type: Option<String>,
/// start page
pub start_page: Option<String>,
/// startup script path
pub startup_script: Option<String>,
/// enable traffic graph default is true
pub traffic_graph: Option<bool>,
@@ -77,8 +81,8 @@ pub struct IVerge {
/// 默认的延迟测试连接
pub default_latency_test: Option<String>,
/// 支持关闭字段过滤,避免meta的新字段都被过滤掉,默认为关闭
pub enable_clash_fields: Option<bool>,
/// 默认的延迟测试超时时间
pub default_latency_timeout: Option<i32>,
/// 是否使用内部的脚本支持,默认为真
pub enable_builtin_enhanced: Option<bool>,
@@ -86,6 +90,9 @@ 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>,
@@ -94,11 +101,27 @@ pub struct IVerge {
#[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>,
/// verge mixed port 用于覆盖 clash 的 mixed port
pub verge_mixed_port: Option<u16>,
pub verge_socks_port: Option<u16>,
pub verge_port: Option<u16>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IVergeTestItem {
pub uid: Option<String>,
pub name: Option<String>,
pub icon: Option<String>,
pub url: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -130,28 +153,27 @@ impl IVerge {
pub fn template() -> Self {
Self {
clash_core: match cfg!(feature = "default-meta") {
false => Some("clash".into()),
true => Some("clash-meta".into()),
},
language: match cfg!(feature = "default-meta") {
false => Some("en".into()),
true => Some("zh".into()),
},
clash_core: Some("clash-meta".into()),
language: Some("zh".into()),
theme_mode: Some("system".into()),
theme_blur: Some(false),
#[cfg(not(target_os = "windows"))]
env_type: Some("bash".into()),
#[cfg(target_os = "windows")]
env_type: Some("powershell".into()),
start_page: Some("/".into()),
traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_auto_launch: Some(false),
enable_silent_start: Some(false),
enable_system_proxy: Some(false),
enable_random_port: Some(false),
verge_mixed_port: Some(7890),
verge_mixed_port: Some(7897),
verge_socks_port: Some(7898),
verge_port: Some(7899),
enable_proxy_guard: Some(false),
proxy_guard_duration: Some(30),
auto_close_connection: Some(true),
enable_builtin_enhanced: Some(true),
enable_clash_fields: Some(false),
auto_log_clean: Some(3),
..Self::default()
}
@@ -176,8 +198,10 @@ impl IVerge {
patch!(app_log_level);
patch!(language);
patch!(theme_mode);
patch!(theme_blur);
patch!(tray_event);
patch!(env_type);
patch!(start_page);
patch!(startup_script);
patch!(traffic_graph);
patch!(enable_memory_usage);
@@ -187,6 +211,8 @@ impl IVerge {
patch!(enable_silent_start);
patch!(enable_random_port);
patch!(verge_mixed_port);
patch!(verge_socks_port);
patch!(verge_port);
patch!(enable_system_proxy);
patch!(enable_proxy_guard);
patch!(system_proxy_bypass);
@@ -199,11 +225,13 @@ impl IVerge {
patch!(auto_close_connection);
patch!(default_latency_test);
patch!(default_latency_timeout);
patch!(enable_builtin_enhanced);
patch!(proxy_layout_column);
patch!(enable_clash_fields);
patch!(test_list);
patch!(auto_log_clean);
patch!(window_size_position);
patch!(window_is_maximized);
}
/// 在初始化前尝试拿到单例端口的值
+12 -8
View File
@@ -20,7 +20,7 @@ pub async fn put_configs(path: &str) -> Result<()> {
match response.status().as_u16() {
204 => Ok(()),
status @ _ => {
status => {
bail!("failed to put configs with status \"{status}\"")
}
}
@@ -44,7 +44,11 @@ pub struct DelayRes {
/// GET /proxies/{name}/delay
/// 获取代理延迟
pub async fn get_proxy_delay(name: String, test_url: Option<String>) -> Result<DelayRes> {
pub async fn get_proxy_delay(
name: String,
test_url: Option<String>,
timeout: i32,
) -> Result<DelayRes> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/proxies/{name}/delay");
@@ -57,7 +61,7 @@ pub async fn get_proxy_delay(name: String, test_url: Option<String>) -> Result<D
let builder = client
.get(&url)
.headers(headers)
.query(&[("timeout", "10000"), ("url", &test_url)]);
.query(&[("timeout", &format!("{timeout}")), ("url", &test_url)]);
let response = builder.send().await?;
Ok(response.json::<DelayRes>().await?)
@@ -83,12 +87,12 @@ fn clash_client_info() -> Result<(String, HeaderMap)> {
/// 缩短clash的日志
pub fn parse_log(log: String) -> String {
if log.starts_with("time=") && log.len() > 33 {
return (&log[33..]).to_owned();
return (log[33..]).to_owned();
}
if log.len() > 9 {
return (&log[9..]).to_owned();
return (log[9..]).to_owned();
}
return log;
log
}
/// 缩短clash -t的错误输出
@@ -105,7 +109,7 @@ pub fn parse_check_output(log: String) -> String {
};
if mr > m {
return (&log[e..mr]).to_owned();
return (log[e..mr]).to_owned();
}
}
@@ -113,7 +117,7 @@ pub fn parse_check_output(log: String) -> String {
let r = log.find("path=").or(Some(log.len()));
if let (Some(l), Some(r)) = (l, r) {
return (&log[(l + 6)..(r - 1)]).to_owned();
return (log[(l + 6)..(r - 1)]).to_owned();
}
log
+6 -6
View File
@@ -5,7 +5,7 @@ use anyhow::{bail, Context, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{fs, io::Write, sync::Arc, time::Duration};
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
use sysinfo::{Pid, System};
use tauri::api::process::{Command, CommandChild, CommandEvent};
use tokio::time::sleep;
@@ -35,12 +35,12 @@ impl CoreManager {
.map(|pid| {
let mut system = System::new();
system.refresh_all();
system.process(Pid::from_u32(pid)).map(|proc| {
if let Some(proc) = system.process(Pid::from_u32(pid)) {
if proc.name().contains("clash") {
log::debug!(target: "app", "kill old clash process");
proc.kill();
}
});
}
});
tauri::async_runtime::spawn(async {
@@ -68,7 +68,7 @@ impl CoreManager {
if !output.status.success() {
let error = clash_api::parse_check_output(output.stdout.clone());
let error = match error.len() > 0 {
let error = match !error.is_empty() {
true => error,
false => output.stdout.clone(),
};
@@ -110,7 +110,7 @@ impl CoreManager {
use super::win_service;
// 服务模式
let enable = { Config::verge().latest().enable_service_mode.clone() };
let enable = { Config::verge().latest().enable_service_mode };
let enable = enable.unwrap_or(false);
*self.use_service_mode.lock() = enable;
@@ -259,7 +259,7 @@ impl CoreManager {
/// 切换核心
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
const CLASH_CORES: [&str; 3] = ["clash", "clash-meta", "clash-meta-alpha"];
const CLASH_CORES: [&str; 2] = ["clash-meta", "clash-meta-alpha"];
if !CLASH_CORES.contains(&clash_core.as_str()) {
bail!("invalid clash core name \"{clash_core}\"");
+1 -1
View File
@@ -28,7 +28,7 @@ impl Handle {
self.app_handle
.lock()
.as_ref()
.map_or(None, |a| a.get_window("main"))
.and_then(|a| a.get_window("main"))
}
pub fn refresh_clash() {
+9 -9
View File
@@ -65,17 +65,17 @@ impl Hotkey {
}
let f = match func.trim() {
"open_dashboard" => || feat::open_dashboard(),
"open_dashboard" => feat::open_dashboard,
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
"clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
"clash_mode_script" => || feat::change_clash_mode("script".into()),
"toggle_system_proxy" => || feat::toggle_system_proxy(),
"enable_system_proxy" => || feat::enable_system_proxy(),
"disable_system_proxy" => || feat::disable_system_proxy(),
"toggle_tun_mode" => || feat::toggle_tun_mode(),
"enable_tun_mode" => || feat::enable_tun_mode(),
"disable_tun_mode" => || feat::disable_tun_mode(),
"toggle_system_proxy" => feat::toggle_system_proxy,
"enable_system_proxy" => feat::enable_system_proxy,
"disable_system_proxy" => feat::disable_system_proxy,
"toggle_tun_mode" => feat::toggle_tun_mode,
"enable_tun_mode" => feat::enable_tun_mode,
"disable_tun_mode" => feat::disable_tun_mode,
_ => bail!("invalid function \"{func}\""),
};
@@ -86,7 +86,7 @@ impl Hotkey {
}
fn unregister(&self, hotkey: &str) -> Result<()> {
self.get_manager()?.unregister(&hotkey)?;
self.get_manager()?.unregister(hotkey)?;
log::info!(target: "app", "unregister hotkey {hotkey}");
Ok(())
}
@@ -110,7 +110,7 @@ impl Hotkey {
Ok(())
}
fn get_map_from_vec<'a>(hotkeys: &'a Vec<String>) -> HashMap<&'a str, &'a str> {
fn get_map_from_vec(hotkeys: &Vec<String>) -> HashMap<&str, &str> {
let mut map = HashMap::new();
hotkeys.iter().for_each(|hotkey| {
-35
View File
@@ -1,5 +1,3 @@
use std::borrow::Cow;
/// 给clash内核的tun模式授权
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn grant_permission(core: String) -> anyhow::Result<()> {
@@ -13,9 +11,6 @@ pub fn grant_permission(core: String) -> anyhow::Result<()> {
#[cfg(target_os = "macos")]
let output = {
// the path of clash /Applications/Clash Verge.app/Contents/MacOS/clash
// https://apple.stackexchange.com/questions/82967/problem-with-empty-spaces-when-executing-shell-commands-in-applescript
// let path = escape(&path);
let path = path.replace(' ', "\\\\ ");
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
@@ -50,33 +45,3 @@ pub fn grant_permission(core: String) -> anyhow::Result<()> {
anyhow::bail!("{stderr}");
}
}
#[allow(unused)]
pub fn escape<'a>(text: &'a str) -> Cow<'a, str> {
let bytes = text.as_bytes();
let mut owned = None;
for pos in 0..bytes.len() {
let special = match bytes[pos] {
b' ' => Some(b' '),
_ => None,
};
if let Some(s) = special {
if owned.is_none() {
owned = Some(bytes[0..pos].to_owned());
}
owned.as_mut().unwrap().push(b'\\');
owned.as_mut().unwrap().push(b'\\');
owned.as_mut().unwrap().push(s);
} else if let Some(owned) = owned.as_mut() {
owned.push(bytes[pos]);
}
}
if let Some(owned) = owned {
unsafe { Cow::Owned(String::from_utf8_unchecked(owned)) }
} else {
unsafe { Cow::Borrowed(std::str::from_utf8_unchecked(bytes)) }
}
}
+14 -8
View File
@@ -53,7 +53,7 @@ impl Sysopt {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.clone().unwrap_or(false),
verge.enable_system_proxy.unwrap_or(false),
verge.system_proxy_bypass.clone(),
)
};
@@ -66,7 +66,7 @@ impl Sysopt {
};
if enable {
let old = Sysproxy::get_system_proxy().map_or(None, |p| Some(p));
let old = Sysproxy::get_system_proxy().ok();
current.set_system_proxy()?;
*self.old_sysproxy.lock() = old;
@@ -93,7 +93,7 @@ impl Sysopt {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.clone().unwrap_or(false),
verge.enable_system_proxy.unwrap_or(false),
verge.system_proxy_bypass.clone(),
)
};
@@ -102,6 +102,12 @@ impl Sysopt {
sysproxy.enable = enable;
sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
sysproxy.port = port;
sysproxy.set_system_proxy()?;
*cur_sysproxy = Some(sysproxy);
@@ -142,7 +148,7 @@ impl Sysopt {
/// init the auto launch
pub fn init_launch(&self) -> Result<()> {
let enable = { Config::verge().latest().enable_auto_launch.clone() };
let enable = { Config::verge().latest().enable_auto_launch };
let enable = enable.unwrap_or(false);
let app_exe = current_exe()?;
@@ -233,7 +239,7 @@ impl Sysopt {
drop(auto_launch);
return self.init_launch();
}
let enable = { Config::verge().latest().enable_auto_launch.clone() };
let enable = { Config::verge().latest().enable_auto_launch };
let enable = enable.unwrap_or(false);
let auto_launch = auto_launch.as_ref().unwrap();
@@ -271,9 +277,9 @@ impl Sysopt {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.clone().unwrap_or(false),
verge.enable_proxy_guard.clone().unwrap_or(false),
verge.proxy_guard_duration.clone().unwrap_or(10),
verge.enable_system_proxy.unwrap_or(false),
verge.enable_proxy_guard.unwrap_or(false),
verge.proxy_guard_duration.unwrap_or(10),
verge.system_proxy_bypass.clone(),
)
};
+2 -2
View File
@@ -40,7 +40,7 @@ impl Timer {
let timer_map = self.timer_map.lock();
let delay_timer = self.delay_timer.lock();
Config::profiles().latest().get_items().map(|items| {
if let Some(items) = Config::profiles().latest().get_items() {
items
.iter()
.filter_map(|item| {
@@ -61,7 +61,7 @@ impl Timer {
}
}
})
});
}
Ok(())
}
+26 -43
View File
@@ -41,10 +41,6 @@ impl Tray {
"direct_mode",
t!("Direct Mode", "直连模式"),
))
.add_item(CustomMenuItem::new(
"script_mode",
t!("Script Mode", "脚本模式"),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new(
"system_proxy",
@@ -52,16 +48,8 @@ impl Tray {
))
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
.add_item(CustomMenuItem::new(
"copy_env_sh",
t!("Copy Env (sh)", "复制环境变量(sh)"),
))
.add_item(CustomMenuItem::new(
"copy_env_cmd",
t!("Copy Env (CMD)", "复制环境变量(CMD)"),
))
.add_item(CustomMenuItem::new(
"copy_env_ps",
t!("Copy Env (PS)", "复制环境变量(PS)"),
"copy_env",
t!("Copy Env", "复制环境变量"),
))
.add_submenu(SystemTraySubmenu::new(
t!("Open Dir", "打开目录"),
@@ -136,28 +124,36 @@ impl Tray {
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
let _ = tray.get_item("global_mode").set_selected(mode == "global");
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
let _ = tray.get_item("script_mode").set_selected(mode == "script");
let verge = Config::verge();
let verge = verge.latest();
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 = "windows")]
{
let mut indication_icon = if *system_proxy {
include_bytes!("../../icons/win-tray-icon-activated.png").to_vec()
} else {
include_bytes!("../../icons/win-tray-icon.png").to_vec()
};
let mut indication_icon = if *system_proxy {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon-sys.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
icon
} else {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
icon
};
if *tun_mode {
indication_icon = include_bytes!("../../icons/win-tray-icon-tun.png").to_vec();
}
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
if *tun_mode {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon-tun.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
indication_icon = icon
}
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
@@ -168,7 +164,6 @@ impl Tray {
map
};
#[cfg(not(target_os = "linux"))]
let _ = tray.set_tooltip(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}",
t!("System Proxy", "系统代理"),
@@ -193,35 +188,23 @@ impl Tray {
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
match event {
#[cfg(not(target_os = "linux"))]
SystemTrayEvent::LeftClick { .. } => Tray::on_left_click(app_handle),
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5];
feat::change_clash_mode(mode.into());
}
"open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"copy_env_sh" => feat::copy_clash_env(app_handle, "sh"),
#[cfg(target_os = "windows")]
"copy_env_cmd" => feat::copy_clash_env(app_handle, "cmd"),
#[cfg(target_os = "windows")]
"copy_env_ps" => feat::copy_clash_env(app_handle, "ps"),
"copy_env" => feat::copy_clash_env(app_handle),
"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" => api::process::restart(&app_handle.env()),
"quit" => {
let _ = resolve::save_window_size_position(app_handle, true);
"quit" => cmds::exit_app(app_handle.clone()),
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
std::process::exit(0);
}
_ => {}
},
_ => {}
+4 -5
View File
@@ -7,8 +7,8 @@ use runas::Command as RunasCommand;
use std::process::Command as StdCommand;
pub async fn invoke_uwptools() -> Result<()> {
let binary_path = dirs::service_path()?;
let tool_path = binary_path.with_file_name("enableLoopback.exe");
let resource_dir = dirs::app_resources_dir()?;
let tool_path = resource_dir.join("enableLoopback.exe");
if !tool_path.exists() {
bail!("enableLoopback exe not found");
@@ -17,10 +17,9 @@ pub async fn invoke_uwptools() -> Result<()> {
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
match level {
match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
_ => StdCommand::new(tool_path)
.status()?,
_ => StdCommand::new(tool_path).status()?,
};
Ok(())
+31 -94
View File
@@ -3,9 +3,9 @@ use std::collections::HashSet;
pub const HANDLE_FIELDS: [&str; 9] = [
"mode",
"port",
"socks-port",
"mixed-port",
"socks-port",
"port",
"allow-lan",
"log-level",
"ipv6",
@@ -15,71 +15,13 @@ pub const HANDLE_FIELDS: [&str; 9] = [
pub const DEFAULT_FIELDS: [&str; 5] = [
"proxies",
"proxy-groups",
"proxy-providers",
"rules",
"proxy-groups",
"rule-providers",
"rules",
];
pub const OTHERS_FIELDS: [&str; 31] = [
"dns",
"tun",
"ebpf",
"hosts",
"script",
"profile",
"payload",
"tunnels",
"auto-redir",
"experimental",
"interface-name",
"routing-mark",
"redir-port",
"tproxy-port",
"iptables",
"external-ui",
"bind-address",
"authentication",
"tls", // meta
"sniffer", // meta
"geox-url", // meta
"listeners", // meta
"sub-rules", // meta
"geodata-mode", // meta
"unified-delay", // meta
"tcp-concurrent", // meta
"enable-process", // meta
"find-process-mode", // meta
"skip-auth-prefixes", // meta
"external-controller-tls", // meta
"global-client-fingerprint", // meta
];
pub fn use_clash_fields() -> Vec<String> {
DEFAULT_FIELDS
.into_iter()
.chain(HANDLE_FIELDS)
.chain(OTHERS_FIELDS)
.map(|s| s.to_string())
.collect()
}
pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> {
let others = Vec::from(OTHERS_FIELDS);
valid.iter_mut().for_each(|s| s.make_ascii_lowercase());
valid
.into_iter()
.filter(|s| others.contains(&s.as_str()))
.chain(DEFAULT_FIELDS.iter().map(|s| s.to_string()))
.collect()
}
pub fn use_filter(config: Mapping, filter: &Vec<String>, enable: bool) -> Mapping {
if !enable {
return config;
}
pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping {
let mut ret = Mapping::new();
for (key, value) in config.into_iter() {
@@ -105,40 +47,35 @@ pub fn use_lowercase(config: Mapping) -> Mapping {
ret
}
pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
pub fn use_sort(config: Mapping) -> Mapping {
let mut ret = Mapping::new();
HANDLE_FIELDS.into_iter().for_each(|key| {
let key = Value::from(key);
if let Some(value) = config.get(&key) {
ret.insert(key, value.clone());
}
});
HANDLE_FIELDS
let supported_keys: HashSet<&str> = HANDLE_FIELDS.into_iter().chain(DEFAULT_FIELDS).collect();
let config_keys: HashSet<&str> = config
.keys()
.filter_map(|e| e.as_str())
.into_iter()
.chain(OTHERS_FIELDS)
.chain(DEFAULT_FIELDS)
.for_each(|key| {
let key = Value::from(key);
config.get(&key).map(|value| {
ret.insert(key, value.clone());
});
});
.collect();
if !enable_filter {
let supported_keys: HashSet<&str> = HANDLE_FIELDS
.into_iter()
.chain(OTHERS_FIELDS)
.chain(DEFAULT_FIELDS)
.collect();
let config_keys: HashSet<&str> = config
.keys()
.filter_map(|e| e.as_str())
.into_iter()
.collect();
config_keys.difference(&supported_keys).for_each(|&key| {
let key = Value::from(key);
config.get(&key).map(|value| {
ret.insert(key, value.clone());
});
});
}
config_keys.difference(&supported_keys).for_each(|&key| {
let key = Value::from(key);
if let Some(value) = config.get(&key) {
ret.insert(key, value.clone());
}
});
DEFAULT_FIELDS.into_iter().for_each(|key| {
let key = Value::from(key);
if let Some(value) = config.get(&key) {
ret.insert(key, value.clone());
}
});
ret
}
@@ -150,7 +87,7 @@ pub fn use_keys(config: &Mapping) -> Vec<String> {
.map(|s| {
let mut s = s.to_string();
s.make_ascii_lowercase();
return s;
s
})
.collect()
}
+2 -1
View File
@@ -14,12 +14,13 @@ pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
// 直接覆盖原字段
use_lowercase(merge.clone())
.into_iter()
.filter(|(key, _)| !MERGE_FIELDS.contains(&key.as_str().unwrap_or_default()))
.for_each(|(key, value)| {
config.insert(key, value);
});
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
let merge = use_filter(merge, &merge_list.collect(), true);
let merge = use_filter(merge, &merge_list.collect());
["rules", "proxies", "proxy-groups"]
.iter()
+25 -25
View File
@@ -1,12 +1,11 @@
mod chain;
mod field;
pub mod field;
mod merge;
mod script;
mod tun;
pub(self) use self::field::*;
use self::chain::*;
use self::field::*;
use self::merge::*;
use self::script::*;
use self::tun::*;
@@ -23,50 +22,43 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的订阅
let clash_config = { Config::clash().latest().0.clone() };
let (clash_core, enable_tun, enable_builtin, enable_filter) = {
let (clash_core, enable_tun, enable_builtin) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.clash_core.clone(),
verge.enable_tun_mode.clone().unwrap_or(false),
verge.enable_builtin_enhanced.clone().unwrap_or(true),
verge.enable_clash_fields.clone().unwrap_or(true),
verge.enable_tun_mode.unwrap_or(false),
verge.enable_builtin_enhanced.unwrap_or(true),
)
};
// 从profiles里拿东西
let (mut config, chain, valid) = {
let (mut config, chain) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let current = profiles.current_mapping().unwrap_or(Mapping::new());
let current = profiles.current_mapping().unwrap_or_default();
let chain = match profiles.chain.as_ref() {
Some(chain) => chain
.iter()
.filter_map(|uid| profiles.get_item(uid).ok())
.filter_map(|item| <Option<ChainItem>>::from(item))
.filter_map(<Option<ChainItem>>::from)
.collect::<Vec<ChainItem>>(),
None => vec![],
};
let valid = profiles.valid.clone().unwrap_or(vec![]);
(current, chain, valid)
(current, chain)
};
let mut result_map = HashMap::new(); // 保存脚本日志
let mut exists_keys = use_keys(&config); // 保存出现过的keys
let valid = use_valid_fields(valid);
config = use_filter(config, &valid, enable_filter);
// 处理用户的profile
chain.into_iter().for_each(|item| match item.data {
ChainType::Merge(merge) => {
exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned());
config = use_filter(config.to_owned(), &valid, enable_filter);
}
ChainType::Script(script) => {
let mut logs = vec![];
@@ -74,7 +66,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
match use_script(script, config.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = use_filter(res_config, &valid, enable_filter);
config = res_config;
logs.extend(res_logs);
}
Err(err) => logs.push(("exception".into(), err.to_string())),
@@ -86,11 +78,20 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// 合并默认的config
for (key, value) in clash_config.into_iter() {
config.insert(key, value);
if key.as_str() == Some("tun") {
let mut tun = config.get_mut("tun").map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new());
for (key, value) in patch_tun.into_iter() {
tun.insert(key, value);
}
config.insert("tun".into(), tun.into());
} else {
config.insert(key, value);
}
}
let clash_fields = use_clash_fields();
// 内建脚本最后跑
if enable_builtin {
ChainItem::builtin()
@@ -103,7 +104,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
match item.data {
ChainType::Script(script) => match use_script(script, config.to_owned()) {
Ok((res_config, _)) => {
config = use_filter(res_config, &clash_fields, enable_filter);
config = res_config;
}
Err(err) => {
log::error!(target: "app", "builtin script error `{err}`");
@@ -114,12 +115,11 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
});
}
config = use_filter(config, &clash_fields, enable_filter);
config = use_tun(config, enable_tun);
config = use_sort(config, enable_filter);
config = use_sort(config);
let mut exists_set = HashSet::new();
exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));
exists_set.extend(exists_keys.into_iter());
exists_keys = exists_set.into_iter().collect();
(config, exists_keys, result_map)
+1 -1
View File
@@ -47,7 +47,7 @@ pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(Stri
if result == "\"\"" {
anyhow::bail!("main function should return object");
}
return Ok(serde_json::from_str::<Mapping>(result.as_str())?);
Ok(serde_json::from_str::<Mapping>(result.as_str())?)
});
let mut out = outputs.lock().unwrap();
-6
View File
@@ -30,12 +30,6 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
});
revise!(tun_val, "enable", enable);
if enable {
append!(tun_val, "stack", "gvisor");
append!(tun_val, "dns-hijack", vec!["any:53"]);
append!(tun_val, "auto-route", true);
append!(tun_val, "auto-detect-interface", true);
}
revise!(config, "tun", tun_val);
+68 -11
View File
@@ -62,7 +62,7 @@ pub fn change_clash_mode(mode: String) {
// 切换系统代理
pub fn toggle_system_proxy() {
let enable = Config::verge().draft().enable_system_proxy.clone();
let enable = Config::verge().draft().enable_system_proxy;
let enable = enable.unwrap_or(false);
tauri::async_runtime::spawn(async move {
@@ -110,7 +110,7 @@ pub fn disable_system_proxy() {
// 切换tun模式
pub fn toggle_tun_mode() {
let enable = Config::verge().data().enable_tun_mode.clone();
let enable = Config::verge().data().enable_tun_mode;
let enable = enable.unwrap_or(false);
tauri::async_runtime::spawn(async move {
@@ -162,16 +162,18 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
match {
let mixed_port = patch.get("mixed-port");
let socks_port = patch.get("socks-port");
let port = patch.get("port");
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
if mixed_port.is_some() && !enable_random_port {
let changed = mixed_port.clone().unwrap()
let changed = mixed_port.unwrap()
!= Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
// 检查端口占用
if changed {
if let Some(port) = mixed_port.clone().unwrap().as_u64() {
if let Some(port) = mixed_port.unwrap().as_u64() {
if !port_scanner::local_port_available(port as u16) {
Config::clash().discard();
bail!("port already in use");
@@ -182,6 +184,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
// 激活订阅
if mixed_port.is_some()
|| socks_port.is_some()
|| port.is_some()
|| patch.get("secret").is_some()
|| patch.get("external-controller").is_some()
{
@@ -225,6 +229,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let system_proxy = patch.enable_system_proxy;
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let port = patch.verge_mixed_port;
match {
#[cfg(target_os = "windows")]
@@ -249,7 +254,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
if auto_launch.is_some() {
sysopt::Sysopt::global().update_launch()?;
}
if system_proxy.is_some() || proxy_bypass.is_some() {
if system_proxy.is_some() || proxy_bypass.is_some() || port.is_some() {
sysopt::Sysopt::global().update_sysproxy()?;
sysopt::Sysopt::global().guard_proxy();
}
@@ -337,8 +342,8 @@ async fn update_core_config() -> Result<()> {
}
/// copy env variable
pub fn copy_clash_env(app_handle: &AppHandle, option: &str) {
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7890) };
pub fn copy_clash_env(app_handle: &AppHandle) {
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);
@@ -346,12 +351,64 @@ pub fn copy_clash_env(app_handle: &AppHandle, option: &str) {
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}");
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
let mut cliboard = app_handle.clipboard_manager();
match option {
"sh" => cliboard.write_text(sh).unwrap_or_default(),
let env_type = { Config::verge().latest().env_type.clone() };
let env_type = match env_type {
Some(env_type) => env_type,
None => {
#[cfg(not(target_os = "windows"))]
let default = "bash";
#[cfg(target_os = "windows")]
let default = "powershell";
default.to_string()
}
};
match env_type.as_str() {
"bash" => cliboard.write_text(sh).unwrap_or_default(),
"cmd" => cliboard.write_text(cmd).unwrap_or_default(),
"ps" => cliboard.write_text(ps).unwrap_or_default(),
_ => log::error!(target: "app", "copy_clash_env: Invalid option! {option}"),
"powershell" => cliboard.write_text(ps).unwrap_or_default(),
_ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
};
}
pub async fn test_delay(url: String) -> Result<u32> {
use tokio::time::{Duration, Instant};
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
let proxy_scheme = format!("http://127.0.0.1:{port}");
if !tun_mode {
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
let request = builder
.timeout(Duration::from_millis(10000))
.build()?
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
let start = Instant::now();
let response = request.send().await?;
if response.status().is_success() {
let delay = start.elapsed().as_millis() as u32;
Ok(delay)
} else {
Ok(10000u32)
}
}
+11 -26
View File
@@ -25,7 +25,10 @@ fn main() -> std::io::Result<()> {
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.system_tray(SystemTray::new())
.setup(|app| Ok(resolve::resolve_setup(app)))
.setup(|app| {
resolve::resolve_setup(app);
Ok(())
})
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
.invoke_handler(tauri::generate_handler![
// common
@@ -34,6 +37,7 @@ fn main() -> std::io::Result<()> {
cmds::open_logs_dir,
cmds::open_web_url,
cmds::open_core_dir,
cmds::get_portable_flag,
// cmds::kill_sidecar,
cmds::restart_sidecar,
cmds::grant_permission,
@@ -50,6 +54,8 @@ fn main() -> std::io::Result<()> {
// verge
cmds::get_verge_config,
cmds::patch_verge_config,
cmds::test_delay,
cmds::exit_app,
// cmds::update_hotkeys,
// profile
cmds::get_profiles,
@@ -100,42 +106,21 @@ fn main() -> std::io::Result<()> {
tauri::RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
}
tauri::RunEvent::Exit => {
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
}
tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
resolve::resolve_reset();
api::process::kill_children();
}
#[cfg(target_os = "macos")]
tauri::RunEvent::WindowEvent { label, event, .. } => {
use tauri::Manager;
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
api.prevent_close();
let _ = resolve::save_window_size_position(&app_handle, true);
app_handle.get_window("main").map(|win| {
let _ = win.hide();
});
tauri::WindowEvent::Destroyed => {
let _ = resolve::save_window_size_position(app_handle, true);
}
_ => {}
}
}
}
#[cfg(not(target_os = "macos"))]
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { .. } => {
let _ = resolve::save_window_size_position(&app_handle, true);
let _ = resolve::save_window_size_position(app_handle, true);
}
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
let _ = resolve::save_window_size_position(&app_handle, false);
let _ = resolve::save_window_size_position(app_handle, false);
}
_ => {}
}
+36 -72
View File
@@ -1,86 +1,69 @@
use crate::core::handle;
use anyhow::Result;
use once_cell::sync::OnceCell;
use std::path::PathBuf;
use tauri::{
api::path::{data_dir, resource_dir},
Env, PackageInfo,
Env,
};
#[cfg(not(feature = "verge-dev"))]
static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
#[cfg(feature = "verge-dev")]
static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.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";
static mut RESOURCE_DIR: Option<PathBuf> = None;
/// portable flag
#[allow(unused)]
static mut PORTABLE_FLAG: bool = false;
pub static mut APP_VERSION: &str = "v1.2.0";
/// initialize portable flag
#[cfg(target_os = "windows")]
pub unsafe fn init_portable_flag() -> Result<()> {
/// init portable flag
pub fn init_portable_flag() -> Result<()> {
use tauri::utils::platform::current_exe;
let exe = current_exe()?;
if let Some(dir) = exe.parent() {
let app_exe = current_exe()?;
if let Some(dir) = app_exe.parent() {
let dir = PathBuf::from(dir).join(".config/PORTABLE");
if dir.exists() {
PORTABLE_FLAG = true;
PORTABLE_FLAG.get_or_init(|| true);
}
}
PORTABLE_FLAG.get_or_init(|| false);
Ok(())
}
/// get the verge app home dir
pub fn app_home_dir() -> Result<PathBuf> {
#[cfg(target_os = "windows")]
unsafe {
use tauri::utils::platform::current_exe;
use tauri::utils::platform::current_exe;
if !PORTABLE_FLAG {
Ok(data_dir()
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
.join(APP_ID))
} else {
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_dir = app_exe
.parent()
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
Ok(PathBuf::from(app_dir).join(".config").join(APP_ID))
}
let flag = PORTABLE_FLAG.get().unwrap_or(&false);
if *flag {
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_dir = app_exe
.parent()
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID));
}
#[cfg(not(target_os = "windows"))]
Ok(data_dir()
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
.join("io.github.clash_verge_rev.clash_verge_rev"))
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
.join(APP_ID))
}
/// get the resources dir
pub fn app_resources_dir(package_info: &PackageInfo) -> Result<PathBuf> {
let res_dir = resource_dir(package_info, &Env::default())
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
.join("resources");
unsafe {
RESOURCE_DIR = Some(res_dir.clone());
let ver = package_info.version.to_string();
let ver_str = format!("v{ver}");
APP_VERSION = Box::leak(Box::new(ver_str));
}
Ok(res_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 res_dir = resource_dir(app_handle.package_info(), &Env::default())
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
.join("resources");
return Ok(res_dir);
};
Err(anyhow::anyhow!("failed to get the resource dir"))
}
/// profiles dir
@@ -105,32 +88,13 @@ pub fn profiles_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join(PROFILE_YAML))
}
#[allow(unused)]
pub fn app_res_dir() -> Result<PathBuf> {
unsafe {
Ok(RESOURCE_DIR
.clone()
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?)
}
}
pub fn clash_pid_path() -> Result<PathBuf> {
unsafe {
Ok(RESOURCE_DIR
.clone()
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
.join("clash.pid"))
}
Ok(app_home_dir()?.join("clash.pid"))
}
#[cfg(windows)]
pub fn service_path() -> Result<PathBuf> {
unsafe {
let res_dir = RESOURCE_DIR
.clone()
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?;
Ok(res_dir.join("clash-verge-service.exe"))
}
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
}
#[cfg(windows)]
+30 -20
View File
@@ -14,7 +14,7 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
bail!("file not found \"{}\"", path.display());
}
let yaml_str = fs::read_to_string(&path)
let yaml_str = fs::read_to_string(path)
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
@@ -71,18 +71,28 @@ pub fn get_uid(prefix: &str) -> String {
/// parse the string
/// xxx=123123; => 123123
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
target.find(key).and_then(|idx| {
let idx = idx + key.len();
let value = &target[idx..];
match value.split(';').nth(0) {
Some(value) => value.trim().parse(),
None => value.trim().parse(),
target.split(';').map(str::trim).find_map(|s| {
let mut parts = s.splitn(2, '=');
match (parts.next(), parts.next()) {
(Some(k), Some(v)) if k == key => v.parse::<T>().ok(),
_ => None,
}
.ok()
})
}
/// get the last part of the url, if not found, return empty string
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
let path = url.split('?').next().unwrap_or(""); // Splits URL and takes the path part
let segments: Vec<&str> = path.split('/').collect();
let last_segment = segments.last()?;
Some(
percent_encoding::percent_decode_str(last_segment)
.decode_utf8_lossy()
.to_string(),
)
}
/// open file
/// use vscode by default
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
@@ -92,11 +102,11 @@ pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
let code = "code";
let _ = match Program::from_str(code) {
Ok(code) => open(&app.shell_scope(), &path.to_string_lossy(), Some(code)),
Ok(code) => open(&app.shell_scope(), path.to_string_lossy(), Some(code)),
Err(err) => {
log::error!(target: "app", "Can't find VScode `{err}`");
// default open
open(&app.shell_scope(), &path.to_string_lossy(), None)
open(&app.shell_scope(), path.to_string_lossy(), None)
}
};
@@ -162,17 +172,17 @@ fn test_parse_value() {
let test_1 = "upload=111; download=2222; total=3333; expire=444";
let test_2 = "attachment; filename=Clash.yaml";
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222);
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
assert_eq!(parse_str::<usize>(test_1, "upload").unwrap(), 111);
assert_eq!(parse_str::<usize>(test_1, "download").unwrap(), 2222);
assert_eq!(parse_str::<usize>(test_1, "total").unwrap(), 3333);
assert_eq!(parse_str::<usize>(test_1, "expire").unwrap(), 444);
assert_eq!(
parse_str::<String>(test_2, "filename=").unwrap(),
parse_str::<String>(test_2, "filename").unwrap(),
format!("Clash.yaml")
);
assert_eq!(parse_str::<usize>(test_1, "aaa="), None);
assert_eq!(parse_str::<usize>(test_1, "upload1="), None);
assert_eq!(parse_str::<usize>(test_1, "expire1="), None);
assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
assert_eq!(parse_str::<usize>(test_1, "aaa"), None);
assert_eq!(parse_str::<usize>(test_1, "upload1"), None);
assert_eq!(parse_str::<usize>(test_1, "expire1"), None);
assert_eq!(parse_str::<usize>(test_2, "attachment"), None);
}
+95 -14
View File
@@ -8,8 +8,9 @@ use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Logger, Root};
use log4rs::encode::pattern::PatternEncoder;
use std::fs::{self, DirEntry};
use std::path::PathBuf;
use std::str::FromStr;
use tauri::PackageInfo;
use tauri::api::process::Command;
/// initialize this instance's log file
fn init_log() -> Result<()> {
@@ -80,7 +81,7 @@ pub fn delete_log() -> Result<()> {
let auto_log_clean = {
let verge = Config::verge();
let verge = verge.data();
verge.auto_log_clean.clone().unwrap_or(0)
verge.auto_log_clean.unwrap_or(0)
};
let day = match auto_log_clean {
@@ -116,7 +117,10 @@ pub fn delete_log() -> Result<()> {
if file_name.ends_with(".log") {
let now = Local::now();
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
let file_time = Local.from_local_datetime(&created_time).single().ok_or(anyhow::anyhow!("invalid local datetime"))?;
let file_time = Local
.from_local_datetime(&created_time)
.single()
.ok_or(anyhow::anyhow!("invalid local datetime"))?;
let duration = now.signed_duration_since(file_time);
if duration.num_days() > day {
@@ -128,10 +132,8 @@ pub fn delete_log() -> Result<()> {
Ok(())
};
for file in fs::read_dir(&log_dir)? {
if let Ok(file) = file {
let _ = process_file(file);
}
for file in fs::read_dir(&log_dir)?.flatten() {
let _ = process_file(file);
}
Ok(())
}
@@ -139,11 +141,7 @@ pub fn delete_log() -> Result<()> {
/// Initialize all the config files
/// before tauri setup
pub fn init_config() -> Result<()> {
#[cfg(target_os = "windows")]
unsafe {
let _ = dirs::init_portable_flag();
}
let _ = dirs::init_portable_flag();
let _ = init_log();
let _ = delete_log();
@@ -185,9 +183,9 @@ pub fn init_config() -> Result<()> {
/// initialize app resources
/// after tauri setup
pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
pub fn init_resources() -> Result<()> {
let app_dir = dirs::app_home_dir()?;
let res_dir = dirs::app_resources_dir(package_info)?;
let res_dir = dirs::app_resources_dir()?;
if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir);
@@ -241,3 +239,86 @@ pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
Ok(())
}
/// initialize url scheme
#[cfg(target_os = "windows")]
pub fn init_scheme() -> Result<()> {
use tauri::utils::platform::current_exe;
use winreg::enums::*;
use winreg::RegKey;
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_exe = app_exe.to_string_lossy().into_owned();
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
clash.set_value("", &"Clash Verge")?;
clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?;
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
default_icon.set_value("", &app_exe)?;
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?;
command.set_value("", &format!("{app_exe} \"%1\""))?;
Ok(())
}
#[cfg(target_os = "linux")]
pub fn init_scheme() -> Result<()> {
let output = std::process::Command::new("xdg-mime")
.arg("default")
.arg("clash-verge.desktop")
.arg("x-scheme-handler/clash")
.output()?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"failed to set clash scheme, {}",
String::from_utf8_lossy(&output.stderr)
));
}
Ok(())
}
#[cfg(target_os = "macos")]
pub fn init_scheme() -> Result<()> {
Ok(())
}
pub fn startup_script() -> Result<()> {
let path = {
let verge = Config::verge();
let verge = verge.latest();
verge.startup_script.clone().unwrap_or("".to_string())
};
if !path.is_empty() {
let mut shell = "";
if path.ends_with(".sh") {
shell = "bash";
}
if path.ends_with(".ps1") {
shell = "powershell";
}
if path.ends_with(".bat") {
shell = "cmd";
}
if shell.is_empty() {
return Err(anyhow::anyhow!("unsupported script: {path}"));
}
let current_dir = PathBuf::from(path.clone());
if !current_dir.exists() {
return Err(anyhow::anyhow!("script not found: {path}"));
}
let current_dir = current_dir.parent();
match current_dir {
Some(dir) => {
let _ = Command::new(shell)
.current_dir(dir.to_path_buf())
.args(&[path])
.output()?;
}
None => {
let _ = Command::new(shell).args(&[path]).output()?;
}
}
}
Ok(())
}
-1
View File
@@ -4,4 +4,3 @@ pub mod init;
pub mod resolve;
pub mod server;
pub mod tmpl;
// mod winhelp;
+111 -73
View File
@@ -1,10 +1,20 @@
use crate::config::IVerge;
use crate::{config::Config, core::*, utils::init, utils::server};
use crate::config::{IVerge, PrfOption};
use crate::{
config::{Config, PrfItem},
core::*,
utils::init,
utils::server,
};
use crate::{log_err, trace_err};
use anyhow::Result;
use once_cell::sync::OnceCell;
use serde_yaml::Mapping;
use std::net::TcpListener;
use tauri::api::notification;
use tauri::{App, AppHandle, Manager};
use window_shadows::set_shadow;
pub static VERSION: OnceCell<String> = OnceCell::new();
pub fn find_unused_port() -> Result<u16> {
match TcpListener::bind("127.0.0.1:0") {
@@ -27,11 +37,13 @@ pub fn find_unused_port() -> Result<u16> {
pub fn resolve_setup(app: &mut App) {
#[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_resources(app.package_info()));
log_err!(init::init_resources());
log_err!(init::init_scheme());
log_err!(init::startup_script());
// 处理随机端口
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
@@ -73,7 +85,7 @@ pub fn resolve_setup(app: &mut App) {
log::trace!("init system tray");
log_err!(tray::Tray::update_systray(&app.app_handle()));
let silent_start = { Config::verge().data().enable_silent_start.clone() };
let silent_start = { Config::verge().data().enable_silent_start };
if !silent_start.unwrap_or(false) {
create_window(&app.app_handle());
}
@@ -84,6 +96,13 @@ pub fn resolve_setup(app: &mut App) {
log_err!(handle::Handle::update_systray_part());
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
log_err!(timer::Timer::global().init());
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
tauri::async_runtime::block_on(async {
resolve_scheme(argvs[1].to_owned()).await;
});
}
}
/// reset system proxy
@@ -107,6 +126,7 @@ pub fn create_window(app_handle: &AppHandle) {
tauri::WindowUrl::App("index.html".into()),
)
.title("Clash Verge")
.visible(false)
.fullscreen(false)
.min_inner_size(600.0, 520.0);
@@ -135,75 +155,69 @@ pub fn create_window(app_handle: &AppHandle) {
}
}
};
#[cfg(target_os = "windows")]
{
use std::time::Duration;
use tokio::time::sleep;
use window_shadows::set_shadow;
match builder
.decorations(false)
.transparent(true)
.visible(false)
.build()
{
Ok(win) => {
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()?;
if pos.x < -400
|| pos.x > (size.width - 200).try_into()?
|| pos.y < -200
|| pos.y > (size.height - 200).try_into()?
{
center = true;
}
Ok(center)
})();
if center.unwrap_or(true) {
trace_err!(win.center(), "set win center");
}
log::trace!("try to create window");
let app_handle = app_handle.clone();
// 加点延迟避免界面闪一下
tauri::async_runtime::spawn(async move {
sleep(Duration::from_millis(888)).await;
if let Some(window) = app_handle.get_window("main") {
trace_err!(set_shadow(&window, true), "set win shadow");
trace_err!(window.show(), "set win visible");
trace_err!(window.unminimize(), "set win unminimize");
trace_err!(window.set_focus(), "set win focus");
} else {
log::error!(target: "app", "failed to create window, get_window is None")
}
});
}
Err(err) => log::error!(target: "app", "failed to create window, {err}"),
}
}
let window = builder
.decorations(false)
.transparent(true)
.visible(false)
.build();
#[cfg(target_os = "macos")]
crate::log_err!(builder
let window = builder
.decorations(true)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.build());
.build();
#[cfg(target_os = "linux")]
crate::log_err!(builder.decorations(true).transparent(false).build());
let window = builder.decorations(true).transparent(false).build();
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()?;
if pos.x < -400
|| pos.x > (size.width - 200) as i32
|| pos.y < -200
|| pos.y > (size.height - 200) as i32
{
center = true;
}
Ok(center)
})();
if center.unwrap_or(true) {
trace_err!(win.center(), "set win center");
}
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");
return;
}
}
}
/// 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_window("main")
.ok_or(anyhow::anyhow!("failed to get window"))?;
@@ -213,14 +227,38 @@ pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) ->
let size = size.to_logical::<f64>(scale);
let pos = win.outer_position()?;
let pos = pos.to_logical::<f64>(scale);
let verge = Config::verge();
let mut verge = verge.latest();
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
if save_to_file {
verge.save_file()?;
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) {
let url = param
.trim_start_matches("clash://install-config/?url=")
.trim_start_matches("clash://install-config?url=");
let option = PrfOption {
user_agent: None,
with_proxy: Some(true),
self_proxy: None,
update_interval: None,
};
if let Ok(item) = PrfItem::from_url(url, None, None, Some(option)).await {
if Config::profiles().data().append_item(item).is_ok() {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body("Import profile success")
.show()
.unwrap();
};
} else {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body("Import profile failed")
.show()
.unwrap();
log::error!("failed to parse url: {}", url);
}
}
+39 -5
View File
@@ -4,19 +4,42 @@ use super::resolve;
use crate::config::IVerge;
use anyhow::{bail, Result};
use port_scanner::local_port_available;
use std::convert::Infallible;
use tauri::AppHandle;
use warp::Filter;
#[derive(serde::Deserialize, Debug)]
struct QueryParam {
param: String,
}
/// check whether there is already exists
pub fn check_singleton() -> Result<()> {
let port = IVerge::get_singleton_port();
if !local_port_available(port) {
tauri::async_runtime::block_on(async {
let url = format!("http://127.0.0.1:{port}/commands/visible");
let resp = reqwest::get(url).await?.text().await?;
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 {
let param = argvs[1].as_str();
reqwest::get(format!(
"http://127.0.0.1:{port}/commands/scheme?param={param}"
))
.await?
.text()
.await?;
} else {
reqwest::get(format!("http://127.0.0.1:{port}/commands/visible"))
.await?
.text()
.await?;
}
bail!("app exists");
}
@@ -34,11 +57,22 @@ pub fn embed_server(app_handle: AppHandle) {
let port = IVerge::get_singleton_port();
tauri::async_runtime::spawn(async move {
let commands = warp::path!("commands" / "visible").map(move || {
let ping = warp::path!("commands" / "ping").map(move || "ok");
let visible = warp::path!("commands" / "visible").map(move || {
resolve::create_window(&app_handle);
format!("ok")
"ok"
});
warp::serve(commands).bind(([127, 0, 0, 1], port)).await;
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> {
resolve::resolve_scheme(query.param).await;
Ok("ok")
}
let commands = ping.or(visible).or(scheme);
warp::serve(commands).run(([127, 0, 0, 1], port)).await;
});
}
+1 -1
View File
@@ -1,4 +1,4 @@
///! Some config file template
//! Some config file template
/// template for new a profile item
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
-69
View File
@@ -1,69 +0,0 @@
#![cfg(target_os = "windows")]
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
//!
//! From https://github.com/tauri-apps/window-vibrancy/blob/dev/src/windows.rs
//!
use windows_sys::Win32::{
Foundation::*,
System::{LibraryLoader::*, SystemInformation::*},
};
fn get_function_impl(library: &str, function: &str) -> Option<FARPROC> {
assert_eq!(library.chars().last(), Some('\0'));
assert_eq!(function.chars().last(), Some('\0'));
let module = unsafe { LoadLibraryA(library.as_ptr()) };
if module == 0 {
return None;
}
Some(unsafe { GetProcAddress(module, function.as_ptr()) })
}
macro_rules! get_function {
($lib:expr, $func:ident) => {
get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')).map(|f| unsafe {
std::mem::transmute::<::windows_sys::Win32::Foundation::FARPROC, $func>(f)
})
};
}
/// Returns a tuple of (major, minor, buildnumber)
fn get_windows_ver() -> Option<(u32, u32, u32)> {
type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> i32;
let handle = get_function!("ntdll.dll", RtlGetVersion);
if let Some(rtl_get_version) = handle {
unsafe {
let mut vi = OSVERSIONINFOW {
dwOSVersionInfoSize: 0,
dwMajorVersion: 0,
dwMinorVersion: 0,
dwBuildNumber: 0,
dwPlatformId: 0,
szCSDVersion: [0; 128],
};
let status = (rtl_get_version)(&mut vi as _);
if status >= 0 {
Some((vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber))
} else {
None
}
}
} else {
None
}
}
pub fn is_win11() -> bool {
let v = get_windows_ver().unwrap_or_default();
v.2 >= 22000
}
#[test]
fn test_version() {
dbg!(get_windows_ver().unwrap_or_default());
}
+10 -33
View File
@@ -1,7 +1,7 @@
{
"package": {
"productName": "Clash Verge",
"version": "1.4.3"
"version": "1.5.2"
},
"build": {
"distDir": "../dist",
@@ -10,13 +10,8 @@
"beforeBuildCommand": "pnpm run web:build"
},
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.ico",
"iconAsTemplate": true
},
"bundle": {
"active": true,
"targets": ["deb", "appimage", "nsis", "app", "dmg", "updater"],
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"icon": [
"icons/32x32.png",
@@ -30,32 +25,7 @@
"copyright": "© 2022 zzzgydi All Rights Reserved",
"category": "DeveloperTool",
"shortDescription": "A Clash Meta GUI based on tauri.",
"longDescription": "A Clash Meta GUI based on tauri.",
"deb": {
"depends": ["openssl"]
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"type": "embedBootstrapper",
"silent": true
},
"nsis": {
"displayLanguageSelector": true,
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"license": "../LICENSE"
}
}
"longDescription": "A Clash Meta GUI based on tauri."
},
"updater": {
"active": true,
@@ -81,11 +51,18 @@
},
"clipboard": {
"all": true
},
"notification": {
"all": true
},
"dialog": {
"all": false,
"open": true
}
},
"windows": [],
"security": {
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';"
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src http: https: data: 'self';"
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"targets": ["deb", "appimage", "updater"],
"deb": {
"depends": ["openssl"],
"desktopTemplate": "./template/clash-verge.desktop"
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"tauri": {
"systemTray": {
"iconPath": "icons/mac-tray-icon.png",
"iconAsTemplate": true
},
"bundle": {
"targets": ["app", "dmg", "updater"],
"macOS": {
"frameworks": [],
"minimumSystemVersion": "10.15",
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"targets": ["nsis", "updater"],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"type": "embedBootstrapper",
"silent": true
},
"nsis": {
"displayLanguageSelector": true,
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"license": "../LICENSE",
"installMode": "perMachine",
"template": "./template/installer.nsi"
}
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
[Desktop Entry]
Categories={{{categories}}}
Comment={{{comment}}}
Exec={{{exec}}} %u
Icon={{{icon}}}
Name={{{name}}}
Terminal=false
Type=Application
MimeType=x-scheme-handler/clash;
+827
View File
@@ -0,0 +1,827 @@
; 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.
Unicode true
; Set the compression algorithm. Default is LZMA.
!if "{{compression}}" == ""
SetCompressor /SOLID lzma
!else
SetCompressor /SOLID "{{compression}}"
!endif
!include MUI2.nsh
!include FileFunc.nsh
!include x64.nsh
!include WordFunc.nsh
!include "LogicLib.nsh"
!include "StrFunc.nsh"
!addplugindir "$%AppData%\Local\NSIS\"
${StrCase}
${StrLoc}
!define MANUFACTURER "{{manufacturer}}"
!define PRODUCTNAME "{{product_name}}"
!define VERSION "{{version}}"
!define VERSIONWITHBUILD "{{version_with_build}}"
!define SHORTDESCRIPTION "{{short_description}}"
!define INSTALLMODE "{{install_mode}}"
!define LICENSE "{{license}}"
!define INSTALLERICON "{{installer_icon}}"
!define SIDEBARIMAGE "{{sidebar_image}}"
!define HEADERIMAGE "{{header_image}}"
!define MAINBINARYNAME "{{main_binary_name}}"
!define MAINBINARYSRCPATH "{{main_binary_path}}"
!define BUNDLEID "{{bundle_id}}"
!define COPYRIGHT "{{copyright}}"
!define OUTFILE "{{out_file}}"
!define ARCH "{{arch}}"
!define PLUGINSPATH "{{additional_plugins_path}}"
!define ALLOWDOWNGRADES "{{allow_downgrades}}"
!define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}"
!define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}"
!define WEBVIEW2INSTALLERARGS "{{webview2_installer_args}}"
!define WEBVIEW2BOOTSTRAPPERPATH "{{webview2_bootstrapper_path}}"
!define WEBVIEW2INSTALLERPATH "{{webview2_installer_path}}"
!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}"
!define MANUPRODUCTKEY "Software\${MANUFACTURER}\${PRODUCTNAME}"
!define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}"
!define ESTIMATEDSIZE "{{estimated_size}}"
Name "${PRODUCTNAME}"
BrandingText "${COPYRIGHT}"
OutFile "${OUTFILE}"
VIProductVersion "${VERSIONWITHBUILD}"
VIAddVersionKey "ProductName" "${PRODUCTNAME}"
VIAddVersionKey "FileDescription" "${SHORTDESCRIPTION}"
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
VIAddVersionKey "FileVersion" "${VERSION}"
VIAddVersionKey "ProductVersion" "${VERSION}"
; Plugins path, currently exists for linux only
!if "${PLUGINSPATH}" != ""
!addplugindir "${PLUGINSPATH}"
!endif
!if "${UNINSTALLERSIGNCOMMAND}" != ""
!uninstfinalize '${UNINSTALLERSIGNCOMMAND}'
!endif
; Handle install mode, `perUser`, `perMachine` or `both`
!if "${INSTALLMODE}" == "perMachine"
RequestExecutionLevel highest
!endif
!if "${INSTALLMODE}" == "currentUser"
RequestExecutionLevel user
!endif
!if "${INSTALLMODE}" == "both"
!define MULTIUSER_MUI
!define MULTIUSER_INSTALLMODE_INSTDIR "${PRODUCTNAME}"
!define MULTIUSER_INSTALLMODE_COMMANDLINE
!if "${ARCH}" == "x64"
!define MULTIUSER_USE_PROGRAMFILES64
!else if "${ARCH}" == "arm64"
!define MULTIUSER_USE_PROGRAMFILES64
!endif
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}"
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser"
!define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME
!define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation
!define MULTIUSER_EXECUTIONLEVEL Highest
!include MultiUser.nsh
!endif
; installer icon
!if "${INSTALLERICON}" != ""
!define MUI_ICON "${INSTALLERICON}"
!endif
; installer sidebar image
!if "${SIDEBARIMAGE}" != ""
!define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}"
!endif
; installer header image
!if "${HEADERIMAGE}" != ""
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}"
!endif
; Define registry key to store installer language
!define MUI_LANGDLL_REGISTRY_ROOT "HKCU"
!define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}"
!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language"
; Installer pages, must be ordered as they appear
; 1. Welcome Page
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MUI_PAGE_WELCOME
; 2. License Page (if defined)
!if "${LICENSE}" != ""
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MUI_PAGE_LICENSE "${LICENSE}"
!endif
; 3. Install mode (if it is set to `both`)
!if "${INSTALLMODE}" == "both"
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MULTIUSER_PAGE_INSTALLMODE
!endif
; 4. Custom page to ask user if he wants to reinstall/uninstall
; only if a previous installtion was detected
Var ReinstallPageCheck
Page custom PageReinstall PageLeaveReinstall
Function PageReinstall
; Uninstall previous WiX installation if exists.
;
; A WiX installer stores the isntallation info in registry
; using a UUID and so we have to loop through all keys under
; `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`
; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER}
;
; This has a potentional issue that there maybe another installation that matches
; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer,
; however, this should be fine since the user will have to confirm the uninstallation
; and they can chose to abort it if doesn't make sense.
StrCpy $0 0
wix_loop:
EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0
StrCmp $1 "" wix_done ; Exit loop if there is no more keys to loop on
IntOp $0 $0 + 1
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName"
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "Publisher"
StrCmp "$R0$R1" "${PRODUCTNAME}${MANUFACTURER}" 0 wix_loop
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString"
${StrCase} $R1 $R0 "L"
${StrLoc} $R0 $R1 "msiexec" ">"
StrCmp $R0 0 0 wix_done
StrCpy $R7 "wix"
StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1"
Goto compare_version
wix_done:
; Check if there is an existing installation, if not, abort the reinstall page
ReadRegStr $R0 SHCTX "${UNINSTKEY}" ""
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
${IfThen} "$R0$R1" == "" ${|} Abort ${|}
; Compare this installar version with the existing installation
; and modify the messages presented to the user accordingly
compare_version:
StrCpy $R4 "$(older)"
${If} $R7 == "wix"
ReadRegStr $R0 HKLM "$R6" "DisplayVersion"
${Else}
ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion"
${EndIf}
${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|}
nsis_tauri_utils::SemverCompare "${VERSION}" $R0
Pop $R0
; Reinstalling the same version
${If} $R0 == 0
StrCpy $R1 "$(alreadyInstalledLong)"
StrCpy $R2 "$(addOrReinstall)"
StrCpy $R3 "$(uninstallApp)"
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)"
StrCpy $R5 "2"
; Upgrading
${ElseIf} $R0 == 1
StrCpy $R1 "$(olderOrUnknownVersionInstalled)"
StrCpy $R2 "$(uninstallBeforeInstalling)"
StrCpy $R3 "$(dontUninstall)"
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
StrCpy $R5 "1"
; Downgrading
${ElseIf} $R0 == -1
StrCpy $R1 "$(newerVersionInstalled)"
StrCpy $R2 "$(uninstallBeforeInstalling)"
!if "${ALLOWDOWNGRADES}" == "true"
StrCpy $R3 "$(dontUninstall)"
!else
StrCpy $R3 "$(dontUninstallDowngrade)"
!endif
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
StrCpy $R5 "1"
${Else}
Abort
${EndIf}
Call SkipIfPassive
nsDialogs::Create 1018
Pop $R4
${IfThen} $(^RTL) == 1 ${|} nsDialogs::SetRTL $(^RTL) ${|}
${NSD_CreateLabel} 0 0 100% 24u $R1
Pop $R1
${NSD_CreateRadioButton} 30u 50u -30u 8u $R2
Pop $R2
${NSD_OnClick} $R2 PageReinstallUpdateSelection
${NSD_CreateRadioButton} 30u 70u -30u 8u $R3
Pop $R3
; disable this radio button if downgrading and downgrades are disabled
!if "${ALLOWDOWNGRADES}" == "false"
${IfThen} $R0 == -1 ${|} EnableWindow $R3 0 ${|}
!endif
${NSD_OnClick} $R3 PageReinstallUpdateSelection
; Check the first radio button if this the first time
; we enter this page or if the second button wasn't
; selected the last time we were on this page
${If} $ReinstallPageCheck != 2
SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0
${Else}
SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0
${EndIf}
${NSD_SetFocus} $R2
nsDialogs::Show
FunctionEnd
Function PageReinstallUpdateSelection
${NSD_GetState} $R2 $R1
${If} $R1 == ${BST_CHECKED}
StrCpy $ReinstallPageCheck 1
${Else}
StrCpy $ReinstallPageCheck 2
${EndIf}
FunctionEnd
Function PageLeaveReinstall
${NSD_GetState} $R2 $R1
; $R5 holds whether we are reinstalling the same version or not
; $R5 == "1" -> different versions
; $R5 == "2" -> same version
;
; $R1 holds the radio buttons state. its meaning is dependant on the context
StrCmp $R5 "1" 0 +2 ; Existing install is not the same version?
StrCmp $R1 "1" reinst_uninstall reinst_done ; $R1 == "1", then user chose to uninstall existing version, otherwise skip uninstalling
StrCmp $R1 "1" reinst_done ; Same version? skip uninstalling
reinst_uninstall:
HideWindow
ClearErrors
${If} $R7 == "wix"
ReadRegStr $R1 HKLM "$R6" "UninstallString"
ExecWait '$R1' $0
${Else}
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
ExecWait '$R1 /P _?=$4' $0
${EndIf}
BringToFront
${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code
${If} $0 <> 0
${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe"
${If} $0 = 1 ; User aborted uninstaller?
StrCmp $R5 "2" 0 +2 ; Is the existing install the same version?
Quit ; ...yes, already installed, we are done
Abort
${EndIf}
MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)"
Abort
${Else}
StrCpy $0 $R1 1
${IfThen} $0 == '"' ${|} StrCpy $R1 $R1 -1 1 ${|} ; Strip quotes from UninstallString
Delete $R1
RMDir $INSTDIR
${EndIf}
reinst_done:
FunctionEnd
; 5. Choose install directoy page
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MUI_PAGE_DIRECTORY
; 6. Start menu shortcut page
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
Var AppStartMenuFolder
!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder
; 7. Installation page
!insertmacro MUI_PAGE_INSTFILES
; 8. Finish page
;
; Don't auto jump to finish page after installation page,
; because the installation page has useful info that can be used debug any issues with the installer.
!define MUI_FINISHPAGE_NOAUTOCLOSE
; Use show readme button in the finish page as a button create a desktop shortcut
!define MUI_FINISHPAGE_SHOWREADME
!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)"
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut
; Show run app after installation.
!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAINBINARYNAME}.exe"
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MUI_PAGE_FINISH
; Uninstaller Pages
; 1. Confirm uninstall page
Var DeleteAppDataCheckbox
Var DeleteAppDataCheckboxState
!define /ifndef WS_EX_LAYOUTRTL 0x00400000
!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow
Function un.ConfirmShow
FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog
${If} $(^RTL) == 1
System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE}|${WS_EX_LAYOUTRTL},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 50,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'
${Else}
System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 0,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'
${EndIf}
Pop $DeleteAppDataCheckbox
SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1
SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1
FunctionEnd
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave
Function un.ConfirmLeave
SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState
FunctionEnd
!insertmacro MUI_UNPAGE_CONFIRM
; 2. Uninstalling Page
!insertmacro MUI_UNPAGE_INSTFILES
;Languages
{{#each languages}}
!insertmacro MUI_LANGUAGE "{{this}}"
{{/each}}
!insertmacro MUI_RESERVEFILE_LANGDLL
{{#each language_files}}
!include "{{this}}"
{{/each}}
!macro SetContext
!if "${INSTALLMODE}" == "currentUser"
SetShellVarContext current
!else if "${INSTALLMODE}" == "perMachine"
SetShellVarContext all
!endif
${If} ${RunningX64}
!if "${ARCH}" == "x64"
SetRegView 64
!else if "${ARCH}" == "arm64"
SetRegView 64
!else
SetRegView 32
!endif
${EndIf}
!macroend
Var PassiveMode
Function .onInit
${GetOptions} $CMDLINE "/P" $PassiveMode
IfErrors +2 0
StrCpy $PassiveMode 1
!if "${DISPLAYLANGUAGESELECTOR}" == "true"
!insertmacro MUI_LANGDLL_DISPLAY
!endif
!insertmacro SetContext
${If} $INSTDIR == ""
; Set default install location
!if "${INSTALLMODE}" == "perMachine"
${If} ${RunningX64}
!if "${ARCH}" == "x64"
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
!else if "${ARCH}" == "arm64"
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
!else
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
!endif
${Else}
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
${EndIf}
!else if "${INSTALLMODE}" == "currentUser"
StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}"
!endif
Call RestorePreviousInstallLocation
${EndIf}
!if "${INSTALLMODE}" == "both"
!insertmacro MULTIUSER_INIT
!endif
FunctionEnd
!macro CheckAllVergeProcesses
; Check if Clash Verge.exe is running
nsis_tauri_utils::FindProcess "Clash Verge.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill Clash Verge.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "Clash Verge.exe"
!else
nsis_tauri_utils::KillProcess "Clash Verge.exe"
!endif
${EndIf}
; Check if clash-verge-service.exe is running
nsis_tauri_utils::FindProcess "clash-verge-service.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill clash-verge-service.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe"
!else
nsis_tauri_utils::KillProcess "clash-verge-service.exe"
!endif
${EndIf}
; Check if clash-meta-alpha.exe is running
nsis_tauri_utils::FindProcess "clash-meta-alpha.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill clash-meta-alpha.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-meta-alpha.exe"
!else
nsis_tauri_utils::KillProcess "clash-meta-alpha.exe"
!endif
${EndIf}
; Check if clash-meta.exe is running
nsis_tauri_utils::FindProcess "clash-meta.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill clash-meta.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-meta.exe"
!else
nsis_tauri_utils::KillProcess "clash-meta.exe"
!endif
${EndIf}
!macroend
!macro StartVergeService
; Check if the service exists
SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0:service exists;other: service not exists
; Service exists
${If} $0 == 0
Push $0
; Check if the service is running
SimpleSC::ServiceIsRunning "clash_verge_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0
Push $0
${If} $1 == 0
DetailPrint "Restart Clash Verge Service..."
SimpleSC::StartService "clash_verge_service" "" 30
${EndIf}
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Check Service Status Error ($0)"
${EndIf}
${EndIf}
!macroend
!macro RemoveVergeService
; Check if the service exists
SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0:service exists;other: service not exists
; Service exists
${If} $0 == 0
Push $0
; Check if the service is running
SimpleSC::ServiceIsRunning "clash_verge_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0
Push $0
${If} $1 == 1
DetailPrint "Stop Clash Verge Service..."
SimpleSC::StopService "clash_verge_service" 1 30
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
${If} $0 == 0
DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "clash_verge_service"
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
${EndIf}
${ElseIf} $1 == 0
DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "clash_verge_service"
${EndIf}
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Check Service Status Error ($0)"
${EndIf}
${EndIf}
!macroend
Section EarlyChecks
; Abort silent installer if downgrades is disabled
!if "${ALLOWDOWNGRADES}" == "false"
IfSilent 0 silent_downgrades_done
; If downgrading
${If} $R0 == -1
System::Call 'kernel32::AttachConsole(i -1)i.r0'
${If} $0 != 0
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
FileWrite $0 "$(silentDowngrades)"
${EndIf}
Abort
${EndIf}
silent_downgrades_done:
!endif
SectionEnd
Section WebView2
; Check if Webview2 is already installed and skip this section
${If} ${RunningX64}
ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${Else}
ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${EndIf}
ReadRegStr $5 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
StrCmp $4 "" 0 webview2_done
StrCmp $5 "" 0 webview2_done
; Webview2 install modes
!if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper"
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
DetailPrint "$(webview2Downloading)"
nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe"
Pop $0
${If} $0 == 0
DetailPrint "$(webview2DownloadSuccess)"
${Else}
DetailPrint "$(webview2DownloadError)"
Abort "$(webview2AbortError)"
${EndIf}
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
Goto install_webview2
!endif
!if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper"
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}"
DetailPrint "$(installingWebview2)"
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
Goto install_webview2
!endif
!if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller"
Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}"
DetailPrint "$(installingWebview2)"
StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
Goto install_webview2
!endif
Goto webview2_done
install_webview2:
DetailPrint "$(installingWebview2)"
; $6 holds the path to the webview2 installer
ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1
${If} $1 == 0
DetailPrint "$(webview2InstallSuccess)"
${Else}
DetailPrint "$(webview2InstallError)"
Abort "$(webview2AbortError)"
${EndIf}
webview2_done:
SectionEnd
!macro CheckIfAppIsRunning
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "${MAINBINARYNAME}.exe"
!else
nsis_tauri_utils::FindProcess "${MAINBINARYNAME}.exe"
!endif
Pop $R0
${If} $R0 = 0
IfSilent kill 0
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|}
kill:
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
!else
nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe"
!endif
Pop $R0
Sleep 500
${If} $R0 = 0
Goto app_check_done
${Else}
IfSilent silent ui
silent:
System::Call 'kernel32::AttachConsole(i -1)i.r0'
${If} $0 != 0
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
FileWrite $0 "$(appRunning)$\n"
${EndIf}
Abort
ui:
Abort "$(failedToKillApp)"
${EndIf}
cancel:
Abort "$(appRunning)"
${EndIf}
app_check_done:
!macroend
Section Install
SetOutPath $INSTDIR
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
; Copy main executable
File "${MAINBINARYSRCPATH}"
; Copy resources
{{#each resources_dirs}}
CreateDirectory "$INSTDIR\\{{this}}"
{{/each}}
{{#each resources}}
File /a "/oname={{this.[1]}}" "{{@key}}"
{{/each}}
; Copy external binaries
{{#each binaries}}
File /a "/oname={{this}}" "{{@key}}"
{{/each}}
!insertmacro StartVergeService
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
; Save $INSTDIR in registry for future installations
WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR
!if "${INSTALLMODE}" == "both"
; Save install mode to be selected by default for the next installation such as updating
; or when uninstalling
WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1
!endif
; Registry information for add/remove programs
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}"
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\""
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}"
WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}"
WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\""
WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1"
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1"
WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "${ESTIMATEDSIZE}"
; Create start menu shortcut (GUI)
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
Call CreateStartMenuShortcut
!insertmacro MUI_STARTMENU_WRITE_END
; Create shortcuts for silent and passive installers, which
; can be disabled by passing `/NS` flag
; GUI installer has buttons for users to control creating them
IfSilent check_ns_flag 0
${IfThen} $PassiveMode == 1 ${|} Goto check_ns_flag ${|}
Goto shortcuts_done
check_ns_flag:
${GetOptions} $CMDLINE "/NS" $R0
IfErrors 0 shortcuts_done
Call CreateDesktopShortcut
Call CreateStartMenuShortcut
shortcuts_done:
; Auto close this page for passive mode
${IfThen} $PassiveMode == 1 ${|} SetAutoClose true ${|}
SectionEnd
Function .onInstSuccess
; Check for `/R` flag only in silent and passive installers because
; GUI installer has a toggle for the user to (re)start the app
IfSilent check_r_flag 0
${IfThen} $PassiveMode == 1 ${|} Goto check_r_flag ${|}
Goto run_done
check_r_flag:
${GetOptions} $CMDLINE "/R" $R0
IfErrors run_done 0
Exec '"$INSTDIR\${MAINBINARYNAME}.exe"'
run_done:
FunctionEnd
Function un.onInit
!insertmacro SetContext
!if "${INSTALLMODE}" == "both"
!insertmacro MULTIUSER_UNINIT
!endif
!insertmacro MUI_UNGETLANGUAGE
FunctionEnd
Section Uninstall
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
!insertmacro RemoveVergeService
; Delete the app directory and its content from disk
; Copy main executable
Delete "$INSTDIR\${MAINBINARYNAME}.exe"
; Delete resources
{{#each resources}}
Delete "$INSTDIR\\{{this.[1]}}"
{{/each}}
; Delete external binaries
{{#each binaries}}
Delete "$INSTDIR\\{{this}}"
{{/each}}
; Delete uninstaller
Delete "$INSTDIR\uninstall.exe"
${If} $DeleteAppDataCheckboxState == 1
RMDir /R /REBOOTOK "$INSTDIR"
${Else}
{{#each resources_ancestors}}
RMDir /REBOOTOK "$INSTDIR\\{{this}}"
{{/each}}
RMDir "$INSTDIR"
${EndIf}
; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
; Remove desktop shortcuts
Delete "$DESKTOP\${MAINBINARYNAME}.lnk"
; Remove registry information for add/remove programs
!if "${INSTALLMODE}" == "both"
DeleteRegKey SHCTX "${UNINSTKEY}"
!else if "${INSTALLMODE}" == "perMachine"
DeleteRegKey HKLM "${UNINSTKEY}"
!else
DeleteRegKey HKCU "${UNINSTKEY}"
!endif
DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language"
; Delete app data
${If} $DeleteAppDataCheckboxState == 1
SetShellVarContext current
RmDir /r "$APPDATA\${BUNDLEID}"
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
${EndIf}
${GetOptions} $CMDLINE "/P" $R0
IfErrors +2 0
SetAutoClose true
SectionEnd
Function RestorePreviousInstallLocation
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
StrCmp $4 "" +2 0
StrCpy $INSTDIR $4
FunctionEnd
Function SkipIfPassive
${IfThen} $PassiveMode == 1 ${|} Abort ${|}
FunctionEnd
Function CreateDesktopShortcut
CreateShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
ApplicationID::Set "$DESKTOP\${MAINBINARYNAME}.lnk" "${BUNDLEID}"
FunctionEnd
Function CreateStartMenuShortcut
CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder"
CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
ApplicationID::Set "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "${BUNDLEID}"
FunctionEnd
+2 -2
View File
@@ -27,8 +27,8 @@ body {
}
*::-webkit-scrollbar {
width: 6px;
height: 6px;
width: 8px;
height: 8px;
background: transparent;
}
*::-webkit-scrollbar-thumb {
+2 -2
View File
@@ -77,8 +77,8 @@
.the-bar {
position: absolute;
top: 2px;
right: 8px;
top: 0px;
right: 0px;
height: 36px;
display: flex;
align-items: center;
+14 -4
View File
@@ -19,7 +19,7 @@
.base-container {
height: 100%;
overflow: hidden;
border-radius: var(--border-radius);
border-top-left-radius: var(--border-radius);
> section {
position: relative;
@@ -27,15 +27,25 @@
width: 100%;
height: 100%;
overflow: auto;
padding: 5px 5px;
padding: 10px 0;
box-sizing: border-box;
scrollbar-gutter: stable;
.base-content {
width: 100%;
// max-width: 850px;
width: calc(100% - 10px * 2);
margin: 0 auto;
}
}
&.no-padding {
> section {
padding: 0;
overflow: visible;
.base-content {
width: 100%;
}
}
}
}
}
+5 -4
View File
@@ -8,17 +8,18 @@ interface Props {
header?: React.ReactNode; // something behind title
contentStyle?: React.CSSProperties;
children?: ReactNode;
full?: boolean;
}
export const BasePage: React.FC<Props> = (props) => {
const { title, header, contentStyle, children } = props;
const { title, header, contentStyle, full, children } = props;
const { theme } = useCustomTheme();
const isDark = theme.palette.mode === "dark";
return (
<BaseErrorBoundary>
<div className="base-page" data-windrag>
<div className="base-page">
<header data-windrag style={{ userSelect: "none" }}>
<Typography variant="h4" component="h1" data-windrag>
{title}
@@ -28,7 +29,7 @@ export const BasePage: React.FC<Props> = (props) => {
</header>
<div
className="base-container"
className={full ? "base-container no-padding" : "base-container"}
style={{ backgroundColor: isDark ? "#090909" : "#ffffff" }}
>
<section
@@ -38,7 +39,7 @@ export const BasePage: React.FC<Props> = (props) => {
: "",
}}
>
<div className="base-content" style={contentStyle} data-windrag>
<div className="base-content" style={contentStyle}>
{children}
</div>
</section>
+24 -8
View File
@@ -1,6 +1,10 @@
import dayjs from "dayjs";
import { useMemo, useState } from "react";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import {
DataGrid,
GridColDef,
GridValueFormatterParams,
} from "@mui/x-data-grid";
import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic";
@@ -24,6 +28,8 @@ export const ConnectionTable = (props: Props) => {
width: 88,
align: "right",
headerAlign: "right",
valueFormatter: (params: GridValueFormatterParams<number>) =>
parseTraffic(params.value).join(" "),
},
{
field: "upload",
@@ -31,6 +37,8 @@ export const ConnectionTable = (props: Props) => {
width: 88,
align: "right",
headerAlign: "right",
valueFormatter: (params: GridValueFormatterParams<number>) =>
parseTraffic(params.value).join(" "),
},
{
field: "dlSpeed",
@@ -38,6 +46,8 @@ export const ConnectionTable = (props: Props) => {
width: 88,
align: "right",
headerAlign: "right",
valueFormatter: (params: GridValueFormatterParams<number>) =>
parseTraffic(params.value).join(" ") + "/s",
},
{
field: "ulSpeed",
@@ -45,10 +55,12 @@ export const ConnectionTable = (props: Props) => {
width: 88,
align: "right",
headerAlign: "right",
valueFormatter: (params: GridValueFormatterParams<number>) =>
parseTraffic(params.value).join(" ") + "/s",
},
{ field: "chains", headerName: "Chains", flex: 360, minWidth: 360 },
{ field: "rule", headerName: "Rule", flex: 300, minWidth: 250 },
{ field: "process", headerName: "Process", flex: 480, minWidth: 480 },
{ field: "process", headerName: "Process", flex: 240, minWidth: 120 },
{
field: "time",
headerName: "Time",
@@ -56,6 +68,11 @@ export const ConnectionTable = (props: Props) => {
minWidth: 100,
align: "right",
headerAlign: "right",
sortComparator: (v1, v2) => {
return new Date(v2).getTime() - new Date(v1).getTime();
},
valueFormatter: (params: GridValueFormatterParams<string>) =>
dayjs(params.value).fromNow(),
},
{ field: "source", headerName: "Source", flex: 200, minWidth: 130 },
{
@@ -72,20 +89,19 @@ export const ConnectionTable = (props: Props) => {
const { metadata, rulePayload } = each;
const chains = [...each.chains].reverse().join(" / ");
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
return {
id: each.id,
host: metadata.host
? `${metadata.host}:${metadata.destinationPort}`
: `${metadata.destinationIP}:${metadata.destinationPort}`,
download: parseTraffic(each.download).join(" "),
upload: parseTraffic(each.upload).join(" "),
dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s",
ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s",
download: each.download,
upload: each.upload,
dlSpeed: each.curDownload,
ulSpeed: each.curUpload,
chains,
rule,
process: truncateStr(metadata.process || metadata.processPath),
time: dayjs(each.start).fromNow(),
time: each.start,
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
destinationIP: metadata.destinationIP,
type: `${metadata.type}(${metadata.network})`,
+56 -6
View File
@@ -1,16 +1,50 @@
import { Button } from "@mui/material";
import { Button, ButtonGroup } from "@mui/material";
import { appWindow } from "@tauri-apps/api/window";
import {
CloseRounded,
CropSquareRounded,
FilterNoneRounded,
HorizontalRuleRounded,
PushPinOutlined,
PushPinRounded,
} from "@mui/icons-material";
import { useState } from "react";
export const LayoutControl = () => {
const minWidth = 40;
const [isMaximized, setIsMaximized] = useState(false);
const [isPined, setIsPined] = useState(false);
appWindow.isMaximized().then((isMaximized) => {
setIsMaximized(() => isMaximized);
});
return (
<>
<ButtonGroup
variant="text"
sx={{
height: "100%",
".MuiButtonGroup-grouped": {
borderRadius: "0px",
borderRight: "0px",
},
}}
>
<Button
size="small"
sx={{ minWidth, svg: { transform: "scale(0.9)" } }}
onClick={() => {
appWindow.setAlwaysOnTop(!isPined);
setIsPined((isPined) => !isPined);
}}
>
{isPined ? (
<PushPinRounded fontSize="small" />
) : (
<PushPinOutlined fontSize="small" />
)}
</Button>
<Button
size="small"
sx={{ minWidth, svg: { transform: "scale(0.9)" } }}
@@ -22,18 +56,34 @@ export const LayoutControl = () => {
<Button
size="small"
sx={{ minWidth, svg: { transform: "scale(0.9)" } }}
onClick={() => appWindow.toggleMaximize()}
onClick={() => {
setIsMaximized((isMaximized) => !isMaximized);
appWindow.toggleMaximize();
}}
>
<CropSquareRounded fontSize="small" />
{isMaximized ? (
<FilterNoneRounded
fontSize="small"
style={{
transform: "rotate(180deg) scale(0.7)",
}}
/>
) : (
<CropSquareRounded fontSize="small" />
)}
</Button>
<Button
size="small"
sx={{ minWidth, svg: { transform: "scale(1.05)" } }}
sx={{
minWidth,
svg: { transform: "scale(1.05)" },
":hover": { bgcolor: "#ff000090" },
}}
onClick={() => appWindow.close()}
>
<CloseRounded fontSize="small" />
</Button>
</>
</ButtonGroup>
);
};
+139 -27
View File
@@ -7,25 +7,32 @@ import {
List,
ListItem,
ListItemText,
styled,
Box,
alpha,
Typography,
Divider,
LinearProgress,
} from "@mui/material";
import { RefreshRounded } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { getProviders, providerUpdate } from "@/services/api";
import { getProxyProviders, proxyProviderUpdate } from "@/services/api";
import { BaseDialog } from "../base";
import parseTraffic from "@/utils/parse-traffic";
export const ProviderButton = () => {
const { t } = useTranslation();
const { data } = useSWR("getProviders", getProviders);
const { data } = useSWR("getProxyProviders", getProxyProviders);
const [open, setOpen] = useState(false);
const hasProvider = Object.keys(data || {}).length > 0;
const handleUpdate = useLockFn(async (key: string) => {
await providerUpdate(key);
await proxyProviderUpdate(key);
await mutate("getProxies");
await mutate("getProviders");
await mutate("getProxyProviders");
});
if (!hasProvider) return null;
@@ -43,7 +50,23 @@ export const ProviderButton = () => {
<BaseDialog
open={open}
title={t("Proxy Provider")}
title={
<Box display="flex" justifyContent="space-between" gap={1}>
<Typography variant="h6">{t("Proxy Provider")}</Typography>
<Button
variant="contained"
onClick={async () => {
Object.entries(data || {}).forEach(async ([key, item]) => {
await proxyProviderUpdate(key);
await mutate("getProxies");
await mutate("getProxyProviders");
});
}}
>
{t("Update All")}
</Button>
</Box>
}
contentSx={{ width: 400 }}
disableOk
cancelBtn={t("Cancel")}
@@ -53,30 +76,84 @@ export const ProviderButton = () => {
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(data || {}).map(([key, item]) => {
const time = dayjs(item.updatedAt);
const sub = item.subscriptionInfo;
const hasSubInfo = !!sub;
const upload = sub?.Upload || 0;
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
const progress = Math.round(
((download + upload) * 100) / (total + 0.1)
);
return (
<ListItem sx={{ p: 0 }} key={key}>
<ListItemText
primary={key}
secondary={
<>
<span style={{ marginRight: "4em" }}>
Type: {item.vehicleType}
</span>
<span title={time.format("YYYY-MM-DD HH:mm:ss")}>
Updated: {time.fromNow()}
</span>
</>
}
/>
<IconButton
size="small"
color="inherit"
title="Update Provider"
onClick={() => handleUpdate(key)}
<>
<ListItem
sx={(theme) => ({
p: 0,
borderRadius: "10px",
boxShadow: theme.shadows[2],
mb: 1,
})}
key={key}
>
<RefreshRounded />
</IconButton>
</ListItem>
<ListItemText
sx={{ px: 1 }}
primary={
<>
<Typography
variant="h6"
component="span"
noWrap
title={key}
>
{key}
</Typography>
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
{item.proxies.length}
</TypeBox>
</>
}
secondary={
<>
<StyledTypeBox component="span">
{item.vehicleType}
</StyledTypeBox>
<StyledTypeBox component="span">
{t("Update At")} {time.fromNow()}
</StyledTypeBox>
{hasSubInfo && (
<>
<Box sx={{ ...boxStyle, fontSize: 14 }}>
<span title="Used / Total">
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title="Expire Time">
{parseExpire(expire)}
</span>
</Box>
<LinearProgress
variant="determinate"
value={progress}
color="inherit"
/>
</>
)}
</>
}
/>
<Divider orientation="vertical" flexItem />
<IconButton
size="small"
color="inherit"
title="Update Provider"
onClick={() => handleUpdate(key)}
>
<RefreshRounded />
</IconButton>
</ListItem>
</>
);
})}
</List>
@@ -84,3 +161,38 @@ export const ProviderButton = () => {
</>
);
};
const TypeBox = styled(Box)(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.secondary.main, 0.5),
color: alpha(theme.palette.secondary.main, 0.8),
borderRadius: 4,
fontSize: 10,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));
const StyledTypeBox = styled(Box)(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.primary.main, 0.5),
color: alpha(theme.palette.primary.main, 0.8),
borderRadius: 4,
fontSize: 10,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));
const boxStyle = {
height: 26,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
};
function parseExpire(expire?: number) {
if (!expire) return "-";
return dayjs(expire * 1000).format("YYYY-MM-DD");
}
+2 -1
View File
@@ -25,6 +25,7 @@ export const ProxyGroups = (props: Props) => {
const { verge } = useVerge();
const { current, patchCurrent } = useProfiles();
const timeout = verge?.default_latency_timeout || 10000;
const virtuosoRef = useRef<VirtuosoHandle>(null);
@@ -83,7 +84,7 @@ export const ProxyGroups = (props: Props) => {
}
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
await delayManager.checkListDelay(names, groupName);
await delayManager.checkListDelay(names, groupName, timeout);
onProxies();
});
+8 -3
View File
@@ -4,6 +4,7 @@ import { CheckCircleOutlineRounded } from "@mui/icons-material";
import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
import { BaseLoading } from "@/components/base";
import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
interface Props {
groupName: string;
@@ -20,6 +21,8 @@ export const ProxyItemMini = (props: Props) => {
// -1/<=0 为 不显示
// -2 为 loading
const [delay, setDelay] = useState(-1);
const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
@@ -36,7 +39,7 @@ export const ProxyItemMini = (props: Props) => {
const onDelay = useLockFn(async () => {
setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName));
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
});
return (
@@ -98,6 +101,8 @@ export const ProxyItemMini = (props: Props) => {
)}
<TypeBox component="span">{proxy.type}</TypeBox>
{proxy.udp && <TypeBox component="span">UDP</TypeBox>}
{proxy.xudp && <TypeBox component="span">XUDP</TypeBox>}
{proxy.tfo && <TypeBox component="span">TFO</TypeBox>}
</Box>
)}
</Box>
@@ -137,14 +142,14 @@ export const ProxyItemMini = (props: Props) => {
e.stopPropagation();
onDelay();
}}
color={delayManager.formatDelayColor(delay)}
color={delayManager.formatDelayColor(delay, timeout)}
sx={({ palette }) =>
!proxy.provider
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
: {}
}
>
{delayManager.formatDelay(delay)}
{delayManager.formatDelay(delay, timeout)}
</Widget>
)}
+10 -4
View File
@@ -14,6 +14,7 @@ import {
} from "@mui/material";
import { BaseLoading } from "@/components/base";
import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
interface Props {
groupName: string;
@@ -48,7 +49,8 @@ export const ProxyItem = (props: Props) => {
// -1/<=0 为 不显示
// -2 为 loading
const [delay, setDelay] = useState(-1);
const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
@@ -64,7 +66,7 @@ export const ProxyItem = (props: Props) => {
const onDelay = useLockFn(async () => {
setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName));
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
});
return (
@@ -104,6 +106,10 @@ export const ProxyItem = (props: Props) => {
)}
{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>}
</>
}
/>
@@ -145,14 +151,14 @@ export const ProxyItem = (props: Props) => {
e.stopPropagation();
onDelay();
}}
color={delayManager.formatDelayColor(delay)}
color={delayManager.formatDelayColor(delay, timeout)}
sx={({ palette }) =>
!proxy.provider
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
: {}
}
>
{delayManager.formatDelay(delay)}
{delayManager.formatDelay(delay, timeout)}
</Widget>
)}
+17 -5
View File
@@ -31,12 +31,24 @@ export const ProxyRender = (props: RenderProps) => {
props;
const { type, group, headState, proxy, proxyCol } = item;
if (type === 0) {
if (type === 0 && !group.hidden) {
return (
<ListItemButton
dense
onClick={() => onHeadState(group.name, { open: !headState?.open })}
>
{group.icon && group.icon.trim().startsWith("http") && (
<img src={group.icon} height="40px" style={{ marginRight: "8px" }} />
)}
{group.icon && group.icon.trim().startsWith("data") && (
<img src={group.icon} height="40px" style={{ marginRight: "8px" }} />
)}
{group.icon && group.icon.trim().startsWith("<svg") && (
<img
src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
height="40px"
/>
)}
<ListItemText
primary={group.name}
secondary={
@@ -61,7 +73,7 @@ export const ProxyRender = (props: RenderProps) => {
);
}
if (type === 1) {
if (type === 1 && !group.hidden) {
return (
<ProxyHead
sx={{ pl: indent ? 4.5 : 2.5, pr: 3, mt: indent ? 1 : 0.5, mb: 1 }}
@@ -74,7 +86,7 @@ export const ProxyRender = (props: RenderProps) => {
);
}
if (type === 2) {
if (type === 2 && !group.hidden) {
return (
<ProxyItem
groupName={group.name}
@@ -87,7 +99,7 @@ export const ProxyRender = (props: RenderProps) => {
);
}
if (type === 3) {
if (type === 3 && !group.hidden) {
return (
<Box
sx={{
@@ -105,7 +117,7 @@ export const ProxyRender = (props: RenderProps) => {
);
}
if (type === 4) {
if (type === 4 && !group.hidden) {
return (
<Box
sx={{
+1 -1
View File
@@ -16,7 +16,7 @@ type HeadStateStorage = Record<string, Record<string, HeadState>>;
const HEAD_STATE_KEY = "proxy-head-state";
export const DEFAULT_STATE: HeadState = {
open: false,
showType: false,
showType: true,
sortType: 0,
filterText: "",
textState: null,
+159
View File
@@ -0,0 +1,159 @@
import dayjs from "dayjs";
import useSWR, { mutate } from "swr";
import { useState } from "react";
import {
Button,
IconButton,
List,
ListItem,
ListItemText,
Typography,
styled,
Box,
alpha,
Divider,
} from "@mui/material";
import { RefreshRounded } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { getRuleProviders, ruleProviderUpdate } from "@/services/api";
import { BaseDialog } from "../base";
export const ProviderButton = () => {
const { t } = useTranslation();
const { data } = useSWR("getRuleProviders", getRuleProviders);
const [open, setOpen] = useState(false);
const hasProvider = Object.keys(data || {}).length > 0;
const handleUpdate = useLockFn(async (key: string) => {
await ruleProviderUpdate(key);
await mutate("getRules");
await mutate("getRuleProviders");
});
if (!hasProvider) return null;
return (
<>
<Button
size="small"
variant="outlined"
sx={{ textTransform: "capitalize" }}
onClick={() => setOpen(true)}
>
{t("Provider")}
</Button>
<BaseDialog
open={open}
title={
<Box display="flex" justifyContent="space-between" gap={1}>
<Typography variant="h6">{t("Rule Provider")}</Typography>
<Button
variant="contained"
onClick={async () => {
Object.entries(data || {}).forEach(async ([key, item]) => {
await ruleProviderUpdate(key);
await mutate("getRules");
await mutate("getRuleProviders");
});
}}
>
{t("Update All")}
</Button>
</Box>
}
contentSx={{ width: 400 }}
disableOk
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(data || {}).map(([key, item]) => {
const time = dayjs(item.updatedAt);
return (
<>
<ListItem
sx={(theme) => ({
p: 0,
borderRadius: "10px",
boxShadow: theme.shadows[2],
mb: 1,
})}
key={key}
>
<ListItemText
sx={{ px: 1 }}
primary={
<>
<Typography
variant="h6"
component="span"
noWrap
title={key}
>
{key}
</Typography>
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
{item.ruleCount}
</TypeBox>
</>
}
secondary={
<>
<StyledTypeBox component="span">
{item.vehicleType}
</StyledTypeBox>
<StyledTypeBox component="span">
{item.behavior}
</StyledTypeBox>
<StyledTypeBox component="span">
{t("Update At")} {time.fromNow()}
</StyledTypeBox>
</>
}
/>
<Divider orientation="vertical" flexItem />
<IconButton
size="small"
color="inherit"
title="Update Provider"
onClick={() => handleUpdate(key)}
>
<RefreshRounded />
</IconButton>
</ListItem>
</>
);
})}
</List>
</BaseDialog>
</>
);
};
const TypeBox = styled(Box)(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.secondary.main, 0.5),
color: alpha(theme.palette.secondary.main, 0.8),
borderRadius: 4,
fontSize: 10,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));
const StyledTypeBox = styled(Box)(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.primary.main, 0.5),
color: alpha(theme.palette.primary.main, 0.8),
borderRadius: 4,
fontSize: 10,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));
@@ -4,7 +4,8 @@ import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { useLockFn } from "ahooks";
import { Lock } from "@mui/icons-material";
import { LoadingButton } from "@mui/lab";
import { SwitchAccessShortcut, RestartAlt } from "@mui/icons-material";
import {
Box,
Button,
@@ -14,7 +15,7 @@ import {
ListItemText,
} from "@mui/material";
import { changeClashCore, restartSidecar } from "@/services/cmds";
import { closeAllConnections } from "@/services/api";
import { closeAllConnections, upgradeCore } from "@/services/api";
import { grantPermission } from "@/services/cmds";
import getSystem from "@/utils/get-system";
@@ -31,6 +32,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const { verge, mutateVerge } = useVerge();
const [open, setOpen] = useState(false);
const [upgrading, setUpgrading] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
@@ -76,21 +78,52 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
}
});
const onUpgrade = useLockFn(async () => {
try {
setUpgrading(true);
await upgradeCore();
setUpgrading(false);
Notice.success(`Successfully upgrade core`, 1000);
} catch (err: any) {
setUpgrading(false);
Notice.error(err?.response.data.message || err.toString());
}
});
return (
<BaseDialog
open={open}
title={
<Box display="flex" justifyContent="space-between">
{t("Clash Core")}
<Button variant="contained" size="small" onClick={onRestart}>
{t("Restart")}
</Button>
<Box>
{clash_core !== "clash-meta" && (
<LoadingButton
variant="contained"
size="small"
startIcon={<SwitchAccessShortcut />}
loadingPosition="start"
loading={upgrading}
sx={{ marginRight: "8px" }}
onClick={onUpgrade}
>
{t("Upgrade")}
</LoadingButton>
)}
<Button
variant="contained"
size="small"
onClick={onRestart}
startIcon={<RestartAlt />}
>
{t("Restart")}
</Button>
</Box>
</Box>
}
contentSx={{
pb: 0,
width: 320,
width: 400,
height: 180,
overflowY: "auto",
userSelect: "text",
@@ -1,123 +0,0 @@
import useSWR from "swr";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { Checkbox, Divider, Stack, Tooltip, Typography } from "@mui/material";
import { InfoRounded } from "@mui/icons-material";
import { getRuntimeExists } from "@/services/cmds";
import {
HANDLE_FIELDS,
DEFAULT_FIELDS,
OTHERS_FIELDS,
} from "@/utils/clash-fields";
import { BaseDialog, DialogRef } from "@/components/base";
import { useProfiles } from "@/hooks/use-profiles";
import { Notice } from "@/components/base";
const otherFields = [...OTHERS_FIELDS];
const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS];
export const ClashFieldViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { profiles = {}, patchProfiles } = useProfiles();
const { data: existsKeys = [], mutate: mutateExists } = useSWR(
"getRuntimeExists",
getRuntimeExists
);
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState<string[]>([]);
useImperativeHandle(ref, () => ({
open: () => {
mutateExists();
setSelected(profiles.valid || []);
setOpen(true);
},
close: () => setOpen(false),
}));
const handleChange = (item: string) => {
if (!item) return;
setSelected((old) =>
old.includes(item) ? old.filter((e) => e !== item) : [...old, item]
);
};
const handleSave = async () => {
setOpen(false);
const oldSet = new Set(profiles.valid || []);
const curSet = new Set(selected);
const joinSet = new Set(selected.concat([...oldSet]));
if (curSet.size === oldSet.size && curSet.size === joinSet.size) return;
try {
await patchProfiles({ valid: [...curSet] });
// Notice.success("Refresh clash config", 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
};
return (
<BaseDialog
open={open}
title={t("Clash Field")}
contentSx={{
pb: 0,
width: 320,
height: 300,
overflowY: "auto",
userSelect: "text",
}}
okBtn={t("Save")}
cancelBtn={t("Back")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={handleSave}
>
{otherFields.map((item) => {
const inSelect = selected.includes(item);
const inConfig = existsKeys.includes(item);
return (
<Stack key={item} mb={0.5} direction="row" alignItems="center">
<Checkbox
checked={inSelect}
size="small"
sx={{ p: 0.5 }}
onChange={() => handleChange(item)}
/>
<Typography width="100%">{item}</Typography>
{!inSelect && inConfig && <WarnIcon />}
</Stack>
);
})}
<Divider sx={{ my: 1 }}>
<Typography color="text.secondary" fontSize={14}>
Clash Verge Control Fields
</Typography>
</Divider>
{handleFields.map((item) => (
<Stack key={item} mb={0.5} direction="row" alignItems="center">
<Checkbox defaultChecked disabled size="small" sx={{ p: 0.5 }} />
<Typography>{item}</Typography>
</Stack>
))}
</BaseDialog>
);
});
function WarnIcon() {
return (
<Tooltip title="The field exists in the config but not enabled.">
<InfoRounded color="warning" sx={{ cursor: "pointer", opacity: 0.5 }} />
</Tooltip>
);
}
@@ -13,26 +13,42 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
);
const [socksPort, setSocksPort] = useState(
verge?.verge_socks_port ?? clashInfo?.socks_port ?? 7898
);
const [port, setPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.port ?? 7890
verge?.verge_port ?? clashInfo?.port ?? 7899
);
useImperativeHandle(ref, () => ({
open: () => {
if (verge?.verge_mixed_port) setPort(verge?.verge_mixed_port);
if (verge?.verge_mixed_port) setMixedPort(verge?.verge_mixed_port);
if (verge?.verge_socks_port) setSocksPort(verge?.verge_socks_port);
if (verge?.verge_port) setPort(verge?.verge_port);
setOpen(true);
},
close: () => setOpen(false),
}));
const onSave = useLockFn(async () => {
if (port === verge?.verge_mixed_port) {
if (
mixedPort === verge?.verge_mixed_port &&
socksPort === verge?.verge_socks_port &&
port === verge?.verge_port
) {
setOpen(false);
return;
}
try {
await patchInfo({ "mixed-port": port });
await patchVerge({ verge_mixed_port: port });
await patchInfo({ "mixed-port": mixedPort });
await patchInfo({ "socks-port": socksPort });
await patchInfo({ port });
await patchVerge({ verge_mixed_port: mixedPort });
await patchVerge({ verge_socks_port: socksPort });
await patchVerge({ verge_port: port });
setOpen(false);
Notice.success("Change Clash port successfully!", 1000);
} catch (err: any) {
@@ -54,6 +70,30 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Mixed Port" />
<TextField
size="small"
autoComplete="off"
sx={{ width: 135 }}
value={mixedPort}
onChange={(e) =>
setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Socks Port" />
<TextField
size="small"
autoComplete="off"
sx={{ width: 135 }}
value={socksPort}
onChange={(e) =>
setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Http Port" />
<TextField
size="small"
autoComplete="off"
@@ -36,19 +36,6 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)}
>
<List>
<SettingItem label={t("Theme Blur")}>
<GuardState
value={verge?.theme_blur ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ theme_blur: e })}
onGuard={(e) => patchVerge({ theme_blur: e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem label={t("Traffic Graph")}>
<GuardState
value={verge?.traffic_graph ?? true}
+25 -15
View File
@@ -20,12 +20,12 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const [open, setOpen] = useState(false);
const [values, setValues] = useState({
appLogLevel: "info",
autoCloseConnection: false,
enableClashFields: false,
autoCloseConnection: true,
enableBuiltinEnhanced: true,
proxyLayoutColumn: 6,
defaultLatencyTest: "",
autoLogClean: 0,
defaultLatencyTimeout: 10000,
});
useImperativeHandle(ref, () => ({
@@ -34,11 +34,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
setValues({
appLogLevel: verge?.app_log_level ?? "info",
autoCloseConnection: verge?.auto_close_connection ?? true,
enableClashFields: verge?.enable_clash_fields ?? false,
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
proxyLayoutColumn: verge?.proxy_layout_column || 6,
defaultLatencyTest: verge?.default_latency_test || "",
autoLogClean: verge?.auto_log_clean || 0,
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
});
},
close: () => setOpen(false),
@@ -49,10 +49,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
await patchVerge({
app_log_level: values.appLogLevel,
auto_close_connection: values.autoCloseConnection,
enable_clash_fields: values.enableClashFields,
enable_builtin_enhanced: values.enableBuiltinEnhanced,
proxy_layout_column: values.proxyLayoutColumn,
default_latency_test: values.defaultLatencyTest,
default_latency_timeout: values.defaultLatencyTimeout,
auto_log_clean: values.autoLogClean as any,
});
setOpen(false);
@@ -105,17 +105,6 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Enable Clash Fields Filter")} />
<Switch
edge="end"
checked={values.enableClashFields}
onChange={(_, c) =>
setValues((v) => ({ ...v, enableClashFields: c }))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Enable Builtin Enhanced")} />
<Switch
@@ -193,6 +182,27 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Timeout")} />
<TextField
size="small"
type="number"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
value={values.defaultLatencyTimeout}
placeholder="http://1.1.1.1"
onChange={(e) =>
setValues((v) => ({
...v,
defaultLatencyTimeout: parseInt(e.target.value),
}))
}
/>
</ListItem>
</List>
</BaseDialog>
);
@@ -168,6 +168,6 @@ const FlexBox = styled("div")`
.label {
flex: none;
width: 80px;
width: 85px;
}
`;
+192
View File
@@ -0,0 +1,192 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import {
List,
ListItem,
ListItemText,
MenuItem,
Select,
Switch,
TextField,
} from "@mui/material";
import { useClash } from "@/hooks/use-clash";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
export const TunViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { clash, mutateClash, patchClash } = useClash();
const [open, setOpen] = useState(false);
const [values, setValues] = useState({
stack: "gVisor",
device: "Mihomo",
autoRoute: true,
autoDetectInterface: true,
dnsHijack: ["any:53", "tcp://any:53"],
strictRoute: true,
mtu: 9000,
});
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
setValues({
stack: clash?.tun.stack ?? "gVisor",
device: clash?.tun.device ?? "Mihomo",
autoRoute: clash?.tun["auto-route"] ?? true,
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53", "tcp://any:53"],
strictRoute: clash?.tun["strict-route"] ?? true,
mtu: clash?.tun.mtu ?? 9000,
});
},
close: () => setOpen(false),
}));
const onSave = useLockFn(async () => {
try {
let tun = {
stack: values.stack,
device: values.device,
"auto-route": values.autoRoute,
"auto-detect-interface": values.autoDetectInterface,
"dns-hijack": values.dnsHijack,
"strict-route": values.strictRoute,
mtu: values.mtu,
};
await patchClash({ tun });
await mutateClash(
(old) => ({
...(old! || {}),
tun,
}),
false
);
setOpen(false);
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
return (
<BaseDialog
open={open}
title={t("Tun Mode")}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Stack")} />
<Select
size="small"
sx={{ width: 100, "> div": { py: "7.5px" } }}
value={values.stack}
onChange={(e) => {
setValues((v) => ({
...v,
stack: e.target.value as string,
}));
}}
>
{["System", "gVisor", "Mixed"].map((i) => (
<MenuItem value={i} key={i}>
{i}
</MenuItem>
))}
</Select>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Device")} />
<TextField
size="small"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
value={values.device}
placeholder="Mihomo"
onChange={(e) =>
setValues((v) => ({ ...v, device: e.target.value }))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Route")} />
<Switch
edge="end"
checked={values.autoRoute}
onChange={(_, c) => setValues((v) => ({ ...v, autoRoute: c }))}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Strict Route")} />
<Switch
edge="end"
checked={values.strictRoute}
onChange={(_, c) => setValues((v) => ({ ...v, strictRoute: c }))}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Detect Interface")} />
<Switch
edge="end"
checked={values.autoDetectInterface}
onChange={(_, c) =>
setValues((v) => ({ ...v, autoDetectInterface: c }))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("DNS Hijack")} />
<TextField
size="small"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
value={values.dnsHijack.join(",")}
placeholder="Please use , to separate multiple DNS servers"
onChange={(e) =>
setValues((v) => ({ ...v, dnsHijack: e.target.value.split(",") }))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("MTU")} />
<TextField
size="small"
type="number"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
value={values.mtu}
placeholder="9000"
onChange={(e) =>
setValues((v) => ({
...v,
mtu: parseInt(e.target.value),
}))
}
/>
</ListItem>
</List>
</BaseDialog>
);
});
@@ -10,6 +10,7 @@ import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { atomUpdateState } from "@/services/states";
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
import { portableFlag } from "@/pages/_layout";
const UpdateLog = styled(Box)(() => ({
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
@@ -46,6 +47,10 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
}, [updateInfo]);
const onUpdate = useLockFn(async () => {
if (portableFlag) {
Notice.error(t("Portable Updater Error"));
return;
}
if (updateState) return;
setUpdateState(true);
if (eventListener !== null) {
@@ -59,7 +59,7 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
.slice(clashInfo.server.indexOf(":") + 1)
.trim();
url = url.replaceAll("%port", port || "9090");
url = url.replaceAll("%port", port || "9097");
url = url.replaceAll(
"%secret",
encodeURIComponent(clashInfo.secret || "")
+25 -24
View File
@@ -1,5 +1,6 @@
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import {
TextField,
Switch,
@@ -14,7 +15,6 @@ import { DialogRef, Notice } from "@/components/base";
import { useClash } from "@/hooks/use-clash";
import { GuardState } from "./mods/guard-state";
import { WebUIViewer } from "./mods/web-ui-viewer";
import { ClashFieldViewer } from "./mods/clash-field-viewer";
import { ClashPortViewer } from "./mods/clash-port-viewer";
import { ControllerViewer } from "./mods/controller-viewer";
import { SettingList, SettingItem } from "./mods/setting-comp";
@@ -22,6 +22,7 @@ import { ClashCoreViewer } from "./mods/clash-core-viewer";
import { invoke_uwp_tool } from "@/services/cmds";
import getSystem from "@/utils/get-system";
import { useVerge } from "@/hooks/use-verge";
import { updateGeoData } from "@/services/api";
const isWIN = getSystem() === "windows";
@@ -33,19 +34,13 @@ const SettingClash = ({ onError }: Props) => {
const { t } = useTranslation();
const { clash, version, mutateClash, patchClash } = useClash();
const { verge, mutateVerge, patchVerge } = useVerge();
const { ipv6, "allow-lan": allowLan, "log-level": logLevel } = clash ?? {};
const {
enable_random_port = false,
verge_mixed_port,
enable_clash_fields = false,
} = verge ?? {};
const { enable_random_port = false, verge_mixed_port } = verge ?? {};
const webRef = useRef<DialogRef>(null);
const fieldRef = useRef<DialogRef>(null);
const portRef = useRef<DialogRef>(null);
const ctrlRef = useRef<DialogRef>(null);
const coreRef = useRef<DialogRef>(null);
@@ -57,10 +52,18 @@ const SettingClash = ({ onError }: Props) => {
const onChangeVerge = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
};
const onUpdateGeo = useLockFn(async () => {
try {
await updateGeoData();
Notice.success("Start update geodata");
} catch (err: any) {
Notice.error(err?.response.data.message || err.toString());
}
});
return (
<SettingList title={t("Clash Setting")}>
<WebUIViewer ref={webRef} />
<ClashFieldViewer ref={fieldRef} />
<ClashPortViewer ref={portRef} />
<ControllerViewer ref={ctrlRef} />
<ClashCoreViewer ref={coreRef} />
@@ -111,7 +114,7 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
label={t("Mixed Port")}
label={t("Port Config")}
extra={
<Tooltip title={t("Random Port")}>
<IconButton
@@ -135,7 +138,7 @@ const SettingClash = ({ onError }: Props) => {
disabled={enable_random_port}
autoComplete="off"
size="small"
value={verge_mixed_port ?? 7890}
value={verge_mixed_port ?? 7897}
sx={{ width: 100, input: { py: "7.5px", cursor: "pointer" } }}
onClick={(e) => {
portRef.current?.open();
@@ -166,19 +169,6 @@ const SettingClash = ({ onError }: Props) => {
</IconButton>
</SettingItem>
{enable_clash_fields && (
<SettingItem label={t("Clash Field")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => fieldRef.current?.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
)}
<SettingItem
label={t("Clash Core")}
extra={
@@ -209,6 +199,17 @@ const SettingClash = ({ onError }: Props) => {
</IconButton>
</SettingItem>
)}
<SettingItem label={t("Update GeoData")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={onUpdateGeo}
>
<ArrowForward />
</IconButton>
</SettingItem>
</SettingList>
);
};
+35 -3
View File
@@ -1,8 +1,8 @@
import useSWR from "swr";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { IconButton, Switch } from "@mui/material";
import { ArrowForward, PrivacyTipRounded, Settings } from "@mui/icons-material";
import { IconButton, Switch, Tooltip } from "@mui/material";
import { PrivacyTipRounded, Settings, InfoRounded } from "@mui/icons-material";
import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import { DialogRef } from "@/components/base";
@@ -10,6 +10,7 @@ import { SettingList, SettingItem } from "./mods/setting-comp";
import { GuardState } from "./mods/guard-state";
import { ServiceViewer } from "./mods/service-viewer";
import { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer";
import getSystem from "@/utils/get-system";
interface Props {
@@ -36,6 +37,7 @@ const SettingSystem = ({ onError }: Props) => {
const serviceRef = useRef<DialogRef>(null);
const sysproxyRef = useRef<DialogRef>(null);
const tunRef = useRef<DialogRef>(null);
const {
enable_tun_mode,
@@ -53,11 +55,41 @@ const SettingSystem = ({ onError }: Props) => {
return (
<SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} />
{isWIN && (
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
)}
<SettingItem label={t("Tun Mode")}>
<SettingItem
label={t("Tun Mode")}
extra={
<>
<Tooltip
title={
isWIN ? t("Tun Mode Info Windows") : t("Tun Mode Info Unix")
}
placement="top"
>
<IconButton color="inherit" size="small">
<InfoRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</Tooltip>
<IconButton
color="inherit"
size="small"
onClick={() => tunRef.current?.open()}
>
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</>
}
>
<GuardState
value={enable_tun_mode ?? false}
valueProps="checked"
+122 -16
View File
@@ -1,8 +1,16 @@
import { useRef } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { IconButton, MenuItem, Select, Typography } from "@mui/material";
import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
import { open } from "@tauri-apps/api/dialog";
import {
Button,
IconButton,
MenuItem,
Select,
Input,
Typography,
} from "@mui/material";
import { exitApp, openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
import { ArrowForward } from "@mui/icons-material";
import { checkUpdate } from "@tauri-apps/api/updater";
import { useVerge } from "@/hooks/use-verge";
@@ -18,7 +26,7 @@ import { GuardState } from "./mods/guard-state";
import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer";
import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers";
interface Props {
onError?: (err: Error) => void;
}
@@ -29,7 +37,14 @@ const SettingVerge = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
const { theme_mode, language, tray_event } = verge ?? {};
const {
theme_mode,
language,
tray_event,
env_type,
startup_script,
start_page,
} = verge ?? {};
const configRef = useRef<DialogRef>(null);
const hotkeyRef = useRef<DialogRef>(null);
const miscRef = useRef<DialogRef>(null);
@@ -109,6 +124,86 @@ const SettingVerge = ({ onError }: Props) => {
</SettingItem>
)}
<SettingItem label={t("Copy Env Type")}>
<GuardState
value={env_type ?? (OS === "windows" ? "powershell" : "bash")}
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ env_type: e })}
onGuard={(e) => patchVerge({ env_type: e })}
>
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
<MenuItem value="bash">Bash</MenuItem>
<MenuItem value="cmd">CMD</MenuItem>
<MenuItem value="powershell">PowerShell</MenuItem>
</Select>
</GuardState>
</SettingItem>
<SettingItem label={t("Start Page")}>
<GuardState
value={start_page ?? "/"}
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ start_page: e })}
onGuard={(e) => patchVerge({ start_page: e })}
>
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
{routers.map((page: { label: string; link: string }) => {
return <MenuItem value={page.link}>{t(page.label)}</MenuItem>;
})}
</Select>
</GuardState>
</SettingItem>
<SettingItem label={t("Startup Script")}>
<GuardState
value={startup_script ?? ""}
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ startup_script: e })}
onGuard={(e) => patchVerge({ startup_script: e })}
>
<Input
value={startup_script}
disabled
endAdornment={
<>
<Button
onClick={async () => {
const path = await open({
directory: false,
multiple: false,
filters: [
{
name: "Shell Script",
extensions: ["sh", "bat", "ps1"],
},
],
});
if (path?.length) {
onChangeData({ startup_script: `${path}` });
patchVerge({ startup_script: `${path}` });
}
}}
>
{t("Browse")}
</Button>
{startup_script && (
<Button
onClick={async () => {
onChangeData({ startup_script: "" });
patchVerge({ startup_script: "" });
}}
>
{t("Clear")}
</Button>
)}
</>
}
></Input>
</GuardState>
</SettingItem>
<SettingItem label={t("Theme Setting")}>
<IconButton
color="inherit"
@@ -197,18 +292,29 @@ const SettingVerge = ({ onError }: Props) => {
</IconButton>
</SettingItem>
{!(OS === "windows" && WIN_PORTABLE) && (
<SettingItem label={t("Check for Updates")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={onCheckUpdate}
>
<ArrowForward />
</IconButton>
</SettingItem>
)}
<SettingItem label={t("Check for Updates")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={onCheckUpdate}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Exit")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => {
exitApp();
}}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Verge Version")}>
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
+42
View File
@@ -0,0 +1,42 @@
import { alpha, Box, styled } from "@mui/material";
export const TestBox = styled(Box)(({ theme, "aria-selected": selected }) => {
const { mode, primary, text, grey, background } = theme.palette;
const key = `${mode}-${!!selected}`;
const backgroundColor = {
"light-true": alpha(primary.main, 0.2),
"light-false": alpha(background.paper, 0.75),
"dark-true": alpha(primary.main, 0.45),
"dark-false": alpha(grey[700], 0.45),
}[key]!;
const color = {
"light-true": text.secondary,
"light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.85),
"dark-false": alpha(text.secondary, 0.65),
}[key]!;
const h2color = {
"light-true": primary.main,
"light-false": text.primary,
"dark-true": primary.light,
"dark-false": text.primary,
}[key]!;
return {
position: "relative",
width: "100%",
display: "block",
cursor: "pointer",
textAlign: "left",
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
padding: "8px 16px",
boxSizing: "border-box",
backgroundColor,
color,
"& h2": { color: h2color },
};
});
+216
View File
@@ -0,0 +1,216 @@
import { useEffect, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
Box,
Typography,
Divider,
MenuItem,
Menu,
styled,
alpha,
} from "@mui/material";
import { BaseLoading } from "@/components/base";
import { LanguageTwoTone } from "@mui/icons-material";
import { Notice } from "@/components/base";
import { TestBox } from "./test-box";
import delayManager from "@/services/delay";
import { cmdTestDelay } from "@/services/cmds";
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
interface Props {
id: string;
itemData: IVergeTestItem;
onEdit: () => void;
onDelete: (uid: string) => void;
}
let eventListener: UnlistenFn | null = null;
export const TestItem = (props: Props) => {
const { itemData, onEdit, onDelete: onDeleteItem } = props;
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.id });
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const [delay, setDelay] = useState(-1);
const { uid, name, icon, url } = itemData;
const onDelay = async () => {
setDelay(-2);
const result = await cmdTestDelay(url);
setDelay(result);
};
const onEditTest = () => {
setAnchorEl(null);
onEdit();
};
const onDelete = useLockFn(async () => {
setAnchorEl(null);
try {
onDeleteItem(uid);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const menu = [
{ label: "Edit", handler: onEditTest },
{ label: "Delete", handler: onDelete },
];
const listenTsetEvent = async () => {
if (eventListener !== null) {
eventListener();
}
eventListener = await listen("verge://test-all", () => {
onDelay();
});
};
useEffect(() => {
listenTsetEvent();
}, []);
return (
<Box
sx={{
transform: CSS.Transform.toString(transform),
transition,
}}
>
<TestBox
onClick={onEditTest}
onContextMenu={(event) => {
const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX });
setAnchorEl(event.currentTarget);
event.preventDefault();
}}
>
<Box
position="relative"
sx={{ cursor: "move" }}
ref={setNodeRef}
{...attributes}
{...listeners}
>
{icon && icon.trim() !== "" ? (
<Box sx={{ display: "flex", justifyContent: "center" }}>
{icon.trim().startsWith("http") && (
<img src={icon} height="40px" style={{ marginRight: "8px" }} />
)}
{icon.trim().startsWith("data") && (
<img src={icon} height="40px" style={{ marginRight: "8px" }} />
)}
{icon.trim().startsWith("<svg") && (
<img
src={`data:image/svg+xml;base64,${btoa(icon)}`}
height="40px"
/>
)}
</Box>
) : (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<LanguageTwoTone sx={{ height: "40px" }} fontSize="large" />
</Box>
)}
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Typography variant="h6" component="h2" noWrap title={name}>
{name}
</Typography>
</Box>
</Box>
<Divider sx={{ marginTop: "8px" }} />
<Box
sx={{
display: "flex",
justifyContent: "center",
marginTop: "8px",
color: "primary.main",
}}
>
{delay === -2 && (
<Widget>
<BaseLoading />
</Widget>
)}
{delay === -1 && (
<Widget
className="the-check"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelay();
}}
sx={({ palette }) => ({
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
})}
>
Check
</Widget>
)}
{delay >= 0 && (
// 显示延迟
<Widget
className="the-delay"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelay();
}}
color={delayManager.formatDelayColor(delay)}
sx={({ palette }) => ({
":hover": {
bgcolor: alpha(palette.primary.main, 0.15),
},
})}
>
{delayManager.formatDelay(delay)}
</Widget>
)}
</Box>
</TestBox>
<Menu
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorPosition={position}
anchorReference="anchorPosition"
transitionDuration={225}
MenuListProps={{ sx: { py: 0.5 } }}
onContextMenu={(e) => {
setAnchorEl(null);
e.preventDefault();
}}
>
{menu.map((item) => (
<MenuItem
key={item.label}
onClick={item.handler}
sx={{ minWidth: 120 }}
dense
>
{t(item.label)}
</MenuItem>
))}
</Menu>
</Box>
);
};
const Widget = styled(Box)(({ theme: { typography } }) => ({
padding: "3px 6px",
fontSize: 14,
fontFamily: typography.fontFamily,
borderRadius: "4px",
}));
+154
View File
@@ -0,0 +1,154 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { useForm, Controller } from "react-hook-form";
import { TextField } from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, Notice } from "@/components/base";
import { nanoid } from "nanoid";
interface Props {
onChange: (uid: string, patch?: Partial<IVergeTestItem>) => void;
}
export interface TestViewerRef {
create: () => void;
edit: (item: IVergeTestItem) => void;
}
// create or edit the test item
export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [openType, setOpenType] = useState<"new" | "edit">("new");
const [loading, setLoading] = useState(false);
const { verge, patchVerge } = useVerge();
const testList = verge?.test_list ?? [];
const { control, watch, register, ...formIns } = useForm<IVergeTestItem>({
defaultValues: {
name: "",
icon: "",
url: "",
},
});
const patchTestList = async (uid: string, patch: Partial<IVergeTestItem>) => {
const newList = testList.map((x) => {
if (x.uid === uid) {
return { ...x, ...patch };
}
return x;
});
await patchVerge({ ...verge, test_list: newList });
};
useImperativeHandle(ref, () => ({
create: () => {
setOpenType("new");
setOpen(true);
},
edit: (item) => {
if (item) {
Object.entries(item).forEach(([key, value]) => {
formIns.setValue(key as any, value);
});
}
setOpenType("edit");
setOpen(true);
},
}));
const handleOk = useLockFn(
formIns.handleSubmit(async (form) => {
setLoading(true);
try {
if (!form.name) throw new Error("`Name` should not be null");
if (!form.url) throw new Error("`Url` should not be null");
let newList;
let uid;
if (openType === "new") {
uid = nanoid();
const item = { ...form, uid };
newList = [...testList, item];
await patchVerge({ test_list: newList });
props.onChange(uid);
} else {
if (!form.uid) throw new Error("UID not found");
uid = form.uid;
await patchTestList(uid, form);
props.onChange(uid, form);
}
setOpen(false);
setLoading(false);
setTimeout(() => formIns.reset(), 500);
} catch (err: any) {
Notice.error(err.message || err.toString());
setLoading(false);
}
})
);
const handleClose = () => {
setOpen(false);
setTimeout(() => formIns.reset(), 500);
};
const text = {
fullWidth: true,
size: "small",
margin: "normal",
variant: "outlined",
autoComplete: "off",
autoCorrect: "off",
} as const;
return (
<BaseDialog
open={open}
title={openType === "new" ? t("Create Test") : t("Edit Test")}
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={handleClose}
onCancel={handleClose}
onOk={handleOk}
loading={loading}
>
<Controller
name="name"
control={control}
render={({ field }) => (
<TextField {...text} {...field} label={t("Name")} />
)}
/>
<Controller
name="icon"
control={control}
render={({ field }) => (
<TextField
{...text}
{...field}
multiline
maxRows={5}
label={t("Icon")}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field }) => (
<TextField
{...text}
{...field}
multiline
maxRows={3}
label={t("Test URL")}
/>
)}
/>
</BaseDialog>
);
});
+26 -1
View File
@@ -48,11 +48,16 @@ export const useClashInfo = () => {
const patchInfo = async (
patch: Partial<
Pick<IConfigData, "mixed-port" | "external-controller" | "secret">
Pick<
IConfigData,
"port" | "socks-port" | "mixed-port" | "external-controller" | "secret"
>
>
) => {
const hasInfo =
patch["mixed-port"] != null ||
patch["socks-port"] != null ||
patch["port"] != null ||
patch["external-controller"] != null ||
patch.secret != null;
@@ -68,6 +73,26 @@ export const useClashInfo = () => {
}
}
if (patch["socks-port"]) {
const port = patch["socks-port"];
if (port < 1000) {
throw new Error("The port should not < 1000");
}
if (port > 65536) {
throw new Error("The port should not > 65536");
}
}
if (patch["port"]) {
const port = patch["port"];
if (port < 1000) {
throw new Error("The port should not < 1000");
}
if (port > 65536) {
throw new Error("The port should not > 65536");
}
}
await patchClashConfig(patch);
mutateInfo();
mutate("getClashConfig");

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