mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-13 14:22:08 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f59be465ea | ||
|
|
27305317ce | ||
|
|
8378320e50 | ||
|
|
86a69952db | ||
|
|
e212ebfdb9 | ||
|
|
f235125d48 | ||
|
|
8efa815eb9 | ||
|
|
e54d42576b | ||
|
|
1e18539862 | ||
|
|
673fdf4721 | ||
|
|
57d6bfba51 | ||
|
|
8fa23aecda | ||
|
|
f9c10533e8 | ||
|
|
c891c3723e | ||
|
|
a0625ef2e7 | ||
|
|
b2774abb54 | ||
|
|
fa089bbc40 | ||
|
|
8437d1763f | ||
|
|
0532b0f9df | ||
|
|
5f8eb853f0 | ||
|
|
9125a85382 | ||
|
|
5b3dc44c72 | ||
|
|
ee5147f111 | ||
|
|
f67137fd5e | ||
|
|
b67db4a896 | ||
|
|
067018828c | ||
|
|
e990530ada | ||
|
|
45f863a72a | ||
|
|
519febbeb2 | ||
|
|
854b1a4caf | ||
|
|
08eb95e73a | ||
|
|
66f8fbf08c | ||
|
|
86a30bbb29 | ||
|
|
966c453c27 | ||
|
|
c7bcaf0193 | ||
|
|
0ac91e36a0 | ||
|
|
6f1299ce9e | ||
|
|
83ca29d649 | ||
|
|
d0206044dc | ||
|
|
d4623e4175 | ||
|
|
68ef03377d | ||
|
|
d3f9d3d033 | ||
|
|
a6ccd04471 | ||
|
|
0fd8174e77 | ||
|
|
ebc63f479a | ||
|
|
570c39c20a | ||
|
|
1d57257cf5 | ||
|
|
82d7baee0b | ||
|
|
1d91e1f1f7 | ||
|
|
15e8894614 | ||
|
|
0ae96918e9 | ||
|
|
e970880059 | ||
|
|
69ae86aba8 | ||
|
|
8142e1be49 | ||
|
|
cbc7612451 | ||
|
|
09e6a7b7cc | ||
|
|
082e35668a | ||
|
|
c9e78c837b | ||
|
|
2d171db672 | ||
|
|
a8b11abec8 | ||
|
|
b08333dccd | ||
|
|
98bad48971 | ||
|
|
53375bb536 | ||
|
|
88798093e1 | ||
|
|
45a28751af | ||
|
|
1670c44464 | ||
|
|
a286ac85dc | ||
|
|
1606adc0ab | ||
|
|
b4ce557d92 | ||
|
|
2ba7fff8d4 | ||
|
|
98efc93610 | ||
|
|
2680507eae | ||
|
|
df72392ad2 | ||
|
|
a6acb15a00 | ||
|
|
ba7242a815 | ||
|
|
99740c1324 | ||
|
|
c04b20d1fa | ||
|
|
e127067878 | ||
|
|
6f03b72368 | ||
|
|
7bd3ee9340 | ||
|
|
c94db606e5 | ||
|
|
4d2474226b | ||
|
|
1ffc4f538b | ||
|
|
6702ac957b | ||
|
|
ba42b2e77d | ||
|
|
fba18ca40a | ||
|
|
3a2a7a1476 | ||
|
|
3c6d6b90bc | ||
|
|
47dc9ee304 | ||
|
|
0045bf206c | ||
|
|
669a1a6953 | ||
|
|
e28452cc7b | ||
|
|
5ceac03db1 | ||
|
|
96215e5950 | ||
|
|
7a030b9224 | ||
|
|
e743478a4d | ||
|
|
bc29c80d44 | ||
|
|
82b8a474d7 | ||
|
|
99851b297d | ||
|
|
e3a500e12c | ||
|
|
378666e3cc | ||
|
|
b65ad1ebd7 | ||
|
|
933a821b5f | ||
|
|
159337ddbd | ||
|
|
2e25a4333a | ||
|
|
64fafb0795 | ||
|
|
5927e0bb3b | ||
|
|
e21596db83 | ||
|
|
8a608c3c3e | ||
|
|
76f9db8516 | ||
|
|
90f4809b7c | ||
|
|
3ff1af9fd6 | ||
|
|
711dd520f7 | ||
|
|
99503c836a | ||
|
|
2c8bc51862 | ||
|
|
5e7aa15232 |
@@ -1,8 +1,7 @@
|
|||||||
# pnpm install --resolution-only
|
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm check $INPUT_TARGET
|
pnpm check $INPUT_TARGET
|
||||||
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
|
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
|
||||||
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ] || [ "$INPUT_TARGET" = "i686-unknown-linux-gnu" ]; then
|
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
|
||||||
pnpm build --target $INPUT_TARGET
|
pnpm build --target $INPUT_TARGET
|
||||||
else
|
else
|
||||||
pnpm build --target $INPUT_TARGET -b deb
|
pnpm build --target $INPUT_TARGET -b deb
|
||||||
|
|||||||
@@ -10,15 +10,7 @@ rustup target add "$INPUT_TARGET"
|
|||||||
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
|
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
elif [ "$INPUT_TARGET" = "i686-unknown-linux-gnu" ]; then
|
|
||||||
dpkg --add-architecture i386
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y libstdc++6:i386 libgdk-pixbuf2.0-dev:i386 libatomic1:i386 gcc-multilib g++-multilib libwebkit2gtk-4.0-dev:i386 libssl-dev:i386 libgtk-3-dev:i386 librsvg2-dev:i386 patchelf:i386 libayatana-appindicator3-dev:i386
|
|
||||||
export PKG_CONFIG_PATH=/usr/lib/i386-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
|
||||||
export PKG_CONFIG_SYSROOT_DIR=/
|
|
||||||
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
|
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
|
||||||
sed 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch-=amd64,i386] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | tee /etc/apt/sources.list.d/ports.list
|
|
||||||
sed -i 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch=amd64,i386] http:\/\/\1.archive.ubuntu.com\/ubuntu\//g' /etc/apt/sources.list
|
|
||||||
dpkg --add-architecture arm64
|
dpkg --add-architecture arm64
|
||||||
apt-get update
|
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 libncurses6:arm64 libtinfo6:arm64 linux-libc-dev:arm64 libncursesw6:arm64 libssl3:arm64 libcups2:arm64
|
||||||
@@ -29,8 +21,6 @@ elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
|
|||||||
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
|
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
|
||||||
export PKG_CONFIG_ALLOW_CROSS=1
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
elif [ "$INPUT_TARGET" = "armv7-unknown-linux-gnueabihf" ]; then
|
elif [ "$INPUT_TARGET" = "armv7-unknown-linux-gnueabihf" ]; then
|
||||||
sed 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch-=amd64,i386] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | tee /etc/apt/sources.list.d/ports.list
|
|
||||||
sed -i 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch=amd64,i386] http:\/\/\1.archive.ubuntu.com\/ubuntu\//g' /etc/apt/sources.list
|
|
||||||
dpkg --add-architecture armhf
|
dpkg --add-architecture armhf
|
||||||
apt-get update
|
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 libncurses6:armhf libtinfo6:armhf linux-libc-dev:armhf libncursesw6:armhf libssl3:armhf libcups2:armhf
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -2,9 +2,6 @@ name: Release Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v**
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
@@ -18,8 +15,6 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
- os: windows-latest
|
|
||||||
target: i686-pc-windows-msvc
|
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: aarch64-pc-windows-msvc
|
target: aarch64-pc-windows-msvc
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
@@ -78,7 +73,7 @@ jobs:
|
|||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
with:
|
with:
|
||||||
tagName: v__VERSION__
|
tagName: v__VERSION__
|
||||||
releaseName: "Clash Verge v__VERSION__"
|
releaseName: "Clash Verge Rev v__VERSION__"
|
||||||
releaseBody: "More new features are now supported."
|
releaseBody: "More new features are now supported."
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
@@ -92,7 +87,6 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
VITE_WIN_PORTABLE: 1
|
|
||||||
|
|
||||||
release-for-linux:
|
release-for-linux:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -101,12 +95,8 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
- os: ubuntu-latest
|
|
||||||
target: i686-unknown-linux-gnu
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: aarch64-unknown-linux-gnu
|
target: aarch64-unknown-linux-gnu
|
||||||
# - os: ubuntu-latest
|
|
||||||
# target: armv7-unknown-linux-gnueabihf
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
@@ -125,11 +115,11 @@ jobs:
|
|||||||
sudo apt-get install jq
|
sudo apt-get install jq
|
||||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
if: startsWith(matrix.target, 'x86_64') || startsWith(matrix.target, 'i686')
|
if: startsWith(matrix.target, 'x86_64')
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
tag_name: v${{env.VERSION}}
|
tag_name: v${{env.VERSION}}
|
||||||
name: "Clash Verge v${{env.VERSION}}"
|
name: "Clash Verge Rev v${{env.VERSION}}"
|
||||||
body: "More new features are now supported."
|
body: "More new features are now supported."
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
|
||||||
@@ -137,21 +127,20 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
tag_name: v${{env.VERSION}}
|
tag_name: v${{env.VERSION}}
|
||||||
name: "Clash Verge v${{env.VERSION}}"
|
name: "Clash Verge Rev v${{env.VERSION}}"
|
||||||
body: "More new features are now supported."
|
body: "More new features are now supported."
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
|
|
||||||
release-update:
|
release-update:
|
||||||
needs: [release]
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
needs: [release, release-for-linux]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "20"
|
||||||
|
|
||||||
|
|||||||
@@ -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!
|
||||||
@@ -9,10 +9,28 @@
|
|||||||
A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||||
</h3>
|
</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
|
## Features
|
||||||
|
|
||||||
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
|
- 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.
|
- Simple UI and supports custom theme color.
|
||||||
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
|
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
|
||||||
- System proxy setting and guard.
|
- System proxy setting and guard.
|
||||||
@@ -37,27 +55,6 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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.4/Clash.Verge_1.4.4_x64-setup.exe)
|
|
||||||
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/Clash.Verge_1.4.4_x86-setup.exe)
|
|
||||||
- [Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/Clash.Verge_1.4.4_arm64-setup.exe)
|
|
||||||
|
|
||||||
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/Clash.Verge_1.4.4_x64.dmg)
|
|
||||||
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/Clash.Verge_1.4.4_aarch64.dmg)
|
|
||||||
|
|
||||||
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_amd64.AppImage)
|
|
||||||
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_amd64.deb)
|
|
||||||
- [Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_i386.AppImage)
|
|
||||||
- [Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_i386.deb)
|
|
||||||
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_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
|
### FAQ
|
||||||
|
|
||||||
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
|
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
|
||||||
@@ -66,34 +63,14 @@ open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/C
|
|||||||
|
|
||||||
## Development
|
## 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
|
```shell
|
||||||
pnpm i
|
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
|
pnpm run check
|
||||||
```
|
|
||||||
|
|
||||||
Then run
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm dev
|
pnpm dev
|
||||||
|
|
||||||
# run it in another way if app instance exists
|
|
||||||
pnpm dev:diff
|
|
||||||
```
|
|
||||||
|
|
||||||
Or you can build it
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Todos
|
## Todos
|
||||||
@@ -112,11 +89,10 @@ Issue and PR welcome!
|
|||||||
|
|
||||||
Clash Verge rev was based on or inspired by these projects and so on:
|
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.
|
- [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.
|
- [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.
|
- [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.
|
- [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!
|
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
|
||||||
|
|
||||||
|
|||||||
+155
@@ -1,3 +1,158 @@
|
|||||||
|
## 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
|
## v1.4.4
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
+21
-21
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "1.4.4",
|
"version": "1.5.2",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"dev:diff": "tauri dev -f verge-dev",
|
"dev:diff": "tauri dev -f verge-dev",
|
||||||
@@ -19,26 +19,27 @@
|
|||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.3",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@juggle/resize-observer": "^3.4.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/lab": "5.0.0-alpha.149",
|
||||||
"@mui/material": "^5.14.19",
|
"@mui/material": "^5.15.5",
|
||||||
"@mui/x-data-grid": "^6.18.2",
|
"@mui/x-data-grid": "^6.18.7",
|
||||||
"@tauri-apps/api": "^1.5.1",
|
"@tauri-apps/api": "^1.5.3",
|
||||||
"ahooks": "^3.7.8",
|
"ahooks": "^3.7.8",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.5",
|
||||||
"dayjs": "1.11.5",
|
"dayjs": "1.11.5",
|
||||||
"i18next": "^23.7.7",
|
"i18next": "^23.7.16",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"monaco-editor": "^0.34.1",
|
"monaco-editor": "^0.34.1",
|
||||||
|
"nanoid": "^5.0.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
"react-hook-form": "^7.48.2",
|
"react-hook-form": "^7.49.3",
|
||||||
"react-i18next": "^13.5.0",
|
"react-i18next": "^13.5.0",
|
||||||
"react-router-dom": "^6.20.0",
|
"react-router-dom": "^6.21.2",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-virtuoso": "^4.6.2",
|
"react-virtuoso": "^4.6.2",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
@@ -48,15 +49,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^5.1.1",
|
"@actions/github": "^5.1.1",
|
||||||
"@tauri-apps/cli": "^1.5.6",
|
"@tauri-apps/cli": "^1.5.9",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/lodash": "^4.14.202",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/react": "^18.2.39",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/react-transition-group": "^4.4.9",
|
"@types/react-transition-group": "^4.4.10",
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
@@ -64,10 +64,10 @@
|
|||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"pretty-quick": "^3.1.3",
|
"pretty-quick": "^3.3.1",
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.70.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^4.5.0",
|
"vite": "^5.0.11",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^4.2.0"
|
"vite-plugin-svgr": "^4.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
From 8b085aea2f11e64f433244eda092c178a2bb50bc Mon Sep 17 00:00:00 2001
|
From 871c9a6d1ed014c93da2436a437df03734e9f76c Mon Sep 17 00:00:00 2001
|
||||||
From: MystiPanda <mystipanda@proton.me>
|
From: MystiPanda <mystipanda@proton.me>
|
||||||
Date: Sun, 10 Dec 2023 19:47:45 +0800
|
Date: Sun, 10 Dec 2023 19:47:45 +0800
|
||||||
Subject: [PATCH] feat: Support windows aarch64
|
Subject: [PATCH] feat: Support windows aarch64
|
||||||
@@ -22,19 +22,19 @@ index 0000000..2eda7e4
|
|||||||
+ path = src-tauri/quick-rs
|
+ path = src-tauri/quick-rs
|
||||||
+ url = https://github.com/clash-verge-rev/quick-rs.git
|
+ url = https://github.com/clash-verge-rev/quick-rs.git
|
||||||
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
|
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
|
||||||
index 4c6dde5..5fd9ad8 100644
|
index 2f1a3be..d67f6ed 100644
|
||||||
--- a/src-tauri/Cargo.toml
|
--- a/src-tauri/Cargo.toml
|
||||||
+++ b/src-tauri/Cargo.toml
|
+++ b/src-tauri/Cargo.toml
|
||||||
@@ -25,7 +25,6 @@ log4rs = "1"
|
@@ -25,7 +25,6 @@ log4rs = "1"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
sysinfo = "0.29"
|
sysinfo = "0.30"
|
||||||
-rquickjs = "0.3" # 高版本不支持 Linux aarch64
|
-rquickjs = "0.3" # 高版本不支持 Linux aarch64
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
auto-launch = "0.5"
|
once_cell = "1.18"
|
||||||
@@ -34,6 +33,7 @@ port_scanner = "0.1.5"
|
@@ -33,6 +32,7 @@ port_scanner = "0.1.5"
|
||||||
delay_timer = "0.11"
|
delay_timer = "0.11.5"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
+quick-rs = { path = "quick-rs" }
|
+quick-rs = { path = "quick-rs" }
|
||||||
@@ -49,7 +49,7 @@ index 0000000..78277c4
|
|||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
+Subproject commit 78277c4509c64f18c0fc5c9f2b84671de7c83343
|
+Subproject commit 78277c4509c64f18c0fc5c9f2b84671de7c83343
|
||||||
diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs
|
diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs
|
||||||
index 6c207d9..d47dc33 100644
|
index 30a922f..d47dc33 100644
|
||||||
--- a/src-tauri/src/enhance/script.rs
|
--- a/src-tauri/src/enhance/script.rs
|
||||||
+++ b/src-tauri/src/enhance/script.rs
|
+++ b/src-tauri/src/enhance/script.rs
|
||||||
@@ -3,61 +3,83 @@ use anyhow::Result;
|
@@ -3,61 +3,83 @@ use anyhow::Result;
|
||||||
@@ -123,7 +123,7 @@ index 6c207d9..d47dc33 100644
|
|||||||
- if result == "\"\"" {
|
- if result == "\"\"" {
|
||||||
- anyhow::bail!("main function should return object");
|
- 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();
|
- let mut out = outputs.lock().unwrap();
|
||||||
|
|||||||
Generated
+651
-591
File diff suppressed because it is too large
Load Diff
+93
-20
@@ -13,25 +13,23 @@ const FORCE = process.argv.includes("--force");
|
|||||||
|
|
||||||
const PLATFORM_MAP = {
|
const PLATFORM_MAP = {
|
||||||
"x86_64-pc-windows-msvc": "win32",
|
"x86_64-pc-windows-msvc": "win32",
|
||||||
"i686-pc-windows-msvc": "win32",
|
|
||||||
"aarch64-pc-windows-msvc": "win32",
|
"aarch64-pc-windows-msvc": "win32",
|
||||||
"x86_64-apple-darwin": "darwin",
|
"x86_64-apple-darwin": "darwin",
|
||||||
"aarch64-apple-darwin": "darwin",
|
"aarch64-apple-darwin": "darwin",
|
||||||
"x86_64-unknown-linux-gnu": "linux",
|
"x86_64-unknown-linux-gnu": "linux",
|
||||||
"i686-unknown-linux-gnu": "linux",
|
|
||||||
"aarch64-unknown-linux-gnu": "linux",
|
"aarch64-unknown-linux-gnu": "linux",
|
||||||
"armv7-unknown-linux-gnueabihf": "linux",
|
"armv7-unknown-linux-gnueabihf": "linux",
|
||||||
|
"loongarch64-unknown-linux-gnu": "linux",
|
||||||
};
|
};
|
||||||
const ARCH_MAP = {
|
const ARCH_MAP = {
|
||||||
"x86_64-pc-windows-msvc": "x64",
|
"x86_64-pc-windows-msvc": "x64",
|
||||||
"i686-pc-windows-msvc": "ia32",
|
|
||||||
"aarch64-pc-windows-msvc": "arm64",
|
"aarch64-pc-windows-msvc": "arm64",
|
||||||
"x86_64-apple-darwin": "x64",
|
"x86_64-apple-darwin": "x64",
|
||||||
"aarch64-apple-darwin": "arm64",
|
"aarch64-apple-darwin": "arm64",
|
||||||
"x86_64-unknown-linux-gnu": "x64",
|
"x86_64-unknown-linux-gnu": "x64",
|
||||||
"i686-unknown-linux-gnu": "ia32",
|
|
||||||
"aarch64-unknown-linux-gnu": "arm64",
|
"aarch64-unknown-linux-gnu": "arm64",
|
||||||
"armv7-unknown-linux-gnueabihf": "arm",
|
"armv7-unknown-linux-gnueabihf": "arm",
|
||||||
|
"loongarch64-unknown-linux-gnu": "loong64",
|
||||||
};
|
};
|
||||||
|
|
||||||
const arg1 = process.argv.slice(2)[0];
|
const arg1 = process.argv.slice(2)[0];
|
||||||
@@ -48,52 +46,93 @@ const SIDECAR_HOST = target
|
|||||||
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||||
|
|
||||||
/* ======= clash meta alpha======= */
|
/* ======= clash meta alpha======= */
|
||||||
const VERSION_URL =
|
const META_ALPHA_VERSION_URL =
|
||||||
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
|
"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`;
|
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
|
||||||
let META_ALPHA_VERSION;
|
let META_ALPHA_VERSION;
|
||||||
|
|
||||||
const META_ALPHA_MAP = {
|
const META_ALPHA_MAP = {
|
||||||
"win32-x64": "mihomo-windows-amd64-compatible",
|
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||||
"win32-ia32": "mihomo-windows-386",
|
|
||||||
"win32-arm64": "mihomo-windows-arm64",
|
"win32-arm64": "mihomo-windows-arm64",
|
||||||
"darwin-x64": "mihomo-darwin-amd64",
|
"darwin-x64": "mihomo-darwin-amd64",
|
||||||
"darwin-arm64": "mihomo-darwin-arm64",
|
"darwin-arm64": "mihomo-darwin-arm64",
|
||||||
"linux-x64": "mihomo-linux-amd64-compatible",
|
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||||
"linux-ia32": "mihomo-linux-386",
|
|
||||||
"linux-arm64": "mihomo-linux-arm64",
|
"linux-arm64": "mihomo-linux-arm64",
|
||||||
"linux-arm": "mihomo-linux-armv7",
|
"linux-arm": "mihomo-linux-armv7",
|
||||||
|
"linux-loong64": "mihomo-linux-loong64",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch the latest release version from the version.txt file
|
// Fetch the latest alpha release version from the version.txt file
|
||||||
async function getLatestVersion() {
|
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 {
|
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();
|
let v = await response.text();
|
||||||
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
|
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) {
|
} catch (error) {
|
||||||
console.error("Error fetching latest release version:", error.message);
|
console.error("Error fetching latest alpha version:", error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ======= clash meta stable ======= */
|
/* ======= 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`;
|
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
||||||
let META_VERSION = "v1.17.0";
|
let META_VERSION;
|
||||||
|
|
||||||
const META_MAP = {
|
const META_MAP = {
|
||||||
"win32-x64": "mihomo-windows-amd64-compatible",
|
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||||
"win32-ia32": "mihomo-windows-386",
|
|
||||||
"win32-arm64": "mihomo-windows-arm64",
|
"win32-arm64": "mihomo-windows-arm64",
|
||||||
"darwin-x64": "mihomo-darwin-amd64",
|
"darwin-x64": "mihomo-darwin-amd64",
|
||||||
"darwin-arm64": "mihomo-darwin-arm64",
|
"darwin-arm64": "mihomo-darwin-arm64",
|
||||||
"linux-x64": "mihomo-linux-amd64-compatible",
|
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||||
"linux-ia32": "mihomo-linux-386",
|
|
||||||
"linux-arm64": "mihomo-linux-arm64",
|
"linux-arm64": "mihomo-linux-arm64",
|
||||||
"linux-arm": "mihomo-linux-armv7",
|
"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
|
* check available
|
||||||
*/
|
*/
|
||||||
@@ -270,11 +309,43 @@ async function downloadFile(url, path) {
|
|||||||
console.log(`[INFO]: download finished "${url}"`);
|
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
|
* 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 = () =>
|
const resolveService = () =>
|
||||||
resolveResource({
|
resolveResource({
|
||||||
@@ -316,15 +387,17 @@ const tasks = [
|
|||||||
// { name: "clash", func: resolveClash, retry: 5 },
|
// { name: "clash", func: resolveClash, retry: 5 },
|
||||||
{
|
{
|
||||||
name: "clash-meta-alpha",
|
name: "clash-meta-alpha",
|
||||||
func: () => getLatestVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
func: () =>
|
||||||
|
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
||||||
retry: 5,
|
retry: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "clash-meta",
|
name: "clash-meta",
|
||||||
func: () => resolveSidecar(clashMeta()),
|
func: () =>
|
||||||
|
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
|
||||||
retry: 5,
|
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: "service", func: resolveService, retry: 5, winOnly: true },
|
||||||
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
|
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
|
||||||
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
|
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
|
||||||
|
|||||||
+15
-4
@@ -5,9 +5,9 @@ import { createRequire } from "module";
|
|||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
|
||||||
const target = process.argv.slice(2)[0];
|
const target = process.argv.slice(2)[0];
|
||||||
|
const alpha = process.argv.slice(2)[1];
|
||||||
|
|
||||||
const ARCH_MAP = {
|
const ARCH_MAP = {
|
||||||
"i686-pc-windows-msvc": "x86",
|
|
||||||
"x86_64-pc-windows-msvc": "x64",
|
"x86_64-pc-windows-msvc": "x64",
|
||||||
"aarch64-pc-windows-msvc": "arm64",
|
"aarch64-pc-windows-msvc": "arm64",
|
||||||
};
|
};
|
||||||
@@ -53,14 +53,25 @@ async function resolvePortable() {
|
|||||||
|
|
||||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||||
|
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
|
||||||
console.log("[INFO]: upload to ", process.env.TAG_NAME || `v${version}`);
|
console.log("[INFO]: upload to ", tag);
|
||||||
|
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...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);
|
console.log(release.name);
|
||||||
|
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
|
|||||||
@@ -45,11 +45,9 @@ async function resolveUpdater() {
|
|||||||
"darwin-intel": { signature: "", url: "" },
|
"darwin-intel": { signature: "", url: "" },
|
||||||
"darwin-x86_64": { signature: "", url: "" },
|
"darwin-x86_64": { signature: "", url: "" },
|
||||||
"linux-x86_64": { signature: "", url: "" },
|
"linux-x86_64": { signature: "", url: "" },
|
||||||
"linux-i686": { signature: "", url: "" },
|
|
||||||
"linux-aarch64": { signature: "", url: "" },
|
"linux-aarch64": { signature: "", url: "" },
|
||||||
"linux-armv7": { signature: "", url: "" },
|
"linux-armv7": { signature: "", url: "" },
|
||||||
"windows-x86_64": { signature: "", url: "" },
|
"windows-x86_64": { signature: "", url: "" },
|
||||||
"windows-i686": { signature: "", url: "" },
|
|
||||||
"windows-aarch64": { signature: "", url: "" },
|
"windows-aarch64": { signature: "", url: "" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -69,16 +67,6 @@ async function resolveUpdater() {
|
|||||||
updateData.platforms["windows-x86_64"].signature = sig;
|
updateData.platforms["windows-x86_64"].signature = sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// win32 url
|
|
||||||
if (name.endsWith("x86-setup.nsis.zip")) {
|
|
||||||
updateData.platforms["windows-i686"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// win32 signature
|
|
||||||
if (name.endsWith("x86-setup.nsis.zip.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms["windows-i686"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// win arm url
|
// win arm url
|
||||||
if (name.endsWith("arm64-setup.nsis.zip")) {
|
if (name.endsWith("arm64-setup.nsis.zip")) {
|
||||||
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||||
@@ -130,16 +118,6 @@ async function resolveUpdater() {
|
|||||||
updateData.platforms["linux-aarch64"].signature = sig;
|
updateData.platforms["linux-aarch64"].signature = sig;
|
||||||
updateData.platforms["linux-armv7"].signature = sig;
|
updateData.platforms["linux-armv7"].signature = sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// linux x86 url
|
|
||||||
if (name.endsWith("i386.AppImage.tar.gz")) {
|
|
||||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// linux x86 signature
|
|
||||||
if (name.endsWith("i386.AppImage.tar.gz.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms["linux-i686"].signature = sig;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises);
|
||||||
|
|||||||
Generated
+882
-489
File diff suppressed because it is too large
Load Diff
+9
-10
@@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "1.4.4"
|
version = "1.5.2"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||||
default-run = "clash-verge"
|
default-run = "clash-verge"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -14,7 +14,7 @@ tauri-build = { version = "1", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
warp = "0.3"
|
warp = "0.3"
|
||||||
which = "5.0.0"
|
which = "6.0.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
open = "5.0"
|
open = "5.0"
|
||||||
@@ -24,28 +24,27 @@ dunce = "1.0"
|
|||||||
log4rs = "1"
|
log4rs = "1"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
sysinfo = "0.29"
|
sysinfo = "0.30"
|
||||||
rquickjs = "0.3" # 高版本不支持 Linux aarch64
|
rquickjs = "0.3" # 高版本不支持 Linux aarch64
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
auto-launch = "0.5"
|
|
||||||
once_cell = "1.18"
|
once_cell = "1.18"
|
||||||
port_scanner = "0.1.5"
|
port_scanner = "0.1.5"
|
||||||
delay_timer = "0.11"
|
delay_timer = "0.11.5"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
window-shadows = { version = "0.2" }
|
window-shadows = { version = "0.2" }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||||
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
|
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
|
||||||
tauri = { version = "1.5", features = ["icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
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]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.0.0" # 高版本会返回错误 Status
|
runas = "=1.0.0" # 高版本会返回错误 Status
|
||||||
deelevate = "0.2.0"
|
deelevate = "0.2.0"
|
||||||
winreg = { version = "0.52", features = ["transactions"] }
|
winreg = "0.52.0"
|
||||||
windows-sys = { version = "0.52", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
#openssl
|
#openssl
|
||||||
|
|||||||
@@ -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.
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
+25
-5
@@ -2,14 +2,14 @@ use crate::{
|
|||||||
config::*,
|
config::*,
|
||||||
core::*,
|
core::*,
|
||||||
feat,
|
feat,
|
||||||
utils::{dirs, help},
|
utils::{dirs, help, resolve},
|
||||||
};
|
};
|
||||||
use crate::{ret_err, wrap_err};
|
use crate::{ret_err, wrap_err};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use sysproxy::Sysproxy;
|
use sysproxy::Sysproxy;
|
||||||
|
use tauri::api;
|
||||||
type CmdResult<T = ()> = Result<T, String>;
|
type CmdResult<T = ()> = Result<T, String>;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -219,7 +219,7 @@ pub fn open_app_dir() -> CmdResult<()> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_core_dir() -> CmdResult<()> {
|
pub fn open_core_dir() -> CmdResult<()> {
|
||||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
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))
|
wrap_err!(open::that(core_dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,13 +249,33 @@ pub mod uwp {
|
|||||||
pub async fn clash_api_get_proxy_delay(
|
pub async fn clash_api_get_proxy_delay(
|
||||||
name: String,
|
name: String,
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
|
timeout: i32,
|
||||||
) -> CmdResult<clash_api::DelayRes> {
|
) -> 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),
|
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)]
|
#[cfg(windows)]
|
||||||
pub mod service {
|
pub mod service {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -12,33 +12,56 @@ pub struct IClashTemp(pub Mapping);
|
|||||||
|
|
||||||
impl IClashTemp {
|
impl IClashTemp {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let template = Self::template();
|
||||||
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
|
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) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "{err}");
|
log::error!(target: "app", "{err}");
|
||||||
Self::template()
|
template
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn template() -> Self {
|
pub fn template() -> Self {
|
||||||
let mut map = Mapping::new();
|
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(), 7897.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("log-level".into(), "info".into());
|
||||||
map.insert("allow-lan".into(), false.into());
|
map.insert("allow-lan".into(), false.into());
|
||||||
map.insert("mode".into(), "rule".into());
|
map.insert("mode".into(), "rule".into());
|
||||||
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
||||||
map.insert("secret".into(), "".into());
|
map.insert("secret".into(), "".into());
|
||||||
|
map.insert("tun".into(), tun.into());
|
||||||
|
|
||||||
Self(map)
|
Self(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn guard(mut config: Mapping) -> Mapping {
|
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);
|
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.insert("external-controller".into(), ctrl.into());
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
@@ -61,12 +84,24 @@ impl IClashTemp {
|
|||||||
Self::guard_mixed_port(&self.0)
|
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 {
|
pub fn get_client_info(&self) -> ClashInfo {
|
||||||
let config = &self.0;
|
let config = &self.0;
|
||||||
|
|
||||||
ClashInfo {
|
ClashInfo {
|
||||||
port: Self::guard_mixed_port(&config),
|
mixed_port: Self::guard_mixed_port(config),
|
||||||
server: Self::guard_client_ctrl(&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 {
|
secret: config.get("secret").and_then(|value| match value {
|
||||||
Value::String(val_str) => Some(val_str.clone()),
|
Value::String(val_str) => Some(val_str.clone()),
|
||||||
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||||
@@ -91,6 +126,36 @@ impl IClashTemp {
|
|||||||
port
|
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
|
||||||
|
}
|
||||||
|
|
||||||
pub fn guard_server_ctrl(config: &Mapping) -> String {
|
pub fn guard_server_ctrl(config: &Mapping) -> String {
|
||||||
config
|
config
|
||||||
.get("external-controller")
|
.get("external-controller")
|
||||||
@@ -98,7 +163,7 @@ impl IClashTemp {
|
|||||||
Some(val_str) => {
|
Some(val_str) => {
|
||||||
let val_str = val_str.trim();
|
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}"),
|
true => format!("127.0.0.1{val_str}"),
|
||||||
false => val_str.to_owned(),
|
false => val_str.to_owned(),
|
||||||
};
|
};
|
||||||
@@ -129,6 +194,8 @@ impl IClashTemp {
|
|||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
pub struct ClashInfo {
|
pub struct ClashInfo {
|
||||||
/// clash core port
|
/// clash core port
|
||||||
|
pub mixed_port: u16,
|
||||||
|
pub socks_port: u16,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
/// same as `external-controller`
|
/// same as `external-controller`
|
||||||
pub server: String,
|
pub server: String,
|
||||||
@@ -148,7 +215,9 @@ fn test_clash_info() {
|
|||||||
|
|
||||||
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
|
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
|
||||||
ClashInfo {
|
ClashInfo {
|
||||||
port,
|
mixed_port: port,
|
||||||
|
socks_port: 7898,
|
||||||
|
port: 7899,
|
||||||
server: server.into(),
|
server: server.into(),
|
||||||
secret: None,
|
secret: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::utils::{dirs, help, tmpl};
|
use crate::utils::{dirs, help, resolve::VERSION, tmpl};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -8,7 +8,7 @@ use sysproxy::Sysproxy;
|
|||||||
|
|
||||||
use super::Config;
|
use super::Config;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
pub struct PrfItem {
|
pub struct PrfItem {
|
||||||
pub uid: Option<String>,
|
pub uid: Option<String>,
|
||||||
|
|
||||||
@@ -59,10 +59,10 @@ pub struct PrfSelected {
|
|||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||||
pub struct PrfExtra {
|
pub struct PrfExtra {
|
||||||
pub upload: usize,
|
pub upload: u64,
|
||||||
pub download: usize,
|
pub download: u64,
|
||||||
pub total: usize,
|
pub total: u64,
|
||||||
pub expire: usize,
|
pub expire: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
@@ -96,25 +96,7 @@ impl PrfOption {
|
|||||||
a.update_interval = b.update_interval.or(a.update_interval);
|
a.update_interval = b.update_interval.or(a.update_interval);
|
||||||
Some(a)
|
Some(a)
|
||||||
}
|
}
|
||||||
t @ _ => t.0.or(t.1),
|
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +134,7 @@ impl PrfItem {
|
|||||||
let desc = item.desc.unwrap_or("".into());
|
let desc = item.desc.unwrap_or("".into());
|
||||||
PrfItem::from_script(name, desc)
|
PrfItem::from_script(name, desc)
|
||||||
}
|
}
|
||||||
typ @ _ => bail!("invalid profile item type \"{typ}\""),
|
typ => bail!("invalid profile item type \"{typ}\""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,9 +170,10 @@ impl PrfItem {
|
|||||||
let opt_ref = option.as_ref();
|
let opt_ref = option.as_ref();
|
||||||
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
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 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().no_proxy();
|
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||||
|
|
||||||
// 使用软件自己的代理
|
// 使用软件自己的代理
|
||||||
if self_proxy {
|
if self_proxy {
|
||||||
@@ -213,26 +196,26 @@ impl PrfItem {
|
|||||||
}
|
}
|
||||||
// 使用系统代理
|
// 使用系统代理
|
||||||
else if with_proxy {
|
else if with_proxy {
|
||||||
match Sysproxy::get_system_proxy() {
|
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
|
||||||
Ok(p @ Sysproxy { enable: true, .. }) => {
|
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
||||||
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
|
||||||
|
|
||||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||||
builder = builder.proxy(proxy);
|
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::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 = match VERSION.get() {
|
||||||
let version = format!("clash-verge/{version}");
|
Some(v) => format!("clash-verge/v{}", v),
|
||||||
|
None => "clash-verge/unknown".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
builder = builder.user_agent(user_agent.unwrap_or(version));
|
builder = builder.user_agent(user_agent.unwrap_or(version));
|
||||||
|
|
||||||
let resp = builder.build()?.get(url).send().await?;
|
let resp = builder.build()?.get(url).send().await?;
|
||||||
@@ -248,12 +231,11 @@ impl PrfItem {
|
|||||||
let extra = match header.get("Subscription-Userinfo") {
|
let extra = match header.get("Subscription-Userinfo") {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
let sub_info = value.to_str().unwrap_or("");
|
let sub_info = value.to_str().unwrap_or("");
|
||||||
|
|
||||||
Some(PrfExtra {
|
Some(PrfExtra {
|
||||||
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
|
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
|
||||||
download: help::parse_str(sub_info, "download=").unwrap_or(0),
|
download: help::parse_str(sub_info, "download").unwrap_or(0),
|
||||||
total: help::parse_str(sub_info, "total=").unwrap_or(0),
|
total: help::parse_str(sub_info, "total").unwrap_or(0),
|
||||||
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
|
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
@@ -262,32 +244,42 @@ impl PrfItem {
|
|||||||
// parse the Content-Disposition
|
// parse the Content-Disposition
|
||||||
let filename = match header.get("Content-Disposition") {
|
let filename = match header.get("Content-Disposition") {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
let filename = value.to_str().unwrap_or("");
|
let filename = format!("{value:?}");
|
||||||
match help::parse_str::<String>(filename, "filename=") {
|
let filename = filename.trim_matches('"');
|
||||||
Some(filename) => Some(filename),
|
match help::parse_str::<String>(filename, "filename*") {
|
||||||
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())
|
||||||
|
}
|
||||||
|
None => match help::parse_str::<String>(filename, "filename") {
|
||||||
Some(filename) => {
|
Some(filename) => {
|
||||||
let iter = percent_encoding::percent_decode(filename.as_bytes());
|
let filename = filename.trim_matches('"');
|
||||||
let filename = iter.decode_utf8().unwrap_or_default();
|
Some(filename.to_string())
|
||||||
filename.split("''").last().map(|s| s.to_string())
|
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => None,
|
None => Some(
|
||||||
|
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
let option = match update_interval {
|
||||||
// parse the profile-update-interval
|
Some(val) => Some(PrfOption {
|
||||||
let option = match header.get("profile-update-interval") {
|
update_interval: Some(val),
|
||||||
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
|
..PrfOption::default()
|
||||||
Ok(val) => Some(PrfOption {
|
}),
|
||||||
update_interval: Some(val * 60), // hour -> min
|
None => match header.get("profile-update-interval") {
|
||||||
..PrfOption::default()
|
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
|
||||||
}),
|
Ok(val) => Some(PrfOption {
|
||||||
Err(_) => None,
|
update_interval: Some(val * 60), // hour -> min
|
||||||
|
..PrfOption::default()
|
||||||
|
}),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
},
|
},
|
||||||
None => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let uid = help::get_uid("r");
|
let uid = help::get_uid("r");
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ pub struct IProfiles {
|
|||||||
/// same as PrfConfig.chain
|
/// same as PrfConfig.chain
|
||||||
pub chain: Option<Vec<String>>,
|
pub chain: Option<Vec<String>>,
|
||||||
|
|
||||||
/// record valid fields for clash
|
|
||||||
pub valid: Option<Vec<String>>,
|
|
||||||
|
|
||||||
/// profile list
|
/// profile list
|
||||||
pub items: Option<Vec<PrfItem>>,
|
pub items: Option<Vec<PrfItem>>,
|
||||||
}
|
}
|
||||||
@@ -37,13 +34,13 @@ impl IProfiles {
|
|||||||
profiles.items = Some(vec![]);
|
profiles.items = Some(vec![]);
|
||||||
}
|
}
|
||||||
// compatible with the old old old version
|
// 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() {
|
for item in items.iter_mut() {
|
||||||
if item.uid.is_none() {
|
if item.uid.is_none() {
|
||||||
item.uid = Some(help::get_uid("d"));
|
item.uid = Some(help::get_uid("d"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
profiles
|
profiles
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -55,7 +52,6 @@ impl IProfiles {
|
|||||||
|
|
||||||
pub fn template() -> Self {
|
pub fn template() -> Self {
|
||||||
Self {
|
Self {
|
||||||
valid: Some(vec!["dns".into()]),
|
|
||||||
items: Some(vec![]),
|
items: Some(vec![]),
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
@@ -88,10 +84,6 @@ impl IProfiles {
|
|||||||
self.chain = Some(chain);
|
self.chain = Some(chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(valid) = patch.valid {
|
|
||||||
self.valid = Some(valid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,17 +139,19 @@ impl IProfiles {
|
|||||||
self.items = Some(vec![]);
|
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()
|
self.save_file()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// reorder items
|
/// reorder items
|
||||||
pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
|
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 old_index = None;
|
||||||
let mut new_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()) {
|
if items[i].uid == Some(active_id.clone()) {
|
||||||
old_index = Some(i);
|
old_index = Some(i);
|
||||||
}
|
}
|
||||||
@@ -177,7 +171,7 @@ impl IProfiles {
|
|||||||
|
|
||||||
/// update the item value
|
/// update the item value
|
||||||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
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() {
|
for each in items.iter_mut() {
|
||||||
if each.uid == Some(uid.clone()) {
|
if each.uid == Some(uid.clone()) {
|
||||||
@@ -250,11 +244,11 @@ impl IProfiles {
|
|||||||
let current = self.current.as_ref().unwrap_or(&uid);
|
let current = self.current.as_ref().unwrap_or(&uid);
|
||||||
let current = current.clone();
|
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;
|
let mut index = None;
|
||||||
|
|
||||||
// get the index
|
// get the index
|
||||||
for i in 0..items.len() {
|
for (i, _) in items.iter().enumerate() {
|
||||||
if items[i].uid == Some(uid.clone()) {
|
if items[i].uid == Some(uid.clone()) {
|
||||||
index = Some(i);
|
index = Some(i);
|
||||||
break;
|
break;
|
||||||
@@ -262,19 +256,19 @@ impl IProfiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(index) = index {
|
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 _ = dirs::app_profiles_dir().map(|path| {
|
||||||
let path = path.join(file);
|
let path = path.join(file);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let _ = fs::remove_file(path);
|
let _ = fs::remove_file(path);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the original uid
|
// delete the original uid
|
||||||
if current == uid {
|
if current == uid {
|
||||||
self.current = match items.len() > 0 {
|
self.current = match !items.is_empty() {
|
||||||
true => items[0].uid.clone(),
|
true => items[0].uid.clone(),
|
||||||
false => None,
|
false => None,
|
||||||
};
|
};
|
||||||
@@ -294,7 +288,7 @@ impl IProfiles {
|
|||||||
Some(file) => dirs::app_profiles_dir()?.join(file),
|
Some(file) => dirs::app_profiles_dir()?.join(file),
|
||||||
None => bail!("failed to get the file field"),
|
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}\"");
|
bail!("failed to find the current profile \"uid:{current}\"");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
use crate::enhance::field::use_keys;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::{Mapping, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct IRuntime {
|
pub struct IRuntime {
|
||||||
pub config: Option<Mapping>,
|
pub config: Option<Mapping>,
|
||||||
@@ -16,7 +16,7 @@ impl IRuntime {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 这里只更改 allow-lan | ipv6 | log-level
|
// 这里只更改 allow-lan | ipv6 | log-level | tun
|
||||||
pub fn patch_config(&mut self, patch: Mapping) {
|
pub fn patch_config(&mut self, patch: Mapping) {
|
||||||
if let Some(config) = self.config.as_mut() {
|
if let Some(config) = self.config.as_mut() {
|
||||||
["allow-lan", "ipv6", "log-level"]
|
["allow-lan", "ipv6", "log-level"]
|
||||||
@@ -26,6 +26,20 @@ impl IRuntime {
|
|||||||
config.insert(key.into(), value.clone());
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,16 +19,17 @@ pub struct IVerge {
|
|||||||
/// `light` or `dark` or `system`
|
/// `light` or `dark` or `system`
|
||||||
pub theme_mode: Option<String>,
|
pub theme_mode: Option<String>,
|
||||||
|
|
||||||
/// enable blur mode
|
|
||||||
/// maybe be able to set the alpha
|
|
||||||
pub theme_blur: Option<bool>,
|
|
||||||
|
|
||||||
/// tray click event
|
/// tray click event
|
||||||
pub tray_event: Option<String>,
|
pub tray_event: Option<String>,
|
||||||
|
|
||||||
/// copy env type
|
/// copy env type
|
||||||
pub env_type: Option<String>,
|
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
|
/// enable traffic graph default is true
|
||||||
pub traffic_graph: Option<bool>,
|
pub traffic_graph: Option<bool>,
|
||||||
|
|
||||||
@@ -80,8 +81,8 @@ pub struct IVerge {
|
|||||||
/// 默认的延迟测试连接
|
/// 默认的延迟测试连接
|
||||||
pub default_latency_test: Option<String>,
|
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>,
|
pub enable_builtin_enhanced: Option<bool>,
|
||||||
@@ -89,6 +90,9 @@ pub struct IVerge {
|
|||||||
/// proxy 页面布局 列数
|
/// proxy 页面布局 列数
|
||||||
pub proxy_layout_column: Option<i32>,
|
pub proxy_layout_column: Option<i32>,
|
||||||
|
|
||||||
|
/// 测试网站列表
|
||||||
|
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||||
|
|
||||||
/// 日志清理
|
/// 日志清理
|
||||||
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
||||||
pub auto_log_clean: Option<i32>,
|
pub auto_log_clean: Option<i32>,
|
||||||
@@ -97,11 +101,27 @@ pub struct IVerge {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub window_size_position: Option<Vec<f64>>,
|
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>,
|
pub enable_random_port: Option<bool>,
|
||||||
|
|
||||||
/// verge mixed port 用于覆盖 clash 的 mixed port
|
/// verge mixed port 用于覆盖 clash 的 mixed port
|
||||||
pub verge_mixed_port: Option<u16>,
|
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)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -140,7 +160,7 @@ impl IVerge {
|
|||||||
env_type: Some("bash".into()),
|
env_type: Some("bash".into()),
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
env_type: Some("powershell".into()),
|
env_type: Some("powershell".into()),
|
||||||
theme_blur: Some(false),
|
start_page: Some("/".into()),
|
||||||
traffic_graph: Some(true),
|
traffic_graph: Some(true),
|
||||||
enable_memory_usage: Some(true),
|
enable_memory_usage: Some(true),
|
||||||
enable_auto_launch: Some(false),
|
enable_auto_launch: Some(false),
|
||||||
@@ -148,11 +168,12 @@ impl IVerge {
|
|||||||
enable_system_proxy: Some(false),
|
enable_system_proxy: Some(false),
|
||||||
enable_random_port: Some(false),
|
enable_random_port: Some(false),
|
||||||
verge_mixed_port: Some(7897),
|
verge_mixed_port: Some(7897),
|
||||||
|
verge_socks_port: Some(7898),
|
||||||
|
verge_port: Some(7899),
|
||||||
enable_proxy_guard: Some(false),
|
enable_proxy_guard: Some(false),
|
||||||
proxy_guard_duration: Some(30),
|
proxy_guard_duration: Some(30),
|
||||||
auto_close_connection: Some(true),
|
auto_close_connection: Some(true),
|
||||||
enable_builtin_enhanced: Some(true),
|
enable_builtin_enhanced: Some(true),
|
||||||
enable_clash_fields: Some(false),
|
|
||||||
auto_log_clean: Some(3),
|
auto_log_clean: Some(3),
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
@@ -177,9 +198,10 @@ impl IVerge {
|
|||||||
patch!(app_log_level);
|
patch!(app_log_level);
|
||||||
patch!(language);
|
patch!(language);
|
||||||
patch!(theme_mode);
|
patch!(theme_mode);
|
||||||
patch!(theme_blur);
|
|
||||||
patch!(tray_event);
|
patch!(tray_event);
|
||||||
patch!(env_type);
|
patch!(env_type);
|
||||||
|
patch!(start_page);
|
||||||
|
patch!(startup_script);
|
||||||
patch!(traffic_graph);
|
patch!(traffic_graph);
|
||||||
patch!(enable_memory_usage);
|
patch!(enable_memory_usage);
|
||||||
|
|
||||||
@@ -189,6 +211,8 @@ impl IVerge {
|
|||||||
patch!(enable_silent_start);
|
patch!(enable_silent_start);
|
||||||
patch!(enable_random_port);
|
patch!(enable_random_port);
|
||||||
patch!(verge_mixed_port);
|
patch!(verge_mixed_port);
|
||||||
|
patch!(verge_socks_port);
|
||||||
|
patch!(verge_port);
|
||||||
patch!(enable_system_proxy);
|
patch!(enable_system_proxy);
|
||||||
patch!(enable_proxy_guard);
|
patch!(enable_proxy_guard);
|
||||||
patch!(system_proxy_bypass);
|
patch!(system_proxy_bypass);
|
||||||
@@ -201,11 +225,13 @@ impl IVerge {
|
|||||||
|
|
||||||
patch!(auto_close_connection);
|
patch!(auto_close_connection);
|
||||||
patch!(default_latency_test);
|
patch!(default_latency_test);
|
||||||
|
patch!(default_latency_timeout);
|
||||||
patch!(enable_builtin_enhanced);
|
patch!(enable_builtin_enhanced);
|
||||||
patch!(proxy_layout_column);
|
patch!(proxy_layout_column);
|
||||||
patch!(enable_clash_fields);
|
patch!(test_list);
|
||||||
patch!(auto_log_clean);
|
patch!(auto_log_clean);
|
||||||
patch!(window_size_position);
|
patch!(window_size_position);
|
||||||
|
patch!(window_is_maximized);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在初始化前尝试拿到单例端口的值
|
/// 在初始化前尝试拿到单例端口的值
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub async fn put_configs(path: &str) -> Result<()> {
|
|||||||
|
|
||||||
match response.status().as_u16() {
|
match response.status().as_u16() {
|
||||||
204 => Ok(()),
|
204 => Ok(()),
|
||||||
status @ _ => {
|
status => {
|
||||||
bail!("failed to put configs with status \"{status}\"")
|
bail!("failed to put configs with status \"{status}\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,11 @@ pub struct DelayRes {
|
|||||||
|
|
||||||
/// GET /proxies/{name}/delay
|
/// 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, headers) = clash_client_info()?;
|
||||||
let url = format!("{url}/proxies/{name}/delay");
|
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
|
let builder = client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.headers(headers)
|
.headers(headers)
|
||||||
.query(&[("timeout", "10000"), ("url", &test_url)]);
|
.query(&[("timeout", &format!("{timeout}")), ("url", &test_url)]);
|
||||||
let response = builder.send().await?;
|
let response = builder.send().await?;
|
||||||
|
|
||||||
Ok(response.json::<DelayRes>().await?)
|
Ok(response.json::<DelayRes>().await?)
|
||||||
@@ -83,12 +87,12 @@ fn clash_client_info() -> Result<(String, HeaderMap)> {
|
|||||||
/// 缩短clash的日志
|
/// 缩短clash的日志
|
||||||
pub fn parse_log(log: String) -> String {
|
pub fn parse_log(log: String) -> String {
|
||||||
if log.starts_with("time=") && log.len() > 33 {
|
if log.starts_with("time=") && log.len() > 33 {
|
||||||
return (&log[33..]).to_owned();
|
return (log[33..]).to_owned();
|
||||||
}
|
}
|
||||||
if log.len() > 9 {
|
if log.len() > 9 {
|
||||||
return (&log[9..]).to_owned();
|
return (log[9..]).to_owned();
|
||||||
}
|
}
|
||||||
return log;
|
log
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 缩短clash -t的错误输出
|
/// 缩短clash -t的错误输出
|
||||||
@@ -105,7 +109,7 @@ pub fn parse_check_output(log: String) -> String {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if mr > m {
|
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()));
|
let r = log.find("path=").or(Some(log.len()));
|
||||||
|
|
||||||
if let (Some(l), Some(r)) = (l, r) {
|
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
|
log
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use anyhow::{bail, Context, Result};
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{fs, io::Write, sync::Arc, time::Duration};
|
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 tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
@@ -35,12 +35,12 @@ impl CoreManager {
|
|||||||
.map(|pid| {
|
.map(|pid| {
|
||||||
let mut system = System::new();
|
let mut system = System::new();
|
||||||
system.refresh_all();
|
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") {
|
if proc.name().contains("clash") {
|
||||||
log::debug!(target: "app", "kill old clash process");
|
log::debug!(target: "app", "kill old clash process");
|
||||||
proc.kill();
|
proc.kill();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async {
|
tauri::async_runtime::spawn(async {
|
||||||
@@ -68,7 +68,7 @@ impl CoreManager {
|
|||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let error = clash_api::parse_check_output(output.stdout.clone());
|
let error = clash_api::parse_check_output(output.stdout.clone());
|
||||||
let error = match error.len() > 0 {
|
let error = match !error.is_empty() {
|
||||||
true => error,
|
true => error,
|
||||||
false => output.stdout.clone(),
|
false => output.stdout.clone(),
|
||||||
};
|
};
|
||||||
@@ -110,7 +110,7 @@ impl CoreManager {
|
|||||||
use super::win_service;
|
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);
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
*self.use_service_mode.lock() = enable;
|
*self.use_service_mode.lock() = enable;
|
||||||
@@ -259,7 +259,7 @@ impl CoreManager {
|
|||||||
/// 切换核心
|
/// 切换核心
|
||||||
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
||||||
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
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()) {
|
if !CLASH_CORES.contains(&clash_core.as_str()) {
|
||||||
bail!("invalid clash core name \"{clash_core}\"");
|
bail!("invalid clash core name \"{clash_core}\"");
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl Handle {
|
|||||||
self.app_handle
|
self.app_handle
|
||||||
.lock()
|
.lock()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(None, |a| a.get_window("main"))
|
.and_then(|a| a.get_window("main"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_clash() {
|
pub fn refresh_clash() {
|
||||||
|
|||||||
@@ -65,17 +65,17 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let f = match func.trim() {
|
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_rule" => || feat::change_clash_mode("rule".into()),
|
||||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||||
"clash_mode_script" => || feat::change_clash_mode("script".into()),
|
"clash_mode_script" => || feat::change_clash_mode("script".into()),
|
||||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
"toggle_system_proxy" => feat::toggle_system_proxy,
|
||||||
"enable_system_proxy" => || feat::enable_system_proxy(),
|
"enable_system_proxy" => feat::enable_system_proxy,
|
||||||
"disable_system_proxy" => || feat::disable_system_proxy(),
|
"disable_system_proxy" => feat::disable_system_proxy,
|
||||||
"toggle_tun_mode" => || feat::toggle_tun_mode(),
|
"toggle_tun_mode" => feat::toggle_tun_mode,
|
||||||
"enable_tun_mode" => || feat::enable_tun_mode(),
|
"enable_tun_mode" => feat::enable_tun_mode,
|
||||||
"disable_tun_mode" => || feat::disable_tun_mode(),
|
"disable_tun_mode" => feat::disable_tun_mode,
|
||||||
|
|
||||||
_ => bail!("invalid function \"{func}\""),
|
_ => bail!("invalid function \"{func}\""),
|
||||||
};
|
};
|
||||||
@@ -86,7 +86,7 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn unregister(&self, hotkey: &str) -> Result<()> {
|
fn unregister(&self, hotkey: &str) -> Result<()> {
|
||||||
self.get_manager()?.unregister(&hotkey)?;
|
self.get_manager()?.unregister(hotkey)?;
|
||||||
log::info!(target: "app", "unregister hotkey {hotkey}");
|
log::info!(target: "app", "unregister hotkey {hotkey}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ impl Hotkey {
|
|||||||
Ok(())
|
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();
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
hotkeys.iter().for_each(|hotkey| {
|
hotkeys.iter().for_each(|hotkey| {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
/// 给clash内核的tun模式授权
|
/// 给clash内核的tun模式授权
|
||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
||||||
@@ -13,9 +11,6 @@ pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let output = {
|
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 path = path.replace(' ', "\\\\ ");
|
||||||
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
|
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
|
||||||
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
|
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}");
|
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)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ impl Sysopt {
|
|||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge = verge.latest();
|
let verge = verge.latest();
|
||||||
(
|
(
|
||||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
verge.system_proxy_bypass.clone(),
|
verge.system_proxy_bypass.clone(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -66,7 +66,7 @@ impl Sysopt {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if enable {
|
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()?;
|
current.set_system_proxy()?;
|
||||||
|
|
||||||
*self.old_sysproxy.lock() = old;
|
*self.old_sysproxy.lock() = old;
|
||||||
@@ -93,7 +93,7 @@ impl Sysopt {
|
|||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge = verge.latest();
|
let verge = verge.latest();
|
||||||
(
|
(
|
||||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
verge.system_proxy_bypass.clone(),
|
verge.system_proxy_bypass.clone(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -102,6 +102,12 @@ impl Sysopt {
|
|||||||
sysproxy.enable = enable;
|
sysproxy.enable = enable;
|
||||||
sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
|
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()?;
|
sysproxy.set_system_proxy()?;
|
||||||
*cur_sysproxy = Some(sysproxy);
|
*cur_sysproxy = Some(sysproxy);
|
||||||
|
|
||||||
@@ -142,7 +148,7 @@ impl Sysopt {
|
|||||||
|
|
||||||
/// init the auto launch
|
/// init the auto launch
|
||||||
pub fn init_launch(&self) -> Result<()> {
|
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 enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
let app_exe = current_exe()?;
|
let app_exe = current_exe()?;
|
||||||
@@ -233,7 +239,7 @@ impl Sysopt {
|
|||||||
drop(auto_launch);
|
drop(auto_launch);
|
||||||
return self.init_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 enable = enable.unwrap_or(false);
|
||||||
let auto_launch = auto_launch.as_ref().unwrap();
|
let auto_launch = auto_launch.as_ref().unwrap();
|
||||||
|
|
||||||
@@ -271,9 +277,9 @@ impl Sysopt {
|
|||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge = verge.latest();
|
let verge = verge.latest();
|
||||||
(
|
(
|
||||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
verge.enable_proxy_guard.clone().unwrap_or(false),
|
verge.enable_proxy_guard.unwrap_or(false),
|
||||||
verge.proxy_guard_duration.clone().unwrap_or(10),
|
verge.proxy_guard_duration.unwrap_or(10),
|
||||||
verge.system_proxy_bypass.clone(),
|
verge.system_proxy_bypass.clone(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ impl Timer {
|
|||||||
let timer_map = self.timer_map.lock();
|
let timer_map = self.timer_map.lock();
|
||||||
let delay_timer = self.delay_timer.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
|
items
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| {
|
.filter_map(|item| {
|
||||||
@@ -61,7 +61,7 @@ impl Timer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ impl Tray {
|
|||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
let icon = include_bytes!("../../icons/tray-icon.png").to_vec();
|
let icon = include_bytes!("../../icons/tray-icon.png").to_vec();
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let icon = include_bytes!("../../icons/icon.png").to_vec();
|
let icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
|
||||||
icon
|
icon
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -203,14 +203,8 @@ impl Tray {
|
|||||||
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||||
"restart_clash" => feat::restart_clash_core(),
|
"restart_clash" => feat::restart_clash_core(),
|
||||||
"restart_app" => api::process::restart(&app_handle.env()),
|
"restart_app" => api::process::restart(&app_handle.env()),
|
||||||
"quit" => {
|
"quit" => cmds::exit_app(app_handle.clone()),
|
||||||
let _ = resolve::save_window_size_position(app_handle, true);
|
|
||||||
|
|
||||||
resolve::resolve_reset();
|
|
||||||
api::process::kill_children();
|
|
||||||
app_handle.exit(0);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use runas::Command as RunasCommand;
|
|||||||
use std::process::Command as StdCommand;
|
use std::process::Command as StdCommand;
|
||||||
|
|
||||||
pub async fn invoke_uwptools() -> Result<()> {
|
pub async fn invoke_uwptools() -> Result<()> {
|
||||||
let binary_path = dirs::service_path()?;
|
let resource_dir = dirs::app_resources_dir()?;
|
||||||
let tool_path = binary_path.with_file_name("enableLoopback.exe");
|
let tool_path = resource_dir.join("enableLoopback.exe");
|
||||||
|
|
||||||
if !tool_path.exists() {
|
if !tool_path.exists() {
|
||||||
bail!("enableLoopback exe not found");
|
bail!("enableLoopback exe not found");
|
||||||
@@ -17,10 +17,9 @@ pub async fn invoke_uwptools() -> Result<()> {
|
|||||||
let token = Token::with_current_process()?;
|
let token = Token::with_current_process()?;
|
||||||
let level = token.privilege_level()?;
|
let level = token.privilege_level()?;
|
||||||
|
|
||||||
match level {
|
match level {
|
||||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
|
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
|
||||||
_ => StdCommand::new(tool_path)
|
_ => StdCommand::new(tool_path).status()?,
|
||||||
.status()?,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
pub const HANDLE_FIELDS: [&str; 9] = [
|
pub const HANDLE_FIELDS: [&str; 9] = [
|
||||||
"mode",
|
"mode",
|
||||||
"port",
|
|
||||||
"socks-port",
|
|
||||||
"mixed-port",
|
"mixed-port",
|
||||||
|
"socks-port",
|
||||||
|
"port",
|
||||||
"allow-lan",
|
"allow-lan",
|
||||||
"log-level",
|
"log-level",
|
||||||
"ipv6",
|
"ipv6",
|
||||||
@@ -15,71 +15,13 @@ pub const HANDLE_FIELDS: [&str; 9] = [
|
|||||||
|
|
||||||
pub const DEFAULT_FIELDS: [&str; 5] = [
|
pub const DEFAULT_FIELDS: [&str; 5] = [
|
||||||
"proxies",
|
"proxies",
|
||||||
"proxy-groups",
|
|
||||||
"proxy-providers",
|
"proxy-providers",
|
||||||
"rules",
|
"proxy-groups",
|
||||||
"rule-providers",
|
"rule-providers",
|
||||||
|
"rules",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const OTHERS_FIELDS: [&str; 31] = [
|
pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping {
|
||||||
"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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ret = Mapping::new();
|
let mut ret = Mapping::new();
|
||||||
|
|
||||||
for (key, value) in config.into_iter() {
|
for (key, value) in config.into_iter() {
|
||||||
@@ -105,40 +47,35 @@ pub fn use_lowercase(config: Mapping) -> Mapping {
|
|||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
|
pub fn use_sort(config: Mapping) -> Mapping {
|
||||||
let mut ret = Mapping::new();
|
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()
|
.into_iter()
|
||||||
.chain(OTHERS_FIELDS)
|
.collect();
|
||||||
.chain(DEFAULT_FIELDS)
|
|
||||||
.for_each(|key| {
|
|
||||||
let key = Value::from(key);
|
|
||||||
config.get(&key).map(|value| {
|
|
||||||
ret.insert(key, value.clone());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if !enable_filter {
|
config_keys.difference(&supported_keys).for_each(|&key| {
|
||||||
let supported_keys: HashSet<&str> = HANDLE_FIELDS
|
let key = Value::from(key);
|
||||||
.into_iter()
|
if let Some(value) = config.get(&key) {
|
||||||
.chain(OTHERS_FIELDS)
|
ret.insert(key, value.clone());
|
||||||
.chain(DEFAULT_FIELDS)
|
}
|
||||||
.collect();
|
});
|
||||||
|
DEFAULT_FIELDS.into_iter().for_each(|key| {
|
||||||
let config_keys: HashSet<&str> = config
|
let key = Value::from(key);
|
||||||
.keys()
|
if let Some(value) = config.get(&key) {
|
||||||
.filter_map(|e| e.as_str())
|
ret.insert(key, value.clone());
|
||||||
.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());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
@@ -150,7 +87,7 @@ pub fn use_keys(config: &Mapping) -> Vec<String> {
|
|||||||
.map(|s| {
|
.map(|s| {
|
||||||
let mut s = s.to_string();
|
let mut s = s.to_string();
|
||||||
s.make_ascii_lowercase();
|
s.make_ascii_lowercase();
|
||||||
return s;
|
s
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
|
|||||||
// 直接覆盖原字段
|
// 直接覆盖原字段
|
||||||
use_lowercase(merge.clone())
|
use_lowercase(merge.clone())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|(key, _)| !MERGE_FIELDS.contains(&key.as_str().unwrap_or_default()))
|
||||||
.for_each(|(key, value)| {
|
.for_each(|(key, value)| {
|
||||||
config.insert(key, value);
|
config.insert(key, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
|
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"]
|
["rules", "proxies", "proxy-groups"]
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
mod chain;
|
mod chain;
|
||||||
mod field;
|
pub mod field;
|
||||||
mod merge;
|
mod merge;
|
||||||
mod script;
|
mod script;
|
||||||
mod tun;
|
mod tun;
|
||||||
|
|
||||||
pub(self) use self::field::*;
|
|
||||||
|
|
||||||
use self::chain::*;
|
use self::chain::*;
|
||||||
|
use self::field::*;
|
||||||
use self::merge::*;
|
use self::merge::*;
|
||||||
use self::script::*;
|
use self::script::*;
|
||||||
use self::tun::*;
|
use self::tun::*;
|
||||||
@@ -23,50 +22,43 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
// config.yaml 的订阅
|
// config.yaml 的订阅
|
||||||
let clash_config = { Config::clash().latest().0.clone() };
|
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 = Config::verge();
|
||||||
let verge = verge.latest();
|
let verge = verge.latest();
|
||||||
(
|
(
|
||||||
verge.clash_core.clone(),
|
verge.clash_core.clone(),
|
||||||
verge.enable_tun_mode.clone().unwrap_or(false),
|
verge.enable_tun_mode.unwrap_or(false),
|
||||||
verge.enable_builtin_enhanced.clone().unwrap_or(true),
|
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||||
verge.enable_clash_fields.clone().unwrap_or(true),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从profiles里拿东西
|
// 从profiles里拿东西
|
||||||
let (mut config, chain, valid) = {
|
let (mut config, chain) = {
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles();
|
||||||
let profiles = profiles.latest();
|
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() {
|
let chain = match profiles.chain.as_ref() {
|
||||||
Some(chain) => chain
|
Some(chain) => chain
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|uid| profiles.get_item(uid).ok())
|
.filter_map(|uid| profiles.get_item(uid).ok())
|
||||||
.filter_map(|item| <Option<ChainItem>>::from(item))
|
.filter_map(<Option<ChainItem>>::from)
|
||||||
.collect::<Vec<ChainItem>>(),
|
.collect::<Vec<ChainItem>>(),
|
||||||
None => vec![],
|
None => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let valid = profiles.valid.clone().unwrap_or(vec![]);
|
(current, chain)
|
||||||
|
|
||||||
(current, chain, valid)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut result_map = HashMap::new(); // 保存脚本日志
|
let mut result_map = HashMap::new(); // 保存脚本日志
|
||||||
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
||||||
|
|
||||||
let valid = use_valid_fields(valid);
|
|
||||||
config = use_filter(config, &valid, enable_filter);
|
|
||||||
|
|
||||||
// 处理用户的profile
|
// 处理用户的profile
|
||||||
chain.into_iter().for_each(|item| match item.data {
|
chain.into_iter().for_each(|item| match item.data {
|
||||||
ChainType::Merge(merge) => {
|
ChainType::Merge(merge) => {
|
||||||
exists_keys.extend(use_keys(&merge));
|
exists_keys.extend(use_keys(&merge));
|
||||||
config = use_merge(merge, config.to_owned());
|
config = use_merge(merge, config.to_owned());
|
||||||
config = use_filter(config.to_owned(), &valid, enable_filter);
|
|
||||||
}
|
}
|
||||||
ChainType::Script(script) => {
|
ChainType::Script(script) => {
|
||||||
let mut logs = vec![];
|
let mut logs = vec![];
|
||||||
@@ -74,7 +66,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
match use_script(script, config.to_owned()) {
|
match use_script(script, config.to_owned()) {
|
||||||
Ok((res_config, res_logs)) => {
|
Ok((res_config, res_logs)) => {
|
||||||
exists_keys.extend(use_keys(&res_config));
|
exists_keys.extend(use_keys(&res_config));
|
||||||
config = use_filter(res_config, &valid, enable_filter);
|
config = res_config;
|
||||||
logs.extend(res_logs);
|
logs.extend(res_logs);
|
||||||
}
|
}
|
||||||
Err(err) => logs.push(("exception".into(), err.to_string())),
|
Err(err) => logs.push(("exception".into(), err.to_string())),
|
||||||
@@ -86,11 +78,20 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
|
|
||||||
// 合并默认的config
|
// 合并默认的config
|
||||||
for (key, value) in clash_config.into_iter() {
|
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 {
|
if enable_builtin {
|
||||||
ChainItem::builtin()
|
ChainItem::builtin()
|
||||||
@@ -103,7 +104,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
match item.data {
|
match item.data {
|
||||||
ChainType::Script(script) => match use_script(script, config.to_owned()) {
|
ChainType::Script(script) => match use_script(script, config.to_owned()) {
|
||||||
Ok((res_config, _)) => {
|
Ok((res_config, _)) => {
|
||||||
config = use_filter(res_config, &clash_fields, enable_filter);
|
config = res_config;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "builtin script error `{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_tun(config, enable_tun);
|
||||||
config = use_sort(config, enable_filter);
|
config = use_sort(config);
|
||||||
|
|
||||||
let mut exists_set = HashSet::new();
|
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();
|
exists_keys = exists_set.into_iter().collect();
|
||||||
|
|
||||||
(config, exists_keys, result_map)
|
(config, exists_keys, result_map)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(Stri
|
|||||||
if result == "\"\"" {
|
if result == "\"\"" {
|
||||||
anyhow::bail!("main function should return object");
|
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();
|
let mut out = outputs.lock().unwrap();
|
||||||
|
|||||||
@@ -30,12 +30,6 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
});
|
});
|
||||||
|
|
||||||
revise!(tun_val, "enable", enable);
|
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);
|
revise!(config, "tun", tun_val);
|
||||||
|
|
||||||
|
|||||||
+49
-5
@@ -62,7 +62,7 @@ pub fn change_clash_mode(mode: String) {
|
|||||||
|
|
||||||
// 切换系统代理
|
// 切换系统代理
|
||||||
pub fn toggle_system_proxy() {
|
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);
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
@@ -110,7 +110,7 @@ pub fn disable_system_proxy() {
|
|||||||
|
|
||||||
// 切换tun模式
|
// 切换tun模式
|
||||||
pub fn toggle_tun_mode() {
|
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);
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
@@ -162,16 +162,18 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
|||||||
|
|
||||||
match {
|
match {
|
||||||
let mixed_port = patch.get("mixed-port");
|
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);
|
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
|
||||||
if mixed_port.is_some() && !enable_random_port {
|
if mixed_port.is_some() && !enable_random_port {
|
||||||
let changed = mixed_port.clone().unwrap()
|
let changed = mixed_port.unwrap()
|
||||||
!= Config::verge()
|
!= Config::verge()
|
||||||
.latest()
|
.latest()
|
||||||
.verge_mixed_port
|
.verge_mixed_port
|
||||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
// 检查端口占用
|
// 检查端口占用
|
||||||
if changed {
|
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) {
|
if !port_scanner::local_port_available(port as u16) {
|
||||||
Config::clash().discard();
|
Config::clash().discard();
|
||||||
bail!("port already in use");
|
bail!("port already in use");
|
||||||
@@ -182,6 +184,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
|||||||
|
|
||||||
// 激活订阅
|
// 激活订阅
|
||||||
if mixed_port.is_some()
|
if mixed_port.is_some()
|
||||||
|
|| socks_port.is_some()
|
||||||
|
|| port.is_some()
|
||||||
|| patch.get("secret").is_some()
|
|| patch.get("secret").is_some()
|
||||||
|| patch.get("external-controller").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 system_proxy = patch.enable_system_proxy;
|
||||||
let proxy_bypass = patch.system_proxy_bypass;
|
let proxy_bypass = patch.system_proxy_bypass;
|
||||||
let language = patch.language;
|
let language = patch.language;
|
||||||
|
let port = patch.verge_mixed_port;
|
||||||
|
|
||||||
match {
|
match {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -249,7 +254,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|||||||
if auto_launch.is_some() {
|
if auto_launch.is_some() {
|
||||||
sysopt::Sysopt::global().update_launch()?;
|
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().update_sysproxy()?;
|
||||||
sysopt::Sysopt::global().guard_proxy();
|
sysopt::Sysopt::global().guard_proxy();
|
||||||
}
|
}
|
||||||
@@ -368,3 +373,42 @@ pub fn copy_clash_env(app_handle: &AppHandle) {
|
|||||||
_ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
|
_ => 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
@@ -25,7 +25,10 @@ fn main() -> std::io::Result<()> {
|
|||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut builder = tauri::Builder::default()
|
let mut builder = tauri::Builder::default()
|
||||||
.system_tray(SystemTray::new())
|
.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)
|
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
// common
|
// common
|
||||||
@@ -34,6 +37,7 @@ fn main() -> std::io::Result<()> {
|
|||||||
cmds::open_logs_dir,
|
cmds::open_logs_dir,
|
||||||
cmds::open_web_url,
|
cmds::open_web_url,
|
||||||
cmds::open_core_dir,
|
cmds::open_core_dir,
|
||||||
|
cmds::get_portable_flag,
|
||||||
// cmds::kill_sidecar,
|
// cmds::kill_sidecar,
|
||||||
cmds::restart_sidecar,
|
cmds::restart_sidecar,
|
||||||
cmds::grant_permission,
|
cmds::grant_permission,
|
||||||
@@ -50,6 +54,8 @@ fn main() -> std::io::Result<()> {
|
|||||||
// verge
|
// verge
|
||||||
cmds::get_verge_config,
|
cmds::get_verge_config,
|
||||||
cmds::patch_verge_config,
|
cmds::patch_verge_config,
|
||||||
|
cmds::test_delay,
|
||||||
|
cmds::exit_app,
|
||||||
// cmds::update_hotkeys,
|
// cmds::update_hotkeys,
|
||||||
// profile
|
// profile
|
||||||
cmds::get_profiles,
|
cmds::get_profiles,
|
||||||
@@ -100,42 +106,21 @@ fn main() -> std::io::Result<()> {
|
|||||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||||
api.prevent_exit();
|
api.prevent_exit();
|
||||||
}
|
}
|
||||||
tauri::RunEvent::Exit => {
|
|
||||||
resolve::resolve_reset();
|
|
||||||
api::process::kill_children();
|
|
||||||
app_handle.exit(0);
|
|
||||||
}
|
|
||||||
tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
|
tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
|
||||||
resolve::resolve_reset();
|
resolve::resolve_reset();
|
||||||
api::process::kill_children();
|
api::process::kill_children();
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
if label == "main" {
|
if label == "main" {
|
||||||
match event {
|
match event {
|
||||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
tauri::WindowEvent::Destroyed => {
|
||||||
api.prevent_close();
|
let _ = resolve::save_window_size_position(app_handle, true);
|
||||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
|
||||||
|
|
||||||
app_handle.get_window("main").map(|win| {
|
|
||||||
let _ = win.hide();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
|
||||||
if label == "main" {
|
|
||||||
match event {
|
|
||||||
tauri::WindowEvent::CloseRequested { .. } => {
|
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(_) => {
|
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
||||||
let _ = resolve::save_window_size_position(&app_handle, false);
|
let _ = resolve::save_window_size_position(app_handle, false);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-73
@@ -1,86 +1,69 @@
|
|||||||
|
use crate::core::handle;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
api::path::{data_dir, resource_dir},
|
api::path::{data_dir, resource_dir},
|
||||||
Env, PackageInfo,
|
Env,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "verge-dev"))]
|
#[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")]
|
#[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 CLASH_CONFIG: &str = "config.yaml";
|
||||||
static VERGE_CONFIG: &str = "verge.yaml";
|
static VERGE_CONFIG: &str = "verge.yaml";
|
||||||
static PROFILE_YAML: &str = "profiles.yaml";
|
static PROFILE_YAML: &str = "profiles.yaml";
|
||||||
|
|
||||||
static mut RESOURCE_DIR: Option<PathBuf> = None;
|
/// init portable flag
|
||||||
static mut APP_HOME_DIR: Option<PathBuf> = None;
|
pub fn init_portable_flag() -> Result<()> {
|
||||||
|
|
||||||
/// 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<()> {
|
|
||||||
use tauri::utils::platform::current_exe;
|
use tauri::utils::platform::current_exe;
|
||||||
|
|
||||||
let exe = current_exe()?;
|
let app_exe = current_exe()?;
|
||||||
|
if let Some(dir) = app_exe.parent() {
|
||||||
if let Some(dir) = exe.parent() {
|
|
||||||
let dir = PathBuf::from(dir).join(".config/PORTABLE");
|
let dir = PathBuf::from(dir).join(".config/PORTABLE");
|
||||||
|
|
||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
PORTABLE_FLAG = true;
|
PORTABLE_FLAG.get_or_init(|| true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PORTABLE_FLAG.get_or_init(|| false);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the verge app home dir
|
/// get the verge app home dir
|
||||||
pub fn app_home_dir() -> Result<PathBuf> {
|
pub fn app_home_dir() -> Result<PathBuf> {
|
||||||
use tauri::utils::platform::current_exe;
|
use tauri::utils::platform::current_exe;
|
||||||
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"))?;
|
|
||||||
|
|
||||||
let portable_home_dir = PathBuf::from(app_dir).join(".config").join(APP_ID);
|
let flag = PORTABLE_FLAG.get().unwrap_or(&false);
|
||||||
let home_dir = data_dir()
|
if *flag {
|
||||||
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
|
let app_exe = current_exe()?;
|
||||||
.join(APP_ID);
|
let app_exe = dunce::canonicalize(app_exe)?;
|
||||||
|
let app_dir = app_exe
|
||||||
unsafe {
|
.parent()
|
||||||
if PORTABLE_FLAG {
|
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
||||||
APP_HOME_DIR = Some(portable_home_dir.clone());
|
return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID));
|
||||||
Ok(portable_home_dir)
|
|
||||||
} else {
|
|
||||||
APP_HOME_DIR = Some(home_dir.clone());
|
|
||||||
Ok(home_dir)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(data_dir()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
|
||||||
|
.join(APP_ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the resources dir
|
/// get the resources dir
|
||||||
pub fn app_resources_dir(package_info: &PackageInfo) -> Result<PathBuf> {
|
pub fn app_resources_dir() -> Result<PathBuf> {
|
||||||
let res_dir = resource_dir(package_info, &Env::default())
|
let handle = handle::Handle::global();
|
||||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
let app_handle = handle.app_handle.lock();
|
||||||
.join("resources");
|
if let Some(app_handle) = app_handle.as_ref() {
|
||||||
|
let res_dir = resource_dir(app_handle.package_info(), &Env::default())
|
||||||
unsafe {
|
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
||||||
RESOURCE_DIR = Some(res_dir.clone());
|
.join("resources");
|
||||||
|
return Ok(res_dir);
|
||||||
let ver = package_info.version.to_string();
|
};
|
||||||
let ver_str = format!("v{ver}");
|
Err(anyhow::anyhow!("failed to get the resource dir"))
|
||||||
APP_VERSION = Box::leak(Box::new(ver_str));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res_dir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// profiles dir
|
/// profiles dir
|
||||||
@@ -105,32 +88,13 @@ pub fn profiles_path() -> Result<PathBuf> {
|
|||||||
Ok(app_home_dir()?.join(PROFILE_YAML))
|
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> {
|
pub fn clash_pid_path() -> Result<PathBuf> {
|
||||||
unsafe {
|
Ok(app_home_dir()?.join("clash.pid"))
|
||||||
Ok(APP_HOME_DIR
|
|
||||||
.clone()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
|
|
||||||
.join("clash.pid"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub fn service_path() -> Result<PathBuf> {
|
pub fn service_path() -> Result<PathBuf> {
|
||||||
unsafe {
|
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
|
||||||
let res_dir = RESOURCE_DIR
|
|
||||||
.clone()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?;
|
|
||||||
Ok(res_dir.join("clash-verge-service.exe"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
+30
-20
@@ -14,7 +14,7 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
|||||||
bail!("file not found \"{}\"", path.display());
|
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()))?;
|
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||||
|
|
||||||
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
|
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
|
||||||
@@ -71,18 +71,28 @@ pub fn get_uid(prefix: &str) -> String {
|
|||||||
/// parse the string
|
/// parse the string
|
||||||
/// xxx=123123; => 123123
|
/// xxx=123123; => 123123
|
||||||
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||||
target.find(key).and_then(|idx| {
|
target.split(';').map(str::trim).find_map(|s| {
|
||||||
let idx = idx + key.len();
|
let mut parts = s.splitn(2, '=');
|
||||||
let value = &target[idx..];
|
match (parts.next(), parts.next()) {
|
||||||
|
(Some(k), Some(v)) if k == key => v.parse::<T>().ok(),
|
||||||
match value.split(';').nth(0) {
|
_ => None,
|
||||||
Some(value) => value.trim().parse(),
|
|
||||||
None => value.trim().parse(),
|
|
||||||
}
|
}
|
||||||
.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
|
/// open file
|
||||||
/// use vscode by default
|
/// use vscode by default
|
||||||
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
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 code = "code";
|
||||||
|
|
||||||
let _ = match Program::from_str(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) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "Can't find VScode `{err}`");
|
log::error!(target: "app", "Can't find VScode `{err}`");
|
||||||
// default open
|
// 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_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||||
let test_2 = "attachment; filename=Clash.yaml";
|
let test_2 = "attachment; filename=Clash.yaml";
|
||||||
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
|
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, "download").unwrap(), 2222);
|
||||||
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
|
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, "expire").unwrap(), 444);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_str::<String>(test_2, "filename=").unwrap(),
|
parse_str::<String>(test_2, "filename").unwrap(),
|
||||||
format!("Clash.yaml")
|
format!("Clash.yaml")
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "aaa="), 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, "upload1"), None);
|
||||||
assert_eq!(parse_str::<usize>(test_1, "expire1="), 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_2, "attachment"), None);
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-14
@@ -8,8 +8,9 @@ use log4rs::append::file::FileAppender;
|
|||||||
use log4rs::config::{Appender, Logger, Root};
|
use log4rs::config::{Appender, Logger, Root};
|
||||||
use log4rs::encode::pattern::PatternEncoder;
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
use std::fs::{self, DirEntry};
|
use std::fs::{self, DirEntry};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tauri::PackageInfo;
|
use tauri::api::process::Command;
|
||||||
|
|
||||||
/// initialize this instance's log file
|
/// initialize this instance's log file
|
||||||
fn init_log() -> Result<()> {
|
fn init_log() -> Result<()> {
|
||||||
@@ -80,7 +81,7 @@ pub fn delete_log() -> Result<()> {
|
|||||||
let auto_log_clean = {
|
let auto_log_clean = {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge = verge.data();
|
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 {
|
let day = match auto_log_clean {
|
||||||
@@ -116,7 +117,10 @@ pub fn delete_log() -> Result<()> {
|
|||||||
if file_name.ends_with(".log") {
|
if file_name.ends_with(".log") {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
|
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);
|
let duration = now.signed_duration_since(file_time);
|
||||||
if duration.num_days() > day {
|
if duration.num_days() > day {
|
||||||
@@ -128,10 +132,8 @@ pub fn delete_log() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
for file in fs::read_dir(&log_dir)? {
|
for file in fs::read_dir(&log_dir)?.flatten() {
|
||||||
if let Ok(file) = file {
|
let _ = process_file(file);
|
||||||
let _ = process_file(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -139,11 +141,7 @@ pub fn delete_log() -> Result<()> {
|
|||||||
/// Initialize all the config files
|
/// Initialize all the config files
|
||||||
/// before tauri setup
|
/// before tauri setup
|
||||||
pub fn init_config() -> Result<()> {
|
pub fn init_config() -> Result<()> {
|
||||||
#[cfg(target_os = "windows")]
|
let _ = dirs::init_portable_flag();
|
||||||
unsafe {
|
|
||||||
let _ = dirs::init_portable_flag();
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = init_log();
|
let _ = init_log();
|
||||||
let _ = delete_log();
|
let _ = delete_log();
|
||||||
|
|
||||||
@@ -185,9 +183,9 @@ pub fn init_config() -> Result<()> {
|
|||||||
|
|
||||||
/// initialize app resources
|
/// initialize app resources
|
||||||
/// after tauri setup
|
/// after tauri setup
|
||||||
pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
pub fn init_resources() -> Result<()> {
|
||||||
let app_dir = dirs::app_home_dir()?;
|
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() {
|
if !app_dir.exists() {
|
||||||
let _ = fs::create_dir_all(&app_dir);
|
let _ = fs::create_dir_all(&app_dir);
|
||||||
@@ -241,3 +239,86 @@ pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ pub mod init;
|
|||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod tmpl;
|
pub mod tmpl;
|
||||||
// mod winhelp;
|
|
||||||
|
|||||||
+110
-63
@@ -1,10 +1,20 @@
|
|||||||
use crate::config::IVerge;
|
use crate::config::{IVerge, PrfOption};
|
||||||
use crate::{config::Config, core::*, utils::init, utils::server};
|
use crate::{
|
||||||
|
config::{Config, PrfItem},
|
||||||
|
core::*,
|
||||||
|
utils::init,
|
||||||
|
utils::server,
|
||||||
|
};
|
||||||
use crate::{log_err, trace_err};
|
use crate::{log_err, trace_err};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
|
use tauri::api::notification;
|
||||||
use tauri::{App, AppHandle, Manager};
|
use tauri::{App, AppHandle, Manager};
|
||||||
|
use window_shadows::set_shadow;
|
||||||
|
|
||||||
|
pub static VERSION: OnceCell<String> = OnceCell::new();
|
||||||
|
|
||||||
pub fn find_unused_port() -> Result<u16> {
|
pub fn find_unused_port() -> Result<u16> {
|
||||||
match TcpListener::bind("127.0.0.1:0") {
|
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) {
|
pub fn resolve_setup(app: &mut App) {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
|
let version = app.package_info().version.to_string();
|
||||||
handle::Handle::global().init(app.app_handle());
|
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);
|
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::trace!("init system tray");
|
||||||
log_err!(tray::Tray::update_systray(&app.app_handle()));
|
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) {
|
if !silent_start.unwrap_or(false) {
|
||||||
create_window(&app.app_handle());
|
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!(handle::Handle::update_systray_part());
|
||||||
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
|
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
|
||||||
log_err!(timer::Timer::global().init());
|
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
|
/// reset system proxy
|
||||||
@@ -136,65 +155,69 @@ pub fn create_window(app_handle: &AppHandle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
let window = builder
|
||||||
use window_shadows::set_shadow;
|
.decorations(false)
|
||||||
|
.transparent(true)
|
||||||
match builder
|
.visible(false)
|
||||||
.decorations(false)
|
.build();
|
||||||
.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();
|
|
||||||
|
|
||||||
if let Some(window) = app_handle.get_window("main") {
|
|
||||||
trace_err!(set_shadow(&window, true), "set win shadow");
|
|
||||||
} else {
|
|
||||||
log::error!(target: "app", "failed to create window, get_window is None")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => log::error!(target: "app", "failed to create window, {err}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
crate::log_err!(builder
|
let window = builder
|
||||||
.decorations(true)
|
.decorations(true)
|
||||||
.hidden_title(true)
|
.hidden_title(true)
|
||||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
.build());
|
.build();
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[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
|
/// save window size and position
|
||||||
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
|
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
|
let win = app_handle
|
||||||
.get_window("main")
|
.get_window("main")
|
||||||
.ok_or(anyhow::anyhow!("failed to get window"))?;
|
.ok_or(anyhow::anyhow!("failed to get window"))?;
|
||||||
@@ -204,14 +227,38 @@ pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) ->
|
|||||||
let size = size.to_logical::<f64>(scale);
|
let size = size.to_logical::<f64>(scale);
|
||||||
let pos = win.outer_position()?;
|
let pos = win.outer_position()?;
|
||||||
let pos = pos.to_logical::<f64>(scale);
|
let pos = pos.to_logical::<f64>(scale);
|
||||||
|
let is_maximized = win.is_maximized()?;
|
||||||
let verge = Config::verge();
|
verge.window_is_maximized = Some(is_maximized);
|
||||||
let mut verge = verge.latest();
|
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]);
|
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
|
||||||
|
|
||||||
if save_to_file {
|
|
||||||
verge.save_file()?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,19 +4,42 @@ use super::resolve;
|
|||||||
use crate::config::IVerge;
|
use crate::config::IVerge;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use port_scanner::local_port_available;
|
use port_scanner::local_port_available;
|
||||||
|
use std::convert::Infallible;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct QueryParam {
|
||||||
|
param: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// check whether there is already exists
|
/// check whether there is already exists
|
||||||
pub fn check_singleton() -> Result<()> {
|
pub fn check_singleton() -> Result<()> {
|
||||||
let port = IVerge::get_singleton_port();
|
let port = IVerge::get_singleton_port();
|
||||||
|
|
||||||
if !local_port_available(port) {
|
if !local_port_available(port) {
|
||||||
tauri::async_runtime::block_on(async {
|
tauri::async_runtime::block_on(async {
|
||||||
let url = format!("http://127.0.0.1:{port}/commands/visible");
|
let resp = reqwest::get(format!("http://127.0.0.1:{port}/commands/ping"))
|
||||||
let resp = reqwest::get(url).await?.text().await?;
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
|
||||||
if &resp == "ok" {
|
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");
|
bail!("app exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,11 +57,22 @@ pub fn embed_server(app_handle: AppHandle) {
|
|||||||
let port = IVerge::get_singleton_port();
|
let port = IVerge::get_singleton_port();
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
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);
|
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,4 +1,4 @@
|
|||||||
///! Some config file template
|
//! Some config file template
|
||||||
|
|
||||||
/// template for new a profile item
|
/// template for new a profile item
|
||||||
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
|
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Clash Verge",
|
"productName": "Clash Verge",
|
||||||
"version": "1.4.4"
|
"version": "1.5.2"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
@@ -51,11 +51,18 @@
|
|||||||
},
|
},
|
||||||
"clipboard": {
|
"clipboard": {
|
||||||
"all": true
|
"all": true
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"dialog": {
|
||||||
|
"all": false,
|
||||||
|
"open": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"windows": [],
|
"windows": [],
|
||||||
"security": {
|
"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';"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"targets": ["deb", "appimage", "updater"],
|
"targets": ["deb", "appimage", "updater"],
|
||||||
"deb": {
|
"deb": {
|
||||||
"depends": ["openssl"]
|
"depends": ["openssl"],
|
||||||
|
"desktopTemplate": "./template/clash-verge.desktop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"systemTray": {
|
"systemTray": {
|
||||||
"iconPath": "icons/icon.png",
|
"iconPath": "icons/mac-tray-icon.png",
|
||||||
"iconAsTemplate": true
|
"iconAsTemplate": true
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"targets": ["app", "dmg", "updater"],
|
"targets": ["app", "dmg", "updater"],
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"frameworks": [],
|
"frameworks": [],
|
||||||
"minimumSystemVersion": "",
|
"minimumSystemVersion": "10.15",
|
||||||
"exceptionDomain": "",
|
"exceptionDomain": "",
|
||||||
"signingIdentity": null,
|
"signingIdentity": null,
|
||||||
"entitlements": null
|
"entitlements": null
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
"displayLanguageSelector": true,
|
"displayLanguageSelector": true,
|
||||||
"installerIcon": "icons/icon.ico",
|
"installerIcon": "icons/icon.ico",
|
||||||
"languages": ["SimpChinese", "English"],
|
"languages": ["SimpChinese", "English"],
|
||||||
"license": "../LICENSE"
|
"license": "../LICENSE",
|
||||||
|
"installMode": "perMachine",
|
||||||
|
"template": "./template/installer.nsi"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -27,8 +27,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 8px;
|
||||||
height: 6px;
|
height: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
|
|
||||||
.the-bar {
|
.the-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 0px;
|
||||||
right: 8px;
|
right: 0px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
.base-container {
|
.base-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--border-radius);
|
border-top-left-radius: var(--border-radius);
|
||||||
|
|
||||||
> section {
|
> section {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -27,15 +27,25 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 5px 5px;
|
padding: 10px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
|
|
||||||
.base-content {
|
.base-content {
|
||||||
width: 100%;
|
width: calc(100% - 10px * 2);
|
||||||
// max-width: 850px;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.no-padding {
|
||||||
|
> section {
|
||||||
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.base-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,18 @@ interface Props {
|
|||||||
header?: React.ReactNode; // something behind title
|
header?: React.ReactNode; // something behind title
|
||||||
contentStyle?: React.CSSProperties;
|
contentStyle?: React.CSSProperties;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
full?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BasePage: React.FC<Props> = (props) => {
|
export const BasePage: React.FC<Props> = (props) => {
|
||||||
const { title, header, contentStyle, children } = props;
|
const { title, header, contentStyle, full, children } = props;
|
||||||
const { theme } = useCustomTheme();
|
const { theme } = useCustomTheme();
|
||||||
|
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseErrorBoundary>
|
<BaseErrorBoundary>
|
||||||
<div className="base-page" data-windrag>
|
<div className="base-page">
|
||||||
<header data-windrag style={{ userSelect: "none" }}>
|
<header data-windrag style={{ userSelect: "none" }}>
|
||||||
<Typography variant="h4" component="h1" data-windrag>
|
<Typography variant="h4" component="h1" data-windrag>
|
||||||
{title}
|
{title}
|
||||||
@@ -28,7 +29,7 @@ export const BasePage: React.FC<Props> = (props) => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="base-container"
|
className={full ? "base-container no-padding" : "base-container"}
|
||||||
style={{ backgroundColor: isDark ? "#090909" : "#ffffff" }}
|
style={{ backgroundColor: isDark ? "#090909" : "#ffffff" }}
|
||||||
>
|
>
|
||||||
<section
|
<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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useMemo, useState } from "react";
|
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 { truncateStr } from "@/utils/truncate-str";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
|
||||||
@@ -24,6 +28,8 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
width: 88,
|
width: 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
|
valueFormatter: (params: GridValueFormatterParams<number>) =>
|
||||||
|
parseTraffic(params.value).join(" "),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "upload",
|
field: "upload",
|
||||||
@@ -31,6 +37,8 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
width: 88,
|
width: 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
|
valueFormatter: (params: GridValueFormatterParams<number>) =>
|
||||||
|
parseTraffic(params.value).join(" "),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "dlSpeed",
|
field: "dlSpeed",
|
||||||
@@ -38,6 +46,8 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
width: 88,
|
width: 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
|
valueFormatter: (params: GridValueFormatterParams<number>) =>
|
||||||
|
parseTraffic(params.value).join(" ") + "/s",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "ulSpeed",
|
field: "ulSpeed",
|
||||||
@@ -45,10 +55,12 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
width: 88,
|
width: 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
|
valueFormatter: (params: GridValueFormatterParams<number>) =>
|
||||||
|
parseTraffic(params.value).join(" ") + "/s",
|
||||||
},
|
},
|
||||||
{ field: "chains", headerName: "Chains", flex: 360, minWidth: 360 },
|
{ field: "chains", headerName: "Chains", flex: 360, minWidth: 360 },
|
||||||
{ field: "rule", headerName: "Rule", flex: 300, minWidth: 250 },
|
{ 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",
|
field: "time",
|
||||||
headerName: "Time",
|
headerName: "Time",
|
||||||
@@ -56,6 +68,11 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "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 },
|
{ field: "source", headerName: "Source", flex: 200, minWidth: 130 },
|
||||||
{
|
{
|
||||||
@@ -72,20 +89,19 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
const { metadata, rulePayload } = each;
|
const { metadata, rulePayload } = each;
|
||||||
const chains = [...each.chains].reverse().join(" / ");
|
const chains = [...each.chains].reverse().join(" / ");
|
||||||
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: each.id,
|
id: each.id,
|
||||||
host: metadata.host
|
host: metadata.host
|
||||||
? `${metadata.host}:${metadata.destinationPort}`
|
? `${metadata.host}:${metadata.destinationPort}`
|
||||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
||||||
download: parseTraffic(each.download).join(" "),
|
download: each.download,
|
||||||
upload: parseTraffic(each.upload).join(" "),
|
upload: each.upload,
|
||||||
dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s",
|
dlSpeed: each.curDownload,
|
||||||
ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s",
|
ulSpeed: each.curUpload,
|
||||||
chains,
|
chains,
|
||||||
rule,
|
rule,
|
||||||
process: truncateStr(metadata.process || metadata.processPath),
|
process: truncateStr(metadata.process || metadata.processPath),
|
||||||
time: dayjs(each.start).fromNow(),
|
time: each.start,
|
||||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||||
destinationIP: metadata.destinationIP,
|
destinationIP: metadata.destinationIP,
|
||||||
type: `${metadata.type}(${metadata.network})`,
|
type: `${metadata.type}(${metadata.network})`,
|
||||||
|
|||||||
@@ -1,16 +1,50 @@
|
|||||||
import { Button } from "@mui/material";
|
import { Button, ButtonGroup } from "@mui/material";
|
||||||
import { appWindow } from "@tauri-apps/api/window";
|
import { appWindow } from "@tauri-apps/api/window";
|
||||||
import {
|
import {
|
||||||
CloseRounded,
|
CloseRounded,
|
||||||
CropSquareRounded,
|
CropSquareRounded,
|
||||||
|
FilterNoneRounded,
|
||||||
HorizontalRuleRounded,
|
HorizontalRuleRounded,
|
||||||
|
PushPinOutlined,
|
||||||
|
PushPinRounded,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export const LayoutControl = () => {
|
export const LayoutControl = () => {
|
||||||
const minWidth = 40;
|
const minWidth = 40;
|
||||||
|
|
||||||
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
|
const [isPined, setIsPined] = useState(false);
|
||||||
|
appWindow.isMaximized().then((isMaximized) => {
|
||||||
|
setIsMaximized(() => isMaximized);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
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
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ minWidth, svg: { transform: "scale(0.9)" } }}
|
sx={{ minWidth, svg: { transform: "scale(0.9)" } }}
|
||||||
@@ -22,18 +56,34 @@ export const LayoutControl = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ minWidth, svg: { transform: "scale(0.9)" } }}
|
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>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ minWidth, svg: { transform: "scale(1.05)" } }}
|
sx={{
|
||||||
|
minWidth,
|
||||||
|
svg: { transform: "scale(1.05)" },
|
||||||
|
":hover": { bgcolor: "#ff000090" },
|
||||||
|
}}
|
||||||
onClick={() => appWindow.close()}
|
onClick={() => appWindow.close()}
|
||||||
>
|
>
|
||||||
<CloseRounded fontSize="small" />
|
<CloseRounded fontSize="small" />
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,25 +7,32 @@ import {
|
|||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
|
styled,
|
||||||
|
Box,
|
||||||
|
alpha,
|
||||||
|
Typography,
|
||||||
|
Divider,
|
||||||
|
LinearProgress,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { RefreshRounded } from "@mui/icons-material";
|
import { RefreshRounded } from "@mui/icons-material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { getProviders, providerUpdate } from "@/services/api";
|
import { getProxyProviders, proxyProviderUpdate } from "@/services/api";
|
||||||
import { BaseDialog } from "../base";
|
import { BaseDialog } from "../base";
|
||||||
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
|
||||||
export const ProviderButton = () => {
|
export const ProviderButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data } = useSWR("getProviders", getProviders);
|
const { data } = useSWR("getProxyProviders", getProxyProviders);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const hasProvider = Object.keys(data || {}).length > 0;
|
const hasProvider = Object.keys(data || {}).length > 0;
|
||||||
|
|
||||||
const handleUpdate = useLockFn(async (key: string) => {
|
const handleUpdate = useLockFn(async (key: string) => {
|
||||||
await providerUpdate(key);
|
await proxyProviderUpdate(key);
|
||||||
await mutate("getProxies");
|
await mutate("getProxies");
|
||||||
await mutate("getProviders");
|
await mutate("getProxyProviders");
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasProvider) return null;
|
if (!hasProvider) return null;
|
||||||
@@ -43,7 +50,23 @@ export const ProviderButton = () => {
|
|||||||
|
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
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 }}
|
contentSx={{ width: 400 }}
|
||||||
disableOk
|
disableOk
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("Cancel")}
|
||||||
@@ -53,30 +76,84 @@ export const ProviderButton = () => {
|
|||||||
<List sx={{ py: 0, minHeight: 250 }}>
|
<List sx={{ py: 0, minHeight: 250 }}>
|
||||||
{Object.entries(data || {}).map(([key, item]) => {
|
{Object.entries(data || {}).map(([key, item]) => {
|
||||||
const time = dayjs(item.updatedAt);
|
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 (
|
return (
|
||||||
<ListItem sx={{ p: 0 }} key={key}>
|
<>
|
||||||
<ListItemText
|
<ListItem
|
||||||
primary={key}
|
sx={(theme) => ({
|
||||||
secondary={
|
p: 0,
|
||||||
<>
|
borderRadius: "10px",
|
||||||
<span style={{ marginRight: "4em" }}>
|
boxShadow: theme.shadows[2],
|
||||||
Type: {item.vehicleType}
|
mb: 1,
|
||||||
</span>
|
})}
|
||||||
<span title={time.format("YYYY-MM-DD HH:mm:ss")}>
|
key={key}
|
||||||
Updated: {time.fromNow()}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
title="Update Provider"
|
|
||||||
onClick={() => handleUpdate(key)}
|
|
||||||
>
|
>
|
||||||
<RefreshRounded />
|
<ListItemText
|
||||||
</IconButton>
|
sx={{ px: 1 }}
|
||||||
</ListItem>
|
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>
|
</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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { current, patchCurrent } = useProfiles();
|
const { current, patchCurrent } = useProfiles();
|
||||||
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
|
|
||||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
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);
|
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
|
||||||
await delayManager.checkListDelay(names, groupName);
|
await delayManager.checkListDelay(names, groupName, timeout);
|
||||||
|
|
||||||
onProxies();
|
onProxies();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CheckCircleOutlineRounded } from "@mui/icons-material";
|
|||||||
import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
|
import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
|
||||||
import { BaseLoading } from "@/components/base";
|
import { BaseLoading } from "@/components/base";
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from "@/services/delay";
|
||||||
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
groupName: string;
|
groupName: string;
|
||||||
@@ -20,6 +21,8 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
// -1/<=0 为 不显示
|
// -1/<=0 为 不显示
|
||||||
// -2 为 loading
|
// -2 为 loading
|
||||||
const [delay, setDelay] = useState(-1);
|
const [delay, setDelay] = useState(-1);
|
||||||
|
const { verge } = useVerge();
|
||||||
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
delayManager.setListener(proxy.name, groupName, setDelay);
|
delayManager.setListener(proxy.name, groupName, setDelay);
|
||||||
@@ -36,7 +39,7 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelay(-2);
|
setDelay(-2);
|
||||||
setDelay(await delayManager.checkDelay(proxy.name, groupName));
|
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -98,6 +101,8 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
<TypeBox component="span">{proxy.type}</TypeBox>
|
<TypeBox component="span">{proxy.type}</TypeBox>
|
||||||
{proxy.udp && <TypeBox component="span">UDP</TypeBox>}
|
{proxy.udp && <TypeBox component="span">UDP</TypeBox>}
|
||||||
|
{proxy.xudp && <TypeBox component="span">XUDP</TypeBox>}
|
||||||
|
{proxy.tfo && <TypeBox component="span">TFO</TypeBox>}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -137,14 +142,14 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelay();
|
onDelay();
|
||||||
}}
|
}}
|
||||||
color={delayManager.formatDelayColor(delay)}
|
color={delayManager.formatDelayColor(delay, timeout)}
|
||||||
sx={({ palette }) =>
|
sx={({ palette }) =>
|
||||||
!proxy.provider
|
!proxy.provider
|
||||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{delayManager.formatDelay(delay)}
|
{delayManager.formatDelay(delay, timeout)}
|
||||||
</Widget>
|
</Widget>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { BaseLoading } from "@/components/base";
|
import { BaseLoading } from "@/components/base";
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from "@/services/delay";
|
||||||
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
groupName: string;
|
groupName: string;
|
||||||
@@ -48,7 +49,8 @@ export const ProxyItem = (props: Props) => {
|
|||||||
// -1/<=0 为 不显示
|
// -1/<=0 为 不显示
|
||||||
// -2 为 loading
|
// -2 为 loading
|
||||||
const [delay, setDelay] = useState(-1);
|
const [delay, setDelay] = useState(-1);
|
||||||
|
const { verge } = useVerge();
|
||||||
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
delayManager.setListener(proxy.name, groupName, setDelay);
|
delayManager.setListener(proxy.name, groupName, setDelay);
|
||||||
|
|
||||||
@@ -64,7 +66,7 @@ export const ProxyItem = (props: Props) => {
|
|||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelay(-2);
|
setDelay(-2);
|
||||||
setDelay(await delayManager.checkDelay(proxy.name, groupName));
|
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -104,6 +106,10 @@ export const ProxyItem = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
{showType && <TypeBox component="span">{proxy.type}</TypeBox>}
|
{showType && <TypeBox component="span">{proxy.type}</TypeBox>}
|
||||||
{showType && proxy.udp && <TypeBox component="span">UDP</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();
|
e.stopPropagation();
|
||||||
onDelay();
|
onDelay();
|
||||||
}}
|
}}
|
||||||
color={delayManager.formatDelayColor(delay)}
|
color={delayManager.formatDelayColor(delay, timeout)}
|
||||||
sx={({ palette }) =>
|
sx={({ palette }) =>
|
||||||
!proxy.provider
|
!proxy.provider
|
||||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{delayManager.formatDelay(delay)}
|
{delayManager.formatDelay(delay, timeout)}
|
||||||
</Widget>
|
</Widget>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -31,12 +31,24 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
props;
|
props;
|
||||||
const { type, group, headState, proxy, proxyCol } = item;
|
const { type, group, headState, proxy, proxyCol } = item;
|
||||||
|
|
||||||
if (type === 0) {
|
if (type === 0 && !group.hidden) {
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
dense
|
dense
|
||||||
onClick={() => onHeadState(group.name, { open: !headState?.open })}
|
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
|
<ListItemText
|
||||||
primary={group.name}
|
primary={group.name}
|
||||||
secondary={
|
secondary={
|
||||||
@@ -61,7 +73,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 1) {
|
if (type === 1 && !group.hidden) {
|
||||||
return (
|
return (
|
||||||
<ProxyHead
|
<ProxyHead
|
||||||
sx={{ pl: indent ? 4.5 : 2.5, pr: 3, mt: indent ? 1 : 0.5, mb: 1 }}
|
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 (
|
return (
|
||||||
<ProxyItem
|
<ProxyItem
|
||||||
groupName={group.name}
|
groupName={group.name}
|
||||||
@@ -87,7 +99,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 3) {
|
if (type === 3 && !group.hidden) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -105,7 +117,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 4) {
|
if (type === 4 && !group.hidden) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type HeadStateStorage = Record<string, Record<string, HeadState>>;
|
|||||||
const HEAD_STATE_KEY = "proxy-head-state";
|
const HEAD_STATE_KEY = "proxy-head-state";
|
||||||
export const DEFAULT_STATE: HeadState = {
|
export const DEFAULT_STATE: HeadState = {
|
||||||
open: false,
|
open: false,
|
||||||
showType: false,
|
showType: true,
|
||||||
sortType: 0,
|
sortType: 0,
|
||||||
filterText: "",
|
filterText: "",
|
||||||
textState: null,
|
textState: null,
|
||||||
|
|||||||
@@ -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 { useTranslation } from "react-i18next";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Lock } from "@mui/icons-material";
|
import { LoadingButton } from "@mui/lab";
|
||||||
|
import { SwitchAccessShortcut, RestartAlt } from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -31,6 +32,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
const { verge, mutateVerge } = useVerge();
|
const { verge, mutateVerge } = useVerge();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [upgrading, setUpgrading] = useState(false);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => setOpen(true),
|
open: () => setOpen(true),
|
||||||
@@ -78,9 +80,12 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
|
|
||||||
const onUpgrade = useLockFn(async () => {
|
const onUpgrade = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
|
setUpgrading(true);
|
||||||
await upgradeCore();
|
await upgradeCore();
|
||||||
|
setUpgrading(false);
|
||||||
Notice.success(`Successfully upgrade core`, 1000);
|
Notice.success(`Successfully upgrade core`, 1000);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
setUpgrading(false);
|
||||||
Notice.error(err?.response.data.message || err.toString());
|
Notice.error(err?.response.data.message || err.toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -93,16 +98,24 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
{t("Clash Core")}
|
{t("Clash Core")}
|
||||||
<Box>
|
<Box>
|
||||||
{clash_core !== "clash-meta" && (
|
{clash_core !== "clash-meta" && (
|
||||||
<Button
|
<LoadingButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
|
startIcon={<SwitchAccessShortcut />}
|
||||||
|
loadingPosition="start"
|
||||||
|
loading={upgrading}
|
||||||
sx={{ marginRight: "8px" }}
|
sx={{ marginRight: "8px" }}
|
||||||
onClick={onUpgrade}
|
onClick={onUpgrade}
|
||||||
>
|
>
|
||||||
{t("Upgrade")}
|
{t("Upgrade")}
|
||||||
</Button>
|
</LoadingButton>
|
||||||
)}
|
)}
|
||||||
<Button variant="contained" size="small" onClick={onRestart}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={onRestart}
|
||||||
|
startIcon={<RestartAlt />}
|
||||||
|
>
|
||||||
{t("Restart")}
|
{t("Restart")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -110,7 +123,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}
|
}
|
||||||
contentSx={{
|
contentSx={{
|
||||||
pb: 0,
|
pb: 0,
|
||||||
width: 320,
|
width: 400,
|
||||||
height: 180,
|
height: 180,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
userSelect: "text",
|
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 { verge, patchVerge } = useVerge();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
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(
|
const [port, setPort] = useState(
|
||||||
verge?.verge_mixed_port ?? clashInfo?.port ?? 7897
|
verge?.verge_port ?? clashInfo?.port ?? 7899
|
||||||
);
|
);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
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);
|
setOpen(true);
|
||||||
},
|
},
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const onSave = useLockFn(async () => {
|
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);
|
setOpen(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await patchInfo({ "mixed-port": port });
|
await patchInfo({ "mixed-port": mixedPort });
|
||||||
await patchVerge({ verge_mixed_port: port });
|
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);
|
setOpen(false);
|
||||||
Notice.success("Change Clash port successfully!", 1000);
|
Notice.success("Change Clash port successfully!", 1000);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -54,6 +70,30 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
<List>
|
<List>
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary="Mixed Port" />
|
<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
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|||||||
@@ -36,19 +36,6 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
<List>
|
<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")}>
|
<SettingItem label={t("Traffic Graph")}>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={verge?.traffic_graph ?? true}
|
value={verge?.traffic_graph ?? true}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [values, setValues] = useState({
|
const [values, setValues] = useState({
|
||||||
appLogLevel: "info",
|
appLogLevel: "info",
|
||||||
autoCloseConnection: false,
|
autoCloseConnection: true,
|
||||||
enableClashFields: false,
|
|
||||||
enableBuiltinEnhanced: true,
|
enableBuiltinEnhanced: true,
|
||||||
proxyLayoutColumn: 6,
|
proxyLayoutColumn: 6,
|
||||||
defaultLatencyTest: "",
|
defaultLatencyTest: "",
|
||||||
autoLogClean: 0,
|
autoLogClean: 0,
|
||||||
|
defaultLatencyTimeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -34,11 +34,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
setValues({
|
setValues({
|
||||||
appLogLevel: verge?.app_log_level ?? "info",
|
appLogLevel: verge?.app_log_level ?? "info",
|
||||||
autoCloseConnection: verge?.auto_close_connection ?? true,
|
autoCloseConnection: verge?.auto_close_connection ?? true,
|
||||||
enableClashFields: verge?.enable_clash_fields ?? false,
|
|
||||||
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
||||||
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
||||||
defaultLatencyTest: verge?.default_latency_test || "",
|
defaultLatencyTest: verge?.default_latency_test || "",
|
||||||
autoLogClean: verge?.auto_log_clean || 0,
|
autoLogClean: verge?.auto_log_clean || 0,
|
||||||
|
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
@@ -49,10 +49,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
await patchVerge({
|
await patchVerge({
|
||||||
app_log_level: values.appLogLevel,
|
app_log_level: values.appLogLevel,
|
||||||
auto_close_connection: values.autoCloseConnection,
|
auto_close_connection: values.autoCloseConnection,
|
||||||
enable_clash_fields: values.enableClashFields,
|
|
||||||
enable_builtin_enhanced: values.enableBuiltinEnhanced,
|
enable_builtin_enhanced: values.enableBuiltinEnhanced,
|
||||||
proxy_layout_column: values.proxyLayoutColumn,
|
proxy_layout_column: values.proxyLayoutColumn,
|
||||||
default_latency_test: values.defaultLatencyTest,
|
default_latency_test: values.defaultLatencyTest,
|
||||||
|
default_latency_timeout: values.defaultLatencyTimeout,
|
||||||
auto_log_clean: values.autoLogClean as any,
|
auto_log_clean: values.autoLogClean as any,
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -105,17 +105,6 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</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" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary={t("Enable Builtin Enhanced")} />
|
<ListItemText primary={t("Enable Builtin Enhanced")} />
|
||||||
<Switch
|
<Switch
|
||||||
@@ -193,6 +182,27 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</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>
|
</List>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -168,6 +168,6 @@ const FlexBox = styled("div")`
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 80px;
|
width: 85px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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 { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||||
import { atomUpdateState } from "@/services/states";
|
import { atomUpdateState } from "@/services/states";
|
||||||
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
|
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
import { portableFlag } from "@/pages/_layout";
|
||||||
|
|
||||||
const UpdateLog = styled(Box)(() => ({
|
const UpdateLog = styled(Box)(() => ({
|
||||||
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
|
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
|
||||||
@@ -46,6 +47,10 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}, [updateInfo]);
|
}, [updateInfo]);
|
||||||
|
|
||||||
const onUpdate = useLockFn(async () => {
|
const onUpdate = useLockFn(async () => {
|
||||||
|
if (portableFlag) {
|
||||||
|
Notice.error(t("Portable Updater Error"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (updateState) return;
|
if (updateState) return;
|
||||||
setUpdateState(true);
|
setUpdateState(true);
|
||||||
if (eventListener !== null) {
|
if (eventListener !== null) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { DialogRef, Notice } from "@/components/base";
|
|||||||
import { useClash } from "@/hooks/use-clash";
|
import { useClash } from "@/hooks/use-clash";
|
||||||
import { GuardState } from "./mods/guard-state";
|
import { GuardState } from "./mods/guard-state";
|
||||||
import { WebUIViewer } from "./mods/web-ui-viewer";
|
import { WebUIViewer } from "./mods/web-ui-viewer";
|
||||||
import { ClashFieldViewer } from "./mods/clash-field-viewer";
|
|
||||||
import { ClashPortViewer } from "./mods/clash-port-viewer";
|
import { ClashPortViewer } from "./mods/clash-port-viewer";
|
||||||
import { ControllerViewer } from "./mods/controller-viewer";
|
import { ControllerViewer } from "./mods/controller-viewer";
|
||||||
import { SettingList, SettingItem } from "./mods/setting-comp";
|
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||||
@@ -39,14 +38,9 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
|
|
||||||
const { ipv6, "allow-lan": allowLan, "log-level": logLevel } = clash ?? {};
|
const { ipv6, "allow-lan": allowLan, "log-level": logLevel } = clash ?? {};
|
||||||
|
|
||||||
const {
|
const { enable_random_port = false, verge_mixed_port } = verge ?? {};
|
||||||
enable_random_port = false,
|
|
||||||
verge_mixed_port,
|
|
||||||
enable_clash_fields = false,
|
|
||||||
} = verge ?? {};
|
|
||||||
|
|
||||||
const webRef = useRef<DialogRef>(null);
|
const webRef = useRef<DialogRef>(null);
|
||||||
const fieldRef = useRef<DialogRef>(null);
|
|
||||||
const portRef = useRef<DialogRef>(null);
|
const portRef = useRef<DialogRef>(null);
|
||||||
const ctrlRef = useRef<DialogRef>(null);
|
const ctrlRef = useRef<DialogRef>(null);
|
||||||
const coreRef = useRef<DialogRef>(null);
|
const coreRef = useRef<DialogRef>(null);
|
||||||
@@ -70,7 +64,6 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<SettingList title={t("Clash Setting")}>
|
<SettingList title={t("Clash Setting")}>
|
||||||
<WebUIViewer ref={webRef} />
|
<WebUIViewer ref={webRef} />
|
||||||
<ClashFieldViewer ref={fieldRef} />
|
|
||||||
<ClashPortViewer ref={portRef} />
|
<ClashPortViewer ref={portRef} />
|
||||||
<ControllerViewer ref={ctrlRef} />
|
<ControllerViewer ref={ctrlRef} />
|
||||||
<ClashCoreViewer ref={coreRef} />
|
<ClashCoreViewer ref={coreRef} />
|
||||||
@@ -121,7 +114,7 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem
|
<SettingItem
|
||||||
label={t("Mixed Port")}
|
label={t("Port Config")}
|
||||||
extra={
|
extra={
|
||||||
<Tooltip title={t("Random Port")}>
|
<Tooltip title={t("Random Port")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -176,19 +169,6 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</SettingItem>
|
</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
|
<SettingItem
|
||||||
label={t("Clash Core")}
|
label={t("Clash Core")}
|
||||||
extra={
|
extra={
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { IconButton, Switch } from "@mui/material";
|
import { IconButton, Switch, Tooltip } from "@mui/material";
|
||||||
import { ArrowForward, PrivacyTipRounded, Settings } from "@mui/icons-material";
|
import { PrivacyTipRounded, Settings, InfoRounded } from "@mui/icons-material";
|
||||||
import { checkService } from "@/services/cmds";
|
import { checkService } from "@/services/cmds";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { DialogRef } from "@/components/base";
|
import { DialogRef } from "@/components/base";
|
||||||
@@ -10,6 +10,7 @@ import { SettingList, SettingItem } from "./mods/setting-comp";
|
|||||||
import { GuardState } from "./mods/guard-state";
|
import { GuardState } from "./mods/guard-state";
|
||||||
import { ServiceViewer } from "./mods/service-viewer";
|
import { ServiceViewer } from "./mods/service-viewer";
|
||||||
import { SysproxyViewer } from "./mods/sysproxy-viewer";
|
import { SysproxyViewer } from "./mods/sysproxy-viewer";
|
||||||
|
import { TunViewer } from "./mods/tun-viewer";
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from "@/utils/get-system";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -36,6 +37,7 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
|
|
||||||
const serviceRef = useRef<DialogRef>(null);
|
const serviceRef = useRef<DialogRef>(null);
|
||||||
const sysproxyRef = useRef<DialogRef>(null);
|
const sysproxyRef = useRef<DialogRef>(null);
|
||||||
|
const tunRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
enable_tun_mode,
|
enable_tun_mode,
|
||||||
@@ -53,11 +55,41 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<SettingList title={t("System Setting")}>
|
<SettingList title={t("System Setting")}>
|
||||||
<SysproxyViewer ref={sysproxyRef} />
|
<SysproxyViewer ref={sysproxyRef} />
|
||||||
|
<TunViewer ref={tunRef} />
|
||||||
{isWIN && (
|
{isWIN && (
|
||||||
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
|
<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
|
<GuardState
|
||||||
value={enable_tun_mode ?? false}
|
value={enable_tun_mode ?? false}
|
||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { IconButton, MenuItem, Select, Typography } from "@mui/material";
|
import { open } from "@tauri-apps/api/dialog";
|
||||||
import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
|
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 { ArrowForward } from "@mui/icons-material";
|
||||||
import { checkUpdate } from "@tauri-apps/api/updater";
|
import { checkUpdate } from "@tauri-apps/api/updater";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
@@ -18,7 +26,7 @@ import { GuardState } from "./mods/guard-state";
|
|||||||
import { LayoutViewer } from "./mods/layout-viewer";
|
import { LayoutViewer } from "./mods/layout-viewer";
|
||||||
import { UpdateViewer } from "./mods/update-viewer";
|
import { UpdateViewer } from "./mods/update-viewer";
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from "@/utils/get-system";
|
||||||
|
import { routers } from "@/pages/_routers";
|
||||||
interface Props {
|
interface Props {
|
||||||
onError?: (err: Error) => void;
|
onError?: (err: Error) => void;
|
||||||
}
|
}
|
||||||
@@ -29,7 +37,14 @@ const SettingVerge = ({ onError }: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||||
const { theme_mode, language, tray_event, env_type } = verge ?? {};
|
const {
|
||||||
|
theme_mode,
|
||||||
|
language,
|
||||||
|
tray_event,
|
||||||
|
env_type,
|
||||||
|
startup_script,
|
||||||
|
start_page,
|
||||||
|
} = verge ?? {};
|
||||||
const configRef = useRef<DialogRef>(null);
|
const configRef = useRef<DialogRef>(null);
|
||||||
const hotkeyRef = useRef<DialogRef>(null);
|
const hotkeyRef = useRef<DialogRef>(null);
|
||||||
const miscRef = useRef<DialogRef>(null);
|
const miscRef = useRef<DialogRef>(null);
|
||||||
@@ -111,7 +126,7 @@ const SettingVerge = ({ onError }: Props) => {
|
|||||||
|
|
||||||
<SettingItem label={t("Copy Env Type")}>
|
<SettingItem label={t("Copy Env Type")}>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={env_type ?? OS === "windows" ? "powershell" : "bash"}
|
value={env_type ?? (OS === "windows" ? "powershell" : "bash")}
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
onFormat={(e: any) => e.target.value}
|
onFormat={(e: any) => e.target.value}
|
||||||
onChange={(e) => onChangeData({ env_type: e })}
|
onChange={(e) => onChangeData({ env_type: e })}
|
||||||
@@ -125,6 +140,70 @@ const SettingVerge = ({ onError }: Props) => {
|
|||||||
</GuardState>
|
</GuardState>
|
||||||
</SettingItem>
|
</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")}>
|
<SettingItem label={t("Theme Setting")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -213,18 +292,29 @@ const SettingVerge = ({ onError }: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
{!(OS === "windows" && WIN_PORTABLE) && (
|
<SettingItem label={t("Check for Updates")}>
|
||||||
<SettingItem label={t("Check for Updates")}>
|
<IconButton
|
||||||
<IconButton
|
color="inherit"
|
||||||
color="inherit"
|
size="small"
|
||||||
size="small"
|
sx={{ my: "2px" }}
|
||||||
sx={{ my: "2px" }}
|
onClick={onCheckUpdate}
|
||||||
onClick={onCheckUpdate}
|
>
|
||||||
>
|
<ArrowForward />
|
||||||
<ArrowForward />
|
</IconButton>
|
||||||
</IconButton>
|
</SettingItem>
|
||||||
</SettingItem>
|
|
||||||
)}
|
<SettingItem label={t("Exit")}>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
sx={{ my: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
exitApp();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowForward />
|
||||||
|
</IconButton>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem label={t("Verge Version")}>
|
<SettingItem label={t("Verge Version")}>
|
||||||
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
|
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -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",
|
||||||
|
}));
|
||||||
@@ -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
@@ -48,11 +48,16 @@ export const useClashInfo = () => {
|
|||||||
|
|
||||||
const patchInfo = async (
|
const patchInfo = async (
|
||||||
patch: Partial<
|
patch: Partial<
|
||||||
Pick<IConfigData, "mixed-port" | "external-controller" | "secret">
|
Pick<
|
||||||
|
IConfigData,
|
||||||
|
"port" | "socks-port" | "mixed-port" | "external-controller" | "secret"
|
||||||
|
>
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const hasInfo =
|
const hasInfo =
|
||||||
patch["mixed-port"] != null ||
|
patch["mixed-port"] != null ||
|
||||||
|
patch["socks-port"] != null ||
|
||||||
|
patch["port"] != null ||
|
||||||
patch["external-controller"] != null ||
|
patch["external-controller"] != null ||
|
||||||
patch.secret != 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);
|
await patchClashConfig(patch);
|
||||||
mutateInfo();
|
mutateInfo();
|
||||||
mutate("getClashConfig");
|
mutate("getClashConfig");
|
||||||
|
|||||||
+20
-3
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"Label-Proxies": "Proxies",
|
"Label-Proxies": "Proxies",
|
||||||
|
"Label-Test": "Test",
|
||||||
"Label-Profiles": "Profiles",
|
"Label-Profiles": "Profiles",
|
||||||
"Label-Connections": "Connections",
|
"Label-Connections": "Connections",
|
||||||
"Label-Logs": "Logs",
|
"Label-Logs": "Logs",
|
||||||
@@ -11,11 +12,17 @@
|
|||||||
"Clear": "Clear",
|
"Clear": "Clear",
|
||||||
"Proxies": "Proxies",
|
"Proxies": "Proxies",
|
||||||
"Proxy Groups": "Proxy Groups",
|
"Proxy Groups": "Proxy Groups",
|
||||||
|
"Test": "Test",
|
||||||
"rule": "rule",
|
"rule": "rule",
|
||||||
"global": "global",
|
"global": "global",
|
||||||
"direct": "direct",
|
"direct": "direct",
|
||||||
"script": "script",
|
"script": "script",
|
||||||
|
|
||||||
|
"Edit": "Edit",
|
||||||
|
"Icon": "Icon",
|
||||||
|
"Test URL": "Test URL",
|
||||||
|
"Test All": "Test All",
|
||||||
|
|
||||||
"Profiles": "Profiles",
|
"Profiles": "Profiles",
|
||||||
"Profile URL": "Profile URL",
|
"Profile URL": "Profile URL",
|
||||||
"Import": "Import",
|
"Import": "Import",
|
||||||
@@ -50,6 +57,8 @@
|
|||||||
"Filter conditions": "Filter conditions",
|
"Filter conditions": "Filter conditions",
|
||||||
"Refresh profiles": "Refresh profiles",
|
"Refresh profiles": "Refresh profiles",
|
||||||
"Rules": "Rules",
|
"Rules": "Rules",
|
||||||
|
"Update All": "Update All",
|
||||||
|
"Update At": "Update At",
|
||||||
|
|
||||||
"Type": "Type",
|
"Type": "Type",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
@@ -66,7 +75,7 @@
|
|||||||
"Allow Lan": "Allow Lan",
|
"Allow Lan": "Allow Lan",
|
||||||
"IPv6": "IPv6",
|
"IPv6": "IPv6",
|
||||||
"Log Level": "Log Level",
|
"Log Level": "Log Level",
|
||||||
"Mixed Port": "Mixed Port",
|
"Port Config": "Port Config",
|
||||||
"Random Port": "Random Port",
|
"Random Port": "Random Port",
|
||||||
"After restart to take effect": "After restart to take effect",
|
"After restart to take effect": "After restart to take effect",
|
||||||
"External": "External",
|
"External": "External",
|
||||||
@@ -89,9 +98,11 @@
|
|||||||
"Bypass": "Bypass",
|
"Bypass": "Bypass",
|
||||||
"Current System Proxy": "Current System Proxy",
|
"Current System Proxy": "Current System Proxy",
|
||||||
"Theme Mode": "Theme Mode",
|
"Theme Mode": "Theme Mode",
|
||||||
"Theme Blur": "Theme Blur",
|
|
||||||
"Tray Click Event": "Tray Click Event",
|
"Tray Click Event": "Tray Click Event",
|
||||||
"Copy Env Type": "Copy Env Type",
|
"Copy Env Type": "Copy Env Type",
|
||||||
|
"Start Page": "Start Page",
|
||||||
|
"Startup Script": "Startup Script",
|
||||||
|
"Browse": "Browse",
|
||||||
"Show Main Window": "Show Main Window",
|
"Show Main Window": "Show Main Window",
|
||||||
"Theme Setting": "Theme Setting",
|
"Theme Setting": "Theme Setting",
|
||||||
"Layout Setting": "Layout Setting",
|
"Layout Setting": "Layout Setting",
|
||||||
@@ -117,6 +128,7 @@
|
|||||||
"Back": "Back",
|
"Back": "Back",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
|
"Exit": "Exit",
|
||||||
|
|
||||||
"Default": "Default",
|
"Default": "Default",
|
||||||
"Download Speed": "Download Speed",
|
"Download Speed": "Download Speed",
|
||||||
@@ -140,10 +152,15 @@
|
|||||||
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
|
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
|
||||||
"Proxy Layout Column": "Proxy Layout Column",
|
"Proxy Layout Column": "Proxy Layout Column",
|
||||||
"Default Latency Test": "Default Latency Test",
|
"Default Latency Test": "Default Latency Test",
|
||||||
|
"Defaule Latency Timeout": "Defaule Latency Timeout",
|
||||||
|
|
||||||
"Auto Log Clean": "Auto Log Clean",
|
"Auto Log Clean": "Auto Log Clean",
|
||||||
"Never Clean": "Never Clean",
|
"Never Clean": "Never Clean",
|
||||||
"Retain 7 Days": "Retain 7 Days",
|
"Retain 7 Days": "Retain 7 Days",
|
||||||
"Retain 30 Days": "Retain 30 Days",
|
"Retain 30 Days": "Retain 30 Days",
|
||||||
"Retain 90 Days": "Retain 90 Days"
|
"Retain 90 Days": "Retain 90 Days",
|
||||||
|
|
||||||
|
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it",
|
||||||
|
"Tun Mode Info Windows": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
|
||||||
|
"Tun Mode Info Unix": "The Tun mode requires granting core-related permissions. Before using it, please authorize the core in the core settings"
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"Label-Proxies": "Прокси",
|
"Label-Proxies": "Прокси",
|
||||||
|
"Label-Test": "Тест",
|
||||||
"Label-Profiles": "Профили",
|
"Label-Profiles": "Профили",
|
||||||
"Label-Connections": "Соединения",
|
"Label-Connections": "Соединения",
|
||||||
"Label-Logs": "Логи",
|
"Label-Logs": "Логи",
|
||||||
@@ -10,12 +11,18 @@
|
|||||||
"Logs": "Логи",
|
"Logs": "Логи",
|
||||||
"Clear": "Очистить",
|
"Clear": "Очистить",
|
||||||
"Proxies": "Прокси",
|
"Proxies": "Прокси",
|
||||||
|
"Test": "Тест",
|
||||||
"Proxy Groups": "Группы прокси",
|
"Proxy Groups": "Группы прокси",
|
||||||
"rule": "правила",
|
"rule": "правила",
|
||||||
"global": "глобальный",
|
"global": "глобальный",
|
||||||
"direct": "прямой",
|
"direct": "прямой",
|
||||||
"script": "скриптовый",
|
"script": "скриптовый",
|
||||||
|
|
||||||
|
"Edit": "Редактировать",
|
||||||
|
"Icon": "Икона",
|
||||||
|
"Test URL": "Тестовый URL",
|
||||||
|
"Test All": "Тест Все",
|
||||||
|
|
||||||
"Profiles": "Профили",
|
"Profiles": "Профили",
|
||||||
"Profile URL": "URL профиля",
|
"Profile URL": "URL профиля",
|
||||||
"Import": "Импорт",
|
"Import": "Импорт",
|
||||||
@@ -49,6 +56,9 @@
|
|||||||
"Filter": "Фильтр",
|
"Filter": "Фильтр",
|
||||||
"Filter conditions": "Условия фильтрации",
|
"Filter conditions": "Условия фильтрации",
|
||||||
"Refresh profiles": "Обновить профили",
|
"Refresh profiles": "Обновить профили",
|
||||||
|
"Rules": "Правила",
|
||||||
|
"Update All": "Обновить все",
|
||||||
|
"Update At": "Обновлено в",
|
||||||
|
|
||||||
"Type": "Тип",
|
"Type": "Тип",
|
||||||
"Name": "Название",
|
"Name": "Название",
|
||||||
@@ -63,7 +73,7 @@
|
|||||||
"Allow Lan": "Разрешить локальную сеть",
|
"Allow Lan": "Разрешить локальную сеть",
|
||||||
"IPv6": "IPv6",
|
"IPv6": "IPv6",
|
||||||
"Log Level": "Уровень логов",
|
"Log Level": "Уровень логов",
|
||||||
"Mixed Port": "Смешанный порт",
|
"Port Config": "Настройка порта",
|
||||||
"Random Port": "Случайный порт",
|
"Random Port": "Случайный порт",
|
||||||
"After restart to take effect": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
|
"After restart to take effect": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
|
||||||
"Clash Core": "Ядро Clash",
|
"Clash Core": "Ядро Clash",
|
||||||
@@ -80,9 +90,11 @@
|
|||||||
"Proxy Bypass": "Игнорирование прокси",
|
"Proxy Bypass": "Игнорирование прокси",
|
||||||
"Current System Proxy": "Текущий системный прокси",
|
"Current System Proxy": "Текущий системный прокси",
|
||||||
"Theme Mode": "Режим темы",
|
"Theme Mode": "Режим темы",
|
||||||
"Theme Blur": "Размытие темы",
|
|
||||||
"Tray Click Event": "Событие щелчка в лотке",
|
"Tray Click Event": "Событие щелчка в лотке",
|
||||||
|
"Start Page": "Главная страница",
|
||||||
"Copy Env Type": "Скопировать тип Env",
|
"Copy Env Type": "Скопировать тип Env",
|
||||||
|
"Startup Script": "Скрипт запуска",
|
||||||
|
"Browse": "Просмотреть",
|
||||||
"Show Main Window": "Показать главное окно",
|
"Show Main Window": "Показать главное окно",
|
||||||
"Theme Setting": "Настройка темы",
|
"Theme Setting": "Настройка темы",
|
||||||
"Hotkey Setting": "Настройка клавиатурных сокращений",
|
"Hotkey Setting": "Настройка клавиатурных сокращений",
|
||||||
@@ -104,6 +116,7 @@
|
|||||||
"Back": "Назад",
|
"Back": "Назад",
|
||||||
"Save": "Сохранить",
|
"Save": "Сохранить",
|
||||||
"Cancel": "Отмена",
|
"Cancel": "Отмена",
|
||||||
|
"Exit": "Выход",
|
||||||
|
|
||||||
"open_dashboard": "Open Dashboard",
|
"open_dashboard": "Open Dashboard",
|
||||||
"clash_mode_rule": "Режим правил",
|
"clash_mode_rule": "Режим правил",
|
||||||
@@ -115,5 +128,7 @@
|
|||||||
"disable_system_proxy": "Отключить системный прокси",
|
"disable_system_proxy": "Отключить системный прокси",
|
||||||
"toggle_tun_mode": "Переключить режим туннеля",
|
"toggle_tun_mode": "Переключить режим туннеля",
|
||||||
"enable_tun_mode": "Включить режим туннеля",
|
"enable_tun_mode": "Включить режим туннеля",
|
||||||
"disable_tun_mode": "Отключить режим туннеля"
|
"disable_tun_mode": "Отключить режим туннеля",
|
||||||
|
|
||||||
|
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную"
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"Label-Proxies": "代 理",
|
"Label-Proxies": "代 理",
|
||||||
|
"Label-Test": "测 试",
|
||||||
"Label-Profiles": "订 阅",
|
"Label-Profiles": "订 阅",
|
||||||
"Label-Connections": "连 接",
|
"Label-Connections": "连 接",
|
||||||
"Label-Logs": "日 志",
|
"Label-Logs": "日 志",
|
||||||
@@ -11,11 +12,17 @@
|
|||||||
"Clear": "清除",
|
"Clear": "清除",
|
||||||
"Proxies": "代理",
|
"Proxies": "代理",
|
||||||
"Proxy Groups": "代理组",
|
"Proxy Groups": "代理组",
|
||||||
|
"Test": "测试",
|
||||||
"rule": "规则",
|
"rule": "规则",
|
||||||
"global": "全局",
|
"global": "全局",
|
||||||
"direct": "直连",
|
"direct": "直连",
|
||||||
"script": "脚本",
|
"script": "脚本",
|
||||||
|
|
||||||
|
"Edit": "编辑",
|
||||||
|
"Icon": "图标",
|
||||||
|
"Test URL": "测试地址",
|
||||||
|
"Test All": "测试全部",
|
||||||
|
|
||||||
"Profiles": "订阅",
|
"Profiles": "订阅",
|
||||||
"Profile URL": "订阅文件链接",
|
"Profile URL": "订阅文件链接",
|
||||||
"Import": "导入",
|
"Import": "导入",
|
||||||
@@ -50,6 +57,8 @@
|
|||||||
"Filter conditions": "过滤条件",
|
"Filter conditions": "过滤条件",
|
||||||
"Refresh profiles": "刷新订阅",
|
"Refresh profiles": "刷新订阅",
|
||||||
"Rules": "规则",
|
"Rules": "规则",
|
||||||
|
"Update All": "更新全部",
|
||||||
|
"Update At": "更新于",
|
||||||
|
|
||||||
"Type": "类型",
|
"Type": "类型",
|
||||||
"Name": "名称",
|
"Name": "名称",
|
||||||
@@ -66,7 +75,7 @@
|
|||||||
"Allow Lan": "局域网连接",
|
"Allow Lan": "局域网连接",
|
||||||
"IPv6": "IPv6",
|
"IPv6": "IPv6",
|
||||||
"Log Level": "日志等级",
|
"Log Level": "日志等级",
|
||||||
"Mixed Port": "端口设置",
|
"Port Config": "端口设置",
|
||||||
"Random Port": "随机端口",
|
"Random Port": "随机端口",
|
||||||
"After restart to take effect": "重启后生效",
|
"After restart to take effect": "重启后生效",
|
||||||
"External": "外部控制",
|
"External": "外部控制",
|
||||||
@@ -89,9 +98,11 @@
|
|||||||
"Server Addr": "服务地址:",
|
"Server Addr": "服务地址:",
|
||||||
"Bypass": "当前绕过:",
|
"Bypass": "当前绕过:",
|
||||||
"Theme Mode": "主题模式",
|
"Theme Mode": "主题模式",
|
||||||
"Theme Blur": "背景模糊",
|
|
||||||
"Tray Click Event": "托盘点击事件",
|
"Tray Click Event": "托盘点击事件",
|
||||||
"Copy Env Type": "复制环境变量类型",
|
"Copy Env Type": "复制环境变量类型",
|
||||||
|
"Start Page": "启动页面",
|
||||||
|
"Startup Script": "启动脚本",
|
||||||
|
"Browse": "浏览",
|
||||||
"Show Main Window": "显示主窗口",
|
"Show Main Window": "显示主窗口",
|
||||||
"Theme Setting": "主题设置",
|
"Theme Setting": "主题设置",
|
||||||
"Layout Setting": "界面设置",
|
"Layout Setting": "界面设置",
|
||||||
@@ -117,6 +128,7 @@
|
|||||||
"Back": "返回",
|
"Back": "返回",
|
||||||
"Save": "保存",
|
"Save": "保存",
|
||||||
"Cancel": "取消",
|
"Cancel": "取消",
|
||||||
|
"Exit": "退出",
|
||||||
|
|
||||||
"Default": "默认",
|
"Default": "默认",
|
||||||
"Download Speed": "下载速度",
|
"Download Speed": "下载速度",
|
||||||
@@ -140,10 +152,15 @@
|
|||||||
"Enable Builtin Enhanced": "开启内建增强功能",
|
"Enable Builtin Enhanced": "开启内建增强功能",
|
||||||
"Proxy Layout Column": "代理页布局列数",
|
"Proxy Layout Column": "代理页布局列数",
|
||||||
"Default Latency Test": "默认测试链接",
|
"Default Latency Test": "默认测试链接",
|
||||||
|
"Default Latency Timeout": "测试超时时间",
|
||||||
|
|
||||||
"Auto Log Clean": "自动清理日志",
|
"Auto Log Clean": "自动清理日志",
|
||||||
"Never Clean": "不清理",
|
"Never Clean": "不清理",
|
||||||
"Retain 7 Days": "保留7天",
|
"Retain 7 Days": "保留7天",
|
||||||
"Retain 30 Days": "保留30天",
|
"Retain 30 Days": "保留30天",
|
||||||
"Retain 90 Days": "保留90天"
|
"Retain 90 Days": "保留90天",
|
||||||
|
|
||||||
|
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
|
||||||
|
"Tun Mode Info Windows": "Tun模式需要授予内核相关权限,使用前请先开启服务模式",
|
||||||
|
"Tun Mode Info Unix": "Tun模式需要授予内核相关权限,使用前请先在内核设置中给内核授权"
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-12
@@ -22,6 +22,9 @@ import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
|||||||
import getSystem from "@/utils/get-system";
|
import getSystem from "@/utils/get-system";
|
||||||
import "dayjs/locale/ru";
|
import "dayjs/locale/ru";
|
||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
|
import { getPortableFlag } from "@/services/cmds";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
export let portableFlag = false;
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
@@ -33,8 +36,8 @@ const Layout = () => {
|
|||||||
const { theme } = useCustomTheme();
|
const { theme } = useCustomTheme();
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { theme_blur, language } = verge || {};
|
const { language, start_page } = verge || {};
|
||||||
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,7 +54,7 @@ const Layout = () => {
|
|||||||
mutate("getProxies");
|
mutate("getProxies");
|
||||||
mutate("getVersion");
|
mutate("getVersion");
|
||||||
mutate("getClashConfig");
|
mutate("getClashConfig");
|
||||||
mutate("getProviders");
|
mutate("getProxyProviders");
|
||||||
});
|
});
|
||||||
|
|
||||||
// update the verge config
|
// update the verge config
|
||||||
@@ -71,10 +74,12 @@ const Layout = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
|
||||||
void appWindow.unminimize();
|
setTimeout(async () => {
|
||||||
void appWindow.show();
|
portableFlag = await getPortableFlag();
|
||||||
void appWindow.setFocus();
|
await appWindow.unminimize();
|
||||||
|
await appWindow.show();
|
||||||
|
await appWindow.setFocus();
|
||||||
}, 50);
|
}, 50);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -83,7 +88,10 @@ const Layout = () => {
|
|||||||
dayjs.locale(language === "zh" ? "zh-cn" : language);
|
dayjs.locale(language === "zh" ? "zh-cn" : language);
|
||||||
i18next.changeLanguage(language);
|
i18next.changeLanguage(language);
|
||||||
}
|
}
|
||||||
}, [language]);
|
if (start_page) {
|
||||||
|
navigate(start_page);
|
||||||
|
}
|
||||||
|
}, [language, start_page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SWRConfig value={{ errorRetryCount: 3 }}>
|
<SWRConfig value={{ errorRetryCount: 3 }}>
|
||||||
@@ -111,7 +119,7 @@ const Layout = () => {
|
|||||||
}}
|
}}
|
||||||
sx={[
|
sx={[
|
||||||
({ palette }) => ({
|
({ palette }) => ({
|
||||||
bgcolor: alpha(palette.background.paper, theme_blur ? 0.8 : 1),
|
bgcolor: palette.background.paper,
|
||||||
}),
|
}),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -119,9 +127,7 @@ const Layout = () => {
|
|||||||
<div className="the-logo" data-windrag>
|
<div className="the-logo" data-windrag>
|
||||||
<LogoSvg />
|
<LogoSvg />
|
||||||
|
|
||||||
{!(OS === "windows" && WIN_PORTABLE) && (
|
{!portableFlag && <UpdateButton className="the-newbtn" />}
|
||||||
<UpdateButton className="the-newbtn" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<List className="the-menu">
|
<List className="the-menu">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import LogsPage from "./logs";
|
import LogsPage from "./logs";
|
||||||
import ProxiesPage from "./proxies";
|
import ProxiesPage from "./proxies";
|
||||||
|
import TestPage from "./test";
|
||||||
import ProfilesPage from "./profiles";
|
import ProfilesPage from "./profiles";
|
||||||
import SettingsPage from "./settings";
|
import SettingsPage from "./settings";
|
||||||
import ConnectionsPage from "./connections";
|
import ConnectionsPage from "./connections";
|
||||||
@@ -31,6 +32,11 @@ export const routers = [
|
|||||||
link: "/logs",
|
link: "/logs",
|
||||||
ele: LogsPage,
|
ele: LogsPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Label-Test",
|
||||||
|
link: "/test",
|
||||||
|
ele: TestPage,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Label-Settings",
|
label: "Label-Settings",
|
||||||
link: "/settings",
|
link: "/settings",
|
||||||
|
|||||||
+77
-69
@@ -24,6 +24,7 @@ import {
|
|||||||
ConnectionDetail,
|
ConnectionDetail,
|
||||||
ConnectionDetailRef,
|
ConnectionDetailRef,
|
||||||
} from "@/components/connection/connection-detail";
|
} from "@/components/connection/connection-detail";
|
||||||
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
|
||||||
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
||||||
|
|
||||||
@@ -48,14 +49,20 @@ const ConnectionsPage = () => {
|
|||||||
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterConn = useMemo(() => {
|
const [filterConn, download, upload] = useMemo(() => {
|
||||||
const orderFunc = orderOpts[curOrderOpt];
|
const orderFunc = orderOpts[curOrderOpt];
|
||||||
const connections = connData.connections.filter((conn) =>
|
let connections = connData.connections.filter((conn) =>
|
||||||
(conn.metadata.host || conn.metadata.destinationIP)?.includes(filterText)
|
(conn.metadata.host || conn.metadata.destinationIP)?.includes(filterText)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (orderFunc) return orderFunc(connections);
|
if (orderFunc) connections = orderFunc(connections);
|
||||||
return connections;
|
let download = 0;
|
||||||
|
let upload = 0;
|
||||||
|
connections.forEach((x) => {
|
||||||
|
download += x.download;
|
||||||
|
upload += x.upload;
|
||||||
|
});
|
||||||
|
return [connections, download, upload];
|
||||||
}, [connData, filterText, curOrderOpt]);
|
}, [connData, filterText, curOrderOpt]);
|
||||||
|
|
||||||
const { connect, disconnect } = useWebsocket(
|
const { connect, disconnect } = useWebsocket(
|
||||||
@@ -114,10 +121,13 @@ const ConnectionsPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
|
full
|
||||||
title={t("Connections")}
|
title={t("Connections")}
|
||||||
contentStyle={{ height: "100%" }}
|
contentStyle={{ height: "100%" }}
|
||||||
header={
|
header={
|
||||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 2 }}>
|
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
|
<Box sx={{ mx: 1 }}>Download: {parseTraffic(download)}</Box>
|
||||||
|
<Box sx={{ mx: 1 }}>Upload: {parseTraffic(upload)}</Box>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -142,75 +152,73 @@ const ConnectionsPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box sx={{ boxShadow: 0, height: "100%" }}>
|
<Box
|
||||||
<Box
|
sx={{
|
||||||
sx={{
|
pt: 1,
|
||||||
pt: 1,
|
mb: 0.5,
|
||||||
mb: 0.5,
|
mx: "10px",
|
||||||
mx: "12px",
|
height: "36px",
|
||||||
height: "36px",
|
display: "flex",
|
||||||
display: "flex",
|
alignItems: "center",
|
||||||
alignItems: "center",
|
userSelect: "text",
|
||||||
userSelect: "text",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{!isTableLayout && (
|
||||||
{!isTableLayout && (
|
<Select
|
||||||
<Select
|
|
||||||
size="small"
|
|
||||||
autoComplete="off"
|
|
||||||
value={curOrderOpt}
|
|
||||||
onChange={(e) => setOrderOpt(e.target.value)}
|
|
||||||
sx={{
|
|
||||||
mr: 1,
|
|
||||||
width: i18n.language === "en" ? 190 : 120,
|
|
||||||
'[role="button"]': { py: 0.65 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.keys(orderOpts).map((opt) => (
|
|
||||||
<MenuItem key={opt} value={opt}>
|
|
||||||
<span style={{ fontSize: 14 }}>{t(opt)}</span>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
hiddenLabel
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
size="small"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
spellCheck="false"
|
value={curOrderOpt}
|
||||||
variant="outlined"
|
onChange={(e) => setOrderOpt(e.target.value)}
|
||||||
placeholder={t("Filter conditions")}
|
sx={{
|
||||||
value={filterText}
|
mr: 1,
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
width: i18n.language === "en" ? 190 : 120,
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
height: 33.375,
|
||||||
/>
|
'[role="button"]': { py: 0.65 },
|
||||||
</Box>
|
}}
|
||||||
|
>
|
||||||
|
{Object.keys(orderOpts).map((opt) => (
|
||||||
|
<MenuItem key={opt} value={opt}>
|
||||||
|
<span style={{ fontSize: 14 }}>{t(opt)}</span>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box height="calc(100% - 50px)" sx={{ userSelect: "text" }}>
|
<TextField
|
||||||
{filterConn.length === 0 ? (
|
hiddenLabel
|
||||||
<BaseEmpty text="No Connections" />
|
fullWidth
|
||||||
) : isTableLayout ? (
|
size="small"
|
||||||
<ConnectionTable
|
autoComplete="off"
|
||||||
connections={filterConn}
|
spellCheck="false"
|
||||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
variant="outlined"
|
||||||
/>
|
placeholder={t("Filter conditions")}
|
||||||
) : (
|
value={filterText}
|
||||||
<Virtuoso
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
data={filterConn}
|
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||||
itemContent={(index, item) => (
|
/>
|
||||||
<ConnectionItem
|
|
||||||
value={item}
|
|
||||||
onShowDetail={() => detailRef.current?.open(item)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<ConnectionDetail ref={detailRef} />
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box height="calc(100% - 50px)" sx={{ userSelect: "text" }}>
|
||||||
|
{filterConn.length === 0 ? (
|
||||||
|
<BaseEmpty text="No Connections" />
|
||||||
|
) : isTableLayout ? (
|
||||||
|
<ConnectionTable
|
||||||
|
connections={filterConn}
|
||||||
|
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Virtuoso
|
||||||
|
data={filterConn}
|
||||||
|
itemContent={(index, item) => (
|
||||||
|
<ConnectionItem
|
||||||
|
value={item}
|
||||||
|
onShowDetail={() => detailRef.current?.open(item)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<ConnectionDetail ref={detailRef} />
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+45
-48
@@ -38,6 +38,7 @@ const LogPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
|
full
|
||||||
title={t("Logs")}
|
title={t("Logs")}
|
||||||
contentStyle={{ height: "100%" }}
|
contentStyle={{ height: "100%" }}
|
||||||
header={
|
header={
|
||||||
@@ -66,61 +67,57 @@ const LogPage = () => {
|
|||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
boxSizing: "border-box",
|
pt: 1,
|
||||||
boxShadow: 0,
|
mb: 0.5,
|
||||||
height: "100%",
|
mx: "10px",
|
||||||
userSelect: "text",
|
height: "36px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Select
|
||||||
|
size="small"
|
||||||
|
autoComplete="off"
|
||||||
|
value={logState}
|
||||||
|
onChange={(e) => setLogState(e.target.value)}
|
||||||
sx={{
|
sx={{
|
||||||
pt: 1,
|
width: 120,
|
||||||
mb: 0.5,
|
height: 33.375,
|
||||||
mx: "12px",
|
mr: 1,
|
||||||
height: "36px",
|
'[role="button"]': { py: 0.65 },
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Select
|
<MenuItem value="all">ALL</MenuItem>
|
||||||
size="small"
|
<MenuItem value="inf">INFO</MenuItem>
|
||||||
autoComplete="off"
|
<MenuItem value="warn">WARN</MenuItem>
|
||||||
value={logState}
|
<MenuItem value="err">ERROR</MenuItem>
|
||||||
onChange={(e) => setLogState(e.target.value)}
|
</Select>
|
||||||
sx={{ width: 120, mr: 1, '[role="button"]': { py: 0.65 } }}
|
|
||||||
>
|
|
||||||
<MenuItem value="all">ALL</MenuItem>
|
|
||||||
<MenuItem value="inf">INFO</MenuItem>
|
|
||||||
<MenuItem value="warn">WARN</MenuItem>
|
|
||||||
<MenuItem value="err">ERROR</MenuItem>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
hiddenLabel
|
hiddenLabel
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder={t("Filter conditions")}
|
placeholder={t("Filter conditions")}
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box height="calc(100% - 50px)">
|
||||||
|
{filterLogs.length > 0 ? (
|
||||||
|
<Virtuoso
|
||||||
|
initialTopMostItemIndex={999}
|
||||||
|
data={filterLogs}
|
||||||
|
itemContent={(index, item) => <LogItem value={item} />}
|
||||||
|
followOutput={"smooth"}
|
||||||
/>
|
/>
|
||||||
</Box>
|
) : (
|
||||||
|
<BaseEmpty text="No Logs" />
|
||||||
<Box height="calc(100% - 50px)">
|
)}
|
||||||
{filterLogs.length > 0 ? (
|
|
||||||
<Virtuoso
|
|
||||||
initialTopMostItemIndex={999}
|
|
||||||
data={filterLogs}
|
|
||||||
itemContent={(index, item) => <LogItem value={item} />}
|
|
||||||
followOutput={"smooth"}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<BaseEmpty text="No Logs" />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-10
@@ -51,6 +51,7 @@ const ProxyPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
|
full
|
||||||
contentStyle={{ height: "100%" }}
|
contentStyle={{ height: "100%" }}
|
||||||
title={t("Proxy Groups")}
|
title={t("Proxy Groups")}
|
||||||
header={
|
header={
|
||||||
@@ -72,16 +73,7 @@ const ProxyPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box
|
<ProxyGroups mode={curMode!} />
|
||||||
sx={{
|
|
||||||
borderRadius: 1,
|
|
||||||
boxShadow: 0,
|
|
||||||
height: "100%",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProxyGroups mode={curMode!} />
|
|
||||||
</Box>
|
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+46
-38
@@ -2,10 +2,11 @@ import useSWR from "swr";
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { Box, Paper, TextField } from "@mui/material";
|
import { Box, TextField } from "@mui/material";
|
||||||
import { getRules } from "@/services/api";
|
import { getRules } from "@/services/api";
|
||||||
import { BaseEmpty, BasePage } from "@/components/base";
|
import { BaseEmpty, BasePage } from "@/components/base";
|
||||||
import RuleItem from "@/components/rule/rule-item";
|
import RuleItem from "@/components/rule/rule-item";
|
||||||
|
import { ProviderButton } from "@/components/rule/provider-button";
|
||||||
|
|
||||||
const RulesPage = () => {
|
const RulesPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -18,45 +19,52 @@ const RulesPage = () => {
|
|||||||
}, [data, filterText]);
|
}, [data, filterText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage title={t("Rules")} contentStyle={{ height: "100%" }}>
|
<BasePage
|
||||||
<Box sx={{ boxSizing: "border-box", boxShadow: 0, height: "100%" }}>
|
full
|
||||||
<Box
|
title={t("Rules")}
|
||||||
sx={{
|
contentStyle={{ height: "100%" }}
|
||||||
pt: 1,
|
header={
|
||||||
mb: 0.5,
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
mx: "12px",
|
<ProviderButton />
|
||||||
height: "36px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
hiddenLabel
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
autoComplete="off"
|
|
||||||
variant="outlined"
|
|
||||||
spellCheck="false"
|
|
||||||
placeholder={t("Filter conditions")}
|
|
||||||
value={filterText}
|
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
pt: 1,
|
||||||
|
mb: 0.5,
|
||||||
|
mx: "10px",
|
||||||
|
height: "36px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
hiddenLabel
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
autoComplete="off"
|
||||||
|
variant="outlined"
|
||||||
|
spellCheck="false"
|
||||||
|
placeholder={t("Filter conditions")}
|
||||||
|
value={filterText}
|
||||||
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
|
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box height="calc(100% - 50px)">
|
<Box height="calc(100% - 50px)">
|
||||||
{rules.length > 0 ? (
|
{rules.length > 0 ? (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
data={rules}
|
data={rules}
|
||||||
itemContent={(index, item) => (
|
itemContent={(index, item) => (
|
||||||
<RuleItem index={index + 1} value={item} />
|
<RuleItem index={index + 1} value={item} />
|
||||||
)}
|
)}
|
||||||
followOutput={"smooth"}
|
followOutput={"smooth"}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<BaseEmpty text="No Rules" />
|
<BaseEmpty text="No Rules" />
|
||||||
)}
|
)}
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { Box, Button, Grid } from "@mui/material";
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
closestCenter,
|
||||||
|
KeyboardSensor,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
DragEndEvent,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { BasePage } from "@/components/base";
|
||||||
|
import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
|
||||||
|
import { TestItem } from "@/components/test/test-item";
|
||||||
|
import { emit } from "@tauri-apps/api/event";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
const TestPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||||
|
|
||||||
|
// test list
|
||||||
|
const testList = verge?.test_list ?? [
|
||||||
|
{
|
||||||
|
uid: nanoid(),
|
||||||
|
name: "Apple",
|
||||||
|
url: "https://www.apple.com",
|
||||||
|
icon: "https://www.apple.com/favicon.ico",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: nanoid(),
|
||||||
|
name: "GitHub",
|
||||||
|
url: "https://www.github.com",
|
||||||
|
icon: `<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#000000"/></svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: nanoid(),
|
||||||
|
name: "Google",
|
||||||
|
url: "https://www.google.com",
|
||||||
|
icon: `<svg enable-background="new 0 0 48 48" height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><path d="m43.611 20.083h-1.611v-.083h-18v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657c-3.572-3.329-8.35-5.382-13.618-5.382-11.045 0-20 8.955-20 20s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z" fill="#ffc107"/><path d="m6.306 14.691 6.571 4.819c1.778-4.402 6.084-7.51 11.123-7.51 3.059 0 5.842 1.154 7.961 3.039l5.657-5.657c-3.572-3.329-8.35-5.382-13.618-5.382-7.682 0-14.344 4.337-17.694 10.691z" fill="#ff3d00"/><path d="m24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238c-2.008 1.521-4.504 2.43-7.219 2.43-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025c3.31 6.477 10.032 10.921 17.805 10.921z" fill="#4caf50"/><path d="m43.611 20.083h-1.611v-.083h-18v8h11.303c-.792 2.237-2.231 4.166-4.087 5.571.001-.001.002-.001.003-.002l6.19 5.238c-.438.398 6.591-4.807 6.591-14.807 0-1.341-.138-2.65-.389-3.917z" fill="#1976d2"/></svg>`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const onTestListItemChange = (
|
||||||
|
uid: string,
|
||||||
|
patch?: Partial<IVergeTestItem>
|
||||||
|
) => {
|
||||||
|
if (patch) {
|
||||||
|
const newList = testList.map((x) => {
|
||||||
|
if (x.uid === uid) {
|
||||||
|
return { ...x, ...patch };
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
mutateVerge({ ...verge, test_list: newList }, false);
|
||||||
|
} else {
|
||||||
|
mutateVerge();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteTestListItem = (uid: string) => {
|
||||||
|
const newList = testList.filter((x) => x.uid !== uid);
|
||||||
|
patchVerge({ test_list: newList });
|
||||||
|
mutateVerge({ ...verge, test_list: newList }, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reorder = (list: any[], startIndex: number, endIndex: number) => {
|
||||||
|
const result = Array.from(list);
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = async (event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (over) {
|
||||||
|
if (active.id !== over.id) {
|
||||||
|
let old_index = testList.findIndex((x) => x.uid === active.id);
|
||||||
|
let new_index = testList.findIndex((x) => x.uid === over.id);
|
||||||
|
if (old_index < 0 || new_index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newList = reorder(testList, old_index, new_index);
|
||||||
|
await mutateVerge({ ...verge, test_list: newList }, false);
|
||||||
|
await patchVerge({ test_list: newList });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!verge) return;
|
||||||
|
if (!verge?.test_list) {
|
||||||
|
patchVerge({ test_list: testList });
|
||||||
|
}
|
||||||
|
}, [verge]);
|
||||||
|
|
||||||
|
const viewerRef = useRef<TestViewerRef>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BasePage
|
||||||
|
title={t("Test")}
|
||||||
|
header={
|
||||||
|
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={() => emit("verge://test-all")}
|
||||||
|
>
|
||||||
|
{t("Test All")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={() => viewerRef.current?.create()}
|
||||||
|
>
|
||||||
|
{t("New")}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
>
|
||||||
|
<Box sx={{ mb: 4.5 }}>
|
||||||
|
<Grid container spacing={{ xs: 1, lg: 1 }}>
|
||||||
|
<SortableContext
|
||||||
|
items={testList.map((x) => {
|
||||||
|
return x.uid;
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{testList.map((item) => (
|
||||||
|
<Grid item xs={6} sm={4} md={3} lg={2} key={item.uid}>
|
||||||
|
<TestItem
|
||||||
|
id={item.uid}
|
||||||
|
itemData={item}
|
||||||
|
onEdit={() => viewerRef.current?.edit(item)}
|
||||||
|
onDelete={onDeleteTestListItem}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</DndContext>
|
||||||
|
<TestViewer ref={viewerRef} onChange={onTestListItemChange} />
|
||||||
|
</BasePage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestPage;
|
||||||
+50
-16
@@ -105,9 +105,8 @@ export const getProxiesInner = async () => {
|
|||||||
export const getProxies = async () => {
|
export const getProxies = async () => {
|
||||||
const [proxyRecord, providerRecord] = await Promise.all([
|
const [proxyRecord, providerRecord] = await Promise.all([
|
||||||
getProxiesInner(),
|
getProxiesInner(),
|
||||||
getProviders(),
|
getProxyProviders(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// provider name map
|
// provider name map
|
||||||
const providerMap = Object.fromEntries(
|
const providerMap = Object.fromEntries(
|
||||||
Object.entries(providerRecord).flatMap(([provider, item]) =>
|
Object.entries(providerRecord).flatMap(([provider, item]) =>
|
||||||
@@ -119,29 +118,39 @@ export const getProxies = async () => {
|
|||||||
const generateItem = (name: string) => {
|
const generateItem = (name: string) => {
|
||||||
if (proxyRecord[name]) return proxyRecord[name];
|
if (proxyRecord[name]) return proxyRecord[name];
|
||||||
if (providerMap[name]) return providerMap[name];
|
if (providerMap[name]) return providerMap[name];
|
||||||
return { name, type: "unknown", udp: false, history: [] };
|
return {
|
||||||
|
name,
|
||||||
|
type: "unknown",
|
||||||
|
udp: false,
|
||||||
|
xudp: false,
|
||||||
|
tfo: false,
|
||||||
|
history: [],
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const { GLOBAL: global, DIRECT: direct, REJECT: reject } = proxyRecord;
|
const { GLOBAL: global, DIRECT: direct, REJECT: reject } = proxyRecord;
|
||||||
|
|
||||||
let groups: IProxyGroupItem[] = [];
|
let groups = Object.values(proxyRecord)
|
||||||
|
.filter((each) => each.name !== "GLOBAL" && each.all)
|
||||||
|
.map((each) => ({
|
||||||
|
...each,
|
||||||
|
all: each.all!.map((item) => generateItem(item)),
|
||||||
|
}));
|
||||||
|
|
||||||
if (global?.all) {
|
if (global?.all) {
|
||||||
groups = global.all
|
let globalGroups = global.all
|
||||||
.filter((name) => proxyRecord[name]?.all)
|
.filter((name) => proxyRecord[name]?.all)
|
||||||
.map((name) => proxyRecord[name])
|
.map((name) => proxyRecord[name])
|
||||||
.map((each) => ({
|
.map((each) => ({
|
||||||
...each,
|
...each,
|
||||||
all: each.all!.map((item) => generateItem(item)),
|
all: each.all!.map((item) => generateItem(item)),
|
||||||
}));
|
}));
|
||||||
} else {
|
let globalNames = globalGroups.map((each) => each.name);
|
||||||
groups = Object.values(proxyRecord)
|
groups = groups
|
||||||
.filter((each) => each.name !== "GLOBAL" && each.all)
|
.filter((group) => {
|
||||||
.map((each) => ({
|
return !globalNames.includes(group.name);
|
||||||
...each,
|
})
|
||||||
all: each.all!.map((item) => generateItem(item)),
|
.concat(globalGroups);
|
||||||
}))
|
|
||||||
.sort((a, b) => b.name.localeCompare(a.name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxies = [direct, reject].concat(
|
const proxies = [direct, reject].concat(
|
||||||
@@ -159,11 +168,31 @@ export const getProxies = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// get proxy providers
|
// get proxy providers
|
||||||
export const getProviders = async () => {
|
export const getProxyProviders = async () => {
|
||||||
const instance = await getAxios();
|
const instance = await getAxios();
|
||||||
const response = await instance.get<any, any>("/providers/proxies");
|
const response = await instance.get<any, any>("/providers/proxies");
|
||||||
|
|
||||||
const providers = (response.providers || {}) as Record<string, IProviderItem>;
|
const providers = (response.providers || {}) as Record<
|
||||||
|
string,
|
||||||
|
IProxyProviderItem
|
||||||
|
>;
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(providers).filter(([key, item]) => {
|
||||||
|
const type = item.vehicleType.toLowerCase();
|
||||||
|
return type === "http" || type === "file";
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRuleProviders = async () => {
|
||||||
|
const instance = await getAxios();
|
||||||
|
const response = await instance.get<any, any>("/providers/rules");
|
||||||
|
|
||||||
|
const providers = (response.providers || {}) as Record<
|
||||||
|
string,
|
||||||
|
IRuleProviderItem
|
||||||
|
>;
|
||||||
|
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(providers).filter(([key, item]) => {
|
Object.entries(providers).filter(([key, item]) => {
|
||||||
@@ -181,11 +210,16 @@ export const providerHealthCheck = async (name: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const providerUpdate = async (name: string) => {
|
export const proxyProviderUpdate = async (name: string) => {
|
||||||
const instance = await getAxios();
|
const instance = await getAxios();
|
||||||
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`);
|
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ruleProviderUpdate = async (name: string) => {
|
||||||
|
const instance = await getAxios();
|
||||||
|
return instance.put(`/providers/rules/${encodeURIComponent(name)}`);
|
||||||
|
};
|
||||||
|
|
||||||
export const getConnections = async () => {
|
export const getConnections = async () => {
|
||||||
const instance = await getAxios();
|
const instance = await getAxios();
|
||||||
const result = await instance.get("/connections");
|
const result = await instance.get("/connections");
|
||||||
|
|||||||
+22
-2
@@ -160,9 +160,21 @@ export async function openWebUrl(url: string) {
|
|||||||
return invoke<void>("open_web_url", { url });
|
return invoke<void>("open_web_url", { url });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cmdGetProxyDelay(name: string, url?: string) {
|
export async function cmdGetProxyDelay(
|
||||||
|
name: string,
|
||||||
|
timeout: number,
|
||||||
|
url?: string
|
||||||
|
) {
|
||||||
name = encodeURIComponent(name);
|
name = encodeURIComponent(name);
|
||||||
return invoke<{ delay: number }>("clash_api_get_proxy_delay", { name, url });
|
return invoke<{ delay: number }>("clash_api_get_proxy_delay", {
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
timeout,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdTestDelay(url: string) {
|
||||||
|
return invoke<number>("test_delay", { url });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// service mode
|
/// service mode
|
||||||
@@ -191,3 +203,11 @@ export async function invoke_uwp_tool() {
|
|||||||
Notice.error(err?.message || err.toString(), 1500)
|
Notice.error(err?.message || err.toString(), 1500)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPortableFlag() {
|
||||||
|
return invoke<boolean>("get_portable_flag");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exitApp() {
|
||||||
|
return invoke("exit_app");
|
||||||
|
}
|
||||||
|
|||||||
+15
-11
@@ -69,12 +69,12 @@ class DelayManager {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkDelay(name: string, group: string) {
|
async checkDelay(name: string, group: string, timeout: number) {
|
||||||
let delay = -1;
|
let delay = -1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = this.getUrl(group);
|
const url = this.getUrl(group);
|
||||||
const result = await cmdGetProxyDelay(name, url);
|
const result = await cmdGetProxyDelay(name, timeout, url);
|
||||||
delay = result.delay;
|
delay = result.delay;
|
||||||
} catch {
|
} catch {
|
||||||
delay = 1e6; // error
|
delay = 1e6; // error
|
||||||
@@ -84,7 +84,12 @@ class DelayManager {
|
|||||||
return delay;
|
return delay;
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkListDelay(nameList: string[], group: string, concurrency = 36) {
|
async checkListDelay(
|
||||||
|
nameList: string[],
|
||||||
|
group: string,
|
||||||
|
timeout: number,
|
||||||
|
concurrency = 36
|
||||||
|
) {
|
||||||
const names = nameList.filter(Boolean);
|
const names = nameList.filter(Boolean);
|
||||||
// 设置正在延迟测试中
|
// 设置正在延迟测试中
|
||||||
names.forEach((name) => this.setDelay(name, group, -2));
|
names.forEach((name) => this.setDelay(name, group, -2));
|
||||||
@@ -98,7 +103,7 @@ class DelayManager {
|
|||||||
const task = names.shift();
|
const task = names.shift();
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
current += 1;
|
current += 1;
|
||||||
await this.checkDelay(task, group);
|
await this.checkDelay(task, group, timeout);
|
||||||
current -= 1;
|
current -= 1;
|
||||||
total -= 1;
|
total -= 1;
|
||||||
if (total <= 0) resolve(null);
|
if (total <= 0) resolve(null);
|
||||||
@@ -108,18 +113,17 @@ class DelayManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDelay(delay: number) {
|
formatDelay(delay: number, timeout = 10000) {
|
||||||
if (delay < 0) return "-";
|
if (delay <= 0) return "Error";
|
||||||
if (delay > 1e5) return "Error";
|
if (delay > 1e5) return "Error";
|
||||||
if (delay >= 10000) return "Timeout"; // 10s
|
if (delay >= timeout) return "Timeout"; // 10s
|
||||||
return `${delay}`;
|
return `${delay}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDelayColor(delay: number) {
|
formatDelayColor(delay: number, timeout = 10000) {
|
||||||
if (delay >= 10000) return "error.main";
|
if (delay >= timeout) return "error.main";
|
||||||
/*if (delay <= 0) return "text.secondary";
|
if (delay <= 0) return "error.main";
|
||||||
if (delay > 500) return "warning.main";
|
if (delay > 500) return "warning.main";
|
||||||
if (delay > 100) return "text.secondary";*/
|
|
||||||
return "success.main";
|
return "success.main";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+46
-5
@@ -14,7 +14,6 @@ type Platform =
|
|||||||
/**
|
/**
|
||||||
* defines in `vite.config.ts`
|
* defines in `vite.config.ts`
|
||||||
*/
|
*/
|
||||||
declare const WIN_PORTABLE: boolean;
|
|
||||||
declare const OS_PLATFORM: Platform;
|
declare const OS_PLATFORM: Platform;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,6 +32,15 @@ interface IConfigData {
|
|||||||
"tproxy-port": number;
|
"tproxy-port": number;
|
||||||
"external-controller": string;
|
"external-controller": string;
|
||||||
secret: string;
|
secret: string;
|
||||||
|
tun: {
|
||||||
|
stack: string;
|
||||||
|
device: string;
|
||||||
|
"auto-route": boolean;
|
||||||
|
"auto-detect-interface": boolean;
|
||||||
|
"dns-hijack": string[];
|
||||||
|
"strict-route": boolean;
|
||||||
|
mtu: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRuleItem {
|
interface IRuleItem {
|
||||||
@@ -45,12 +53,16 @@ interface IProxyItem {
|
|||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
udp: boolean;
|
udp: boolean;
|
||||||
|
xudp: boolean;
|
||||||
|
tfo: boolean;
|
||||||
history: {
|
history: {
|
||||||
time: string;
|
time: string;
|
||||||
delay: number;
|
delay: number;
|
||||||
}[];
|
}[];
|
||||||
all?: string[];
|
all?: string[];
|
||||||
now?: string;
|
now?: string;
|
||||||
|
hidden?: boolean;
|
||||||
|
icon?: string;
|
||||||
provider?: string; // 记录是否来自provider
|
provider?: string; // 记录是否来自provider
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +70,28 @@ type IProxyGroupItem = Omit<IProxyItem, "all"> & {
|
|||||||
all: IProxyItem[];
|
all: IProxyItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IProviderItem {
|
interface IProxyProviderItem {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
proxies: IProxyItem[];
|
proxies: IProxyItem[];
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
vehicleType: string;
|
vehicleType: string;
|
||||||
|
subscriptionInfo?: {
|
||||||
|
Upload: number;
|
||||||
|
Download: number;
|
||||||
|
Total: number;
|
||||||
|
Expire: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRuleProviderItem {
|
||||||
|
name: string;
|
||||||
|
behavior: string;
|
||||||
|
format: string;
|
||||||
|
ruleCount: number;
|
||||||
|
type: string;
|
||||||
|
updatedAt: string;
|
||||||
|
vehicleType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITrafficItem {
|
interface ITrafficItem {
|
||||||
@@ -112,7 +140,9 @@ interface IConnections {
|
|||||||
|
|
||||||
interface IClashInfo {
|
interface IClashInfo {
|
||||||
// status: string;
|
// status: string;
|
||||||
port?: number; // clash mixed port
|
mixed_port?: number; // clash mixed port
|
||||||
|
socks_port?: number; // clash socks port
|
||||||
|
port?: number; // clash http port
|
||||||
server?: string; // external-controller
|
server?: string; // external-controller
|
||||||
secret?: string;
|
secret?: string;
|
||||||
}
|
}
|
||||||
@@ -152,14 +182,22 @@ interface IProfilesConfig {
|
|||||||
items?: IProfileItem[];
|
items?: IProfileItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IVergeTestItem {
|
||||||
|
uid: string;
|
||||||
|
name?: string;
|
||||||
|
icon?: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IVergeConfig {
|
interface IVergeConfig {
|
||||||
app_log_level?: "trace" | "debug" | "info" | "warn" | "error" | string;
|
app_log_level?: "trace" | "debug" | "info" | "warn" | "error" | string;
|
||||||
language?: string;
|
language?: string;
|
||||||
tray_event?: "main_window" | "system_proxy" | "tun_mode" | string;
|
tray_event?: "main_window" | "system_proxy" | "tun_mode" | string;
|
||||||
env_type?: "bash" | "cmd" | "powershell" | string;
|
env_type?: "bash" | "cmd" | "powershell" | string;
|
||||||
|
startup_script?: string;
|
||||||
|
start_page?: string;
|
||||||
clash_core?: string;
|
clash_core?: string;
|
||||||
theme_mode?: "light" | "dark" | "system";
|
theme_mode?: "light" | "dark" | "system";
|
||||||
theme_blur?: boolean;
|
|
||||||
traffic_graph?: boolean;
|
traffic_graph?: boolean;
|
||||||
enable_memory_usage?: boolean;
|
enable_memory_usage?: boolean;
|
||||||
enable_tun_mode?: boolean;
|
enable_tun_mode?: boolean;
|
||||||
@@ -169,6 +207,8 @@ interface IVergeConfig {
|
|||||||
enable_system_proxy?: boolean;
|
enable_system_proxy?: boolean;
|
||||||
enable_random_port?: boolean;
|
enable_random_port?: boolean;
|
||||||
verge_mixed_port?: number;
|
verge_mixed_port?: number;
|
||||||
|
verge_socks_port?: number;
|
||||||
|
verge_port?: number;
|
||||||
enable_proxy_guard?: boolean;
|
enable_proxy_guard?: boolean;
|
||||||
proxy_guard_duration?: number;
|
proxy_guard_duration?: number;
|
||||||
system_proxy_bypass?: string;
|
system_proxy_bypass?: string;
|
||||||
@@ -188,10 +228,11 @@ interface IVergeConfig {
|
|||||||
};
|
};
|
||||||
auto_close_connection?: boolean;
|
auto_close_connection?: boolean;
|
||||||
default_latency_test?: string;
|
default_latency_test?: string;
|
||||||
enable_clash_fields?: boolean;
|
default_latency_timeout?: number;
|
||||||
enable_builtin_enhanced?: boolean;
|
enable_builtin_enhanced?: boolean;
|
||||||
auto_log_clean?: 0 | 1 | 2 | 3;
|
auto_log_clean?: 0 | 1 | 2 | 3;
|
||||||
proxy_layout_column?: number;
|
proxy_layout_column?: number;
|
||||||
|
test_list?: IVergeTestItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type IClashConfigValue = any;
|
type IClashConfigValue = any;
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
export const HANDLE_FIELDS = [
|
|
||||||
"mode",
|
|
||||||
"port",
|
|
||||||
"socks-port",
|
|
||||||
"mixed-port",
|
|
||||||
"allow-lan",
|
|
||||||
"log-level",
|
|
||||||
"ipv6",
|
|
||||||
"secret",
|
|
||||||
"external-controller",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const DEFAULT_FIELDS = [
|
|
||||||
"proxies",
|
|
||||||
"proxy-groups",
|
|
||||||
"proxy-providers",
|
|
||||||
"rules",
|
|
||||||
"rule-providers",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const OTHERS_FIELDS = [
|
|
||||||
"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
|
|
||||||
] as const;
|
|
||||||
@@ -25,6 +25,5 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
OS_PLATFORM: `"${process.platform}"`,
|
OS_PLATFORM: `"${process.platform}"`,
|
||||||
WIN_PORTABLE: !!process.env.VITE_WIN_PORTABLE,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user