@ -0,0 +1,13 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Information**
|
||||||
|
- OS: [e.g. macOS]
|
||||||
|
- Clash Verge Version: [e.g. 1.3.4]
|
||||||
|
- Clash Core: [e.g. Clash or Clash Meta]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[Feature]"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
@ -0,0 +1,93 @@
|
|||||||
|
name: Alpha CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
debug:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, ubuntu-20.04, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
if: startsWith(github.repository, 'zzzgydi')
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Delete current release assets
|
||||||
|
if: startsWith(matrix.os, 'ubuntu-')
|
||||||
|
uses: mknejp/delete-release-assets@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: alpha
|
||||||
|
fail-if-no-assets: false
|
||||||
|
fail-if-no-release: false
|
||||||
|
assets: |
|
||||||
|
*.zip
|
||||||
|
*.gz
|
||||||
|
*.AppImage
|
||||||
|
*.deb
|
||||||
|
*.dmg
|
||||||
|
*.msi
|
||||||
|
*.sig
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
- name: Install Dependencies (ubuntu only)
|
||||||
|
if: startsWith(matrix.os, 'ubuntu-')
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||||
|
|
||||||
|
- name: Yarn install and check
|
||||||
|
run: |
|
||||||
|
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||||
|
yarn run check --force
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: alpha
|
||||||
|
releaseName: "Clash Verge Alpha"
|
||||||
|
releaseBody: "Alpha Version (include debug)"
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: true
|
||||||
|
includeDebug: ${{ github.event.inputs.debug }}
|
||||||
|
|
||||||
|
- name: Portable Bundle
|
||||||
|
if: startsWith(matrix.os, 'windows-')
|
||||||
|
run: |
|
||||||
|
yarn build
|
||||||
|
yarn run portable
|
||||||
|
env:
|
||||||
|
TAG_NAME: alpha
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
VITE_WIN_PORTABLE: 1
|
@ -0,0 +1,98 @@
|
|||||||
|
name: Release CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v**
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
if: startsWith(github.repository, 'zzzgydi')
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Install Dependencies (ubuntu only)
|
||||||
|
if: startsWith(matrix.os, 'ubuntu-')
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||||
|
|
||||||
|
- name: Yarn install and check
|
||||||
|
run: |
|
||||||
|
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||||
|
yarn run check
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
# enable cache even though failed
|
||||||
|
# continue-on-error: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: v__VERSION__
|
||||||
|
releaseName: "Clash Verge v__VERSION__"
|
||||||
|
releaseBody: "More new features are now supported."
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: true
|
||||||
|
|
||||||
|
- name: Portable Bundle
|
||||||
|
if: startsWith(matrix.os, 'windows-')
|
||||||
|
# rebuild with env settings
|
||||||
|
run: |
|
||||||
|
yarn build
|
||||||
|
yarn run portable
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
VITE_WIN_PORTABLE: 1
|
||||||
|
|
||||||
|
release-update:
|
||||||
|
needs: release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
startsWith(github.repository, 'zzzgydi') &&
|
||||||
|
startsWith(github.ref, 'refs/tags/v')
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Yarn install
|
||||||
|
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Release updater file
|
||||||
|
run: yarn run updater
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@ -0,0 +1,102 @@
|
|||||||
|
name: Compatible CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
# push:
|
||||||
|
# tags:
|
||||||
|
# - v**
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
targets:
|
||||||
|
- tag: macOS-10.15
|
||||||
|
os: macos-10.15
|
||||||
|
- tag: Ubuntu18
|
||||||
|
os: ubuntu-18.04
|
||||||
|
- tag: Ubuntu22
|
||||||
|
os: ubuntu-22.04
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.targets.os }}
|
||||||
|
if: startsWith(github.repository, 'zzzgydi')
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
# - name: Install Dependencies (ubuntu18 only)
|
||||||
|
# if: matrix.targets.os == 'ubuntu-18.04'
|
||||||
|
# run: |
|
||||||
|
# sudo apt-get update
|
||||||
|
# sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev libayatana-appindicator3-dev
|
||||||
|
|
||||||
|
- name: Install Dependencies (ubuntu22 only)
|
||||||
|
if: startsWith(matrix.targets.os, 'ubuntu-')
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Get yarn cache dir path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
|
- name: Yarn Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
id: yarn-cache
|
||||||
|
with:
|
||||||
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
|
- name: Yarn install and check
|
||||||
|
run: |
|
||||||
|
yarn install --network-timeout 1000000
|
||||||
|
yarn run check
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: ${{ matrix.targets.tag }}
|
||||||
|
releaseName: "Compatible For ${{ matrix.targets.tag }}"
|
||||||
|
releaseBody: "More new features are now supported."
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
# - name: Portable Bundle
|
||||||
|
# if: matrix.os == 'windows-latest'
|
||||||
|
# # rebuild with env settings
|
||||||
|
# run: |
|
||||||
|
# yarn build
|
||||||
|
# yarn run portable
|
||||||
|
# env:
|
||||||
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
# TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
# VITE_WIN_PORTABLE: 1
|
@ -0,0 +1,107 @@
|
|||||||
|
name: Meta CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v**
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
if: startsWith(github.repository, 'zzzgydi')
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: Delete current release assets
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
uses: mknejp/delete-release-assets@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: meta
|
||||||
|
fail-if-no-assets: false
|
||||||
|
fail-if-no-release: false
|
||||||
|
assets: |
|
||||||
|
*.zip
|
||||||
|
*.gz
|
||||||
|
*.AppImage
|
||||||
|
*.deb
|
||||||
|
*.dmg
|
||||||
|
*.msi
|
||||||
|
*.sig
|
||||||
|
|
||||||
|
- name: Install Dependencies (ubuntu only)
|
||||||
|
if: startsWith(matrix.os, 'ubuntu-')
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||||
|
|
||||||
|
- name: Get yarn cache dir path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
|
- name: Yarn Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
id: yarn-cache
|
||||||
|
with:
|
||||||
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
|
- name: Yarn install and check
|
||||||
|
run: |
|
||||||
|
yarn install --network-timeout 1000000
|
||||||
|
yarn run check
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: meta
|
||||||
|
releaseName: "Clash Verge Meta"
|
||||||
|
releaseBody: ""
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: true
|
||||||
|
args: -f default-meta
|
||||||
|
|
||||||
|
- name: Portable Bundle
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: |
|
||||||
|
yarn build -f default-meta
|
||||||
|
yarn run portable
|
||||||
|
env:
|
||||||
|
TAG_NAME: meta
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
VITE_WIN_PORTABLE: 1
|
@ -0,0 +1,76 @@
|
|||||||
|
name: Test CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
os:
|
||||||
|
description: "Runs on OS"
|
||||||
|
required: true
|
||||||
|
default: windows-latest
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- windows-latest
|
||||||
|
- ubuntu-latest
|
||||||
|
- macos-latest
|
||||||
|
- ubuntu-18.04
|
||||||
|
- ubuntu-20.04
|
||||||
|
- ubuntu-22.04
|
||||||
|
- macos-10.15
|
||||||
|
- macos-11
|
||||||
|
- macos-12
|
||||||
|
- windows-2019
|
||||||
|
- windows-2022
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ${{ github.event.inputs.os }}
|
||||||
|
if: startsWith(github.repository, 'zzzgydi')
|
||||||
|
steps:
|
||||||
|
- name: System Version
|
||||||
|
run: |
|
||||||
|
echo ${{ github.event.inputs.os }}
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Install Dependencies (ubuntu only)
|
||||||
|
if: startsWith(github.event.inputs.os, 'ubuntu-')
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Yarn install and check
|
||||||
|
run: |
|
||||||
|
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||||
|
yarn run check
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: alpha
|
||||||
|
releaseName: "Clash Verge Alpha"
|
||||||
|
releaseBody: "Alpha Version (include debug)"
|
||||||
|
releaseDraft: false
|
||||||
|
includeUpdaterJson: false
|
@ -0,0 +1,25 @@
|
|||||||
|
name: Updater CI
|
||||||
|
|
||||||
|
on: workflow_dispatch
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.repository, 'zzzgydi')
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Yarn install
|
||||||
|
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Release updater file
|
||||||
|
run: yarn run updater
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
update.json
|
||||||
|
scripts/_env.sh
|
||||||
|
.vscode
|
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
yarn pretty-quick --staged
|
@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
@ -0,0 +1,472 @@
|
|||||||
|
## v1.3.8
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash meta core
|
||||||
|
- add default valid keys
|
||||||
|
- adjust the delay display interval and color
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix connections page undefined exception
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.7
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
- profiles page add paste button
|
||||||
|
- subscriptions url textfield use multi lines
|
||||||
|
- set min window size
|
||||||
|
- add check for updates buttons
|
||||||
|
- add open dashboard to the hotkey list
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix profiles page undefined exception
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.6
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add russian translation
|
||||||
|
- support to show connection detail
|
||||||
|
- support clash meta memory usage display
|
||||||
|
- support proxy provider update ui
|
||||||
|
- update geo data file from meta repo
|
||||||
|
- adjust setting page
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- center the window when it is out of screen
|
||||||
|
- use `sudo` when `pkexec` not found (Linux)
|
||||||
|
- reconnect websocket when window focus
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- The current version of the Linux installation package is built by Ubuntu 20.04 (Github Action).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.5
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash core
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix blurry system tray icon (Windows)
|
||||||
|
- fix v1.3.4 wintun.dll not found (Windows)
|
||||||
|
- fix v1.3.4 clash core not found (macOS, Linux)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.4
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
- optimize traffic graph high CPU usage when window hidden
|
||||||
|
- use polkit to elevate permission (Linux)
|
||||||
|
- support app log level setting
|
||||||
|
- support copy environment variable
|
||||||
|
- overwrite resource file according to file modified
|
||||||
|
- save window size and position
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- remove fallback group select status
|
||||||
|
- enable context menu on editable element (Windows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.3
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
- show tray icon variants in different system proxy status (Windows)
|
||||||
|
- close all connections when mode changed
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- encode controller secret into uri
|
||||||
|
- error boundary for each page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix import url issue
|
||||||
|
- fix profile undefined issue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix open url issue
|
||||||
|
- fix appimage path panic
|
||||||
|
- fix grant root permission in macOS
|
||||||
|
- fix linux system proxy default bypass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta
|
||||||
|
- support opening dir on tray
|
||||||
|
- support updating all profiles with one click
|
||||||
|
- support granting root permission to clash core(Linux, macOS)
|
||||||
|
- support enable/disable clash fields filter, feel free to experience the latest features of Clash Meta
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- deb add openssl depend(Linux)
|
||||||
|
- fix the AppImage auto launch path(Linux)
|
||||||
|
- fix get the default network service(macOS)
|
||||||
|
- remove the esc key listener in macOS, cmd+w instead(macOS)
|
||||||
|
- fix infinite retry when websocket error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.2.3
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash
|
||||||
|
- adjust macOS window style
|
||||||
|
- profile supports UTF8 with BOM
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix selected proxy
|
||||||
|
- fix error log
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.2.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash meta
|
||||||
|
- recover clash core after panic
|
||||||
|
- use system window decorations(Linux)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- flush system proxy settings(Windows)
|
||||||
|
- fix parse log panic
|
||||||
|
- fix ui bug
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.2.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash version
|
||||||
|
- proxy groups support multi columns
|
||||||
|
- optimize ui
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix ui websocket connection
|
||||||
|
- adjust delay check concurrency
|
||||||
|
- avoid setting login item repeatedly(macOS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.2.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash meta version
|
||||||
|
- support to change external-controller
|
||||||
|
- support to change default latency test URL
|
||||||
|
- close all connections when proxy changed or profile changed
|
||||||
|
- check the config by using the core
|
||||||
|
- increase the robustness of the program
|
||||||
|
- optimize windows service mode (need to reinstall)
|
||||||
|
- optimize ui
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- invalid hotkey cause panic
|
||||||
|
- invalid theme setting cause panic
|
||||||
|
- fix some other glitches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.1.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- the system tray follows i18n
|
||||||
|
- change the proxy group ui of global mode
|
||||||
|
- support to update profile with the system proxy/clash proxy
|
||||||
|
- check the remote profile more strictly
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- use app version as default user agent
|
||||||
|
- the clash not exit in service mode
|
||||||
|
- reset the system proxy when quit the app
|
||||||
|
- fix some other glitches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.1.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- optimize clash config feedback
|
||||||
|
- hide macOS dock icon
|
||||||
|
- use clash meta compatible version (Linux)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix some other glitches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.1.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add rule page
|
||||||
|
- supports proxy providers delay check
|
||||||
|
- add proxy delay check loading status
|
||||||
|
- supports hotkey/shortcut management
|
||||||
|
- supports displaying connections data in table layout(refer to yacd)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- supports yaml merge key in clash config
|
||||||
|
- detect the network interface and set the system proxy(macOS)
|
||||||
|
- fix some other glitches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.6
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash.meta
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- only script profile display console
|
||||||
|
- automatic configuration update on demand at launch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.5
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- reimplement profile enhanced mode with quick-js
|
||||||
|
- optimize the runtime config generation process
|
||||||
|
- support web ui management
|
||||||
|
- support clash field management
|
||||||
|
- support viewing the runtime config
|
||||||
|
- adjust some pages style
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix silent start
|
||||||
|
- fix incorrectly reset system proxy on exit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.4
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash core and clash meta version
|
||||||
|
- support switch clash mode on system tray
|
||||||
|
- theme mode support follows system
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- config load error on first use
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.3
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- save some states such as URL test, filter, etc
|
||||||
|
- update clash core and clash-meta core
|
||||||
|
- new icon for macOS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- supports for switching clash core
|
||||||
|
- supports release UI processes
|
||||||
|
- supports script mode setting
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix service mode bug (Windows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- adjust default theme settings
|
||||||
|
- reduce gpu usage of traffic graph when hidden
|
||||||
|
- supports more remote profile response header setting
|
||||||
|
- check remote profile data format when imported
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- service mode install and start issue (Windows)
|
||||||
|
- fix launch panic (Some Windows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash core
|
||||||
|
- optimize traffic graph animation
|
||||||
|
- supports interval update profiles
|
||||||
|
- supports service mode (Windows)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- reset system proxy when exit from dock (macOS)
|
||||||
|
- adjust clash dns config process strategy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.0.29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- sort proxy node
|
||||||
|
- custom proxy test url
|
||||||
|
- logs page filter
|
||||||
|
- connections page filter
|
||||||
|
- default user agent for subscription
|
||||||
|
- system tray add tun mode toggle
|
||||||
|
- enable to change the config dir (Windows only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.0.28
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- enable to use clash config fields (UI)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- remove the character
|
||||||
|
- fix some icon color
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.0.27
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- supports custom theme color
|
||||||
|
- tun mode setting control the final config
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix transition flickers (macOS)
|
||||||
|
- reduce proxy page render
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.0.26
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- silent start
|
||||||
|
- profile editor
|
||||||
|
- profile enhance mode supports more fields
|
||||||
|
- optimize profile enhance mode strategy
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix csp restriction on macOS
|
||||||
|
- window controllers on Linux
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.0.25
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash core version
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- app updater error
|
||||||
|
- display window controllers on Linux
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
If you can't update the app properly, please consider downloading the latest version from github release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.0.24
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Connections page
|
||||||
|
- add wintun.dll (Windows)
|
||||||
|
- supports create local profile with selected file (Windows)
|
||||||
|
- system tray enable set system proxy
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- open dir error
|
||||||
|
- auto launch path (Windows)
|
||||||
|
- fix some clash config error
|
||||||
|
- reduce the impact of the enhanced mode
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.0.23
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- i18n supports
|
||||||
|
- Remote profile User Agent supports
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- clash config file case ignore
|
||||||
|
- clash `external-controller` only port
|
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 28 KiB |
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "clash-verge",
|
||||||
|
"version": "1.3.8",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tauri dev",
|
||||||
|
"dev:diff": "tauri dev -f verge-dev",
|
||||||
|
"build": "tauri build",
|
||||||
|
"tauri": "tauri",
|
||||||
|
"web:dev": "vite",
|
||||||
|
"web:build": "tsc && vite build",
|
||||||
|
"web:serve": "vite preview",
|
||||||
|
"aarch": "node scripts/aarch.mjs",
|
||||||
|
"check": "node scripts/check.mjs",
|
||||||
|
"updater": "node scripts/updater.mjs",
|
||||||
|
"publish": "node scripts/publish.mjs",
|
||||||
|
"portable": "node scripts/portable.mjs",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.10.5",
|
||||||
|
"@emotion/styled": "^11.10.5",
|
||||||
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
|
"@mui/icons-material": "^5.10.9",
|
||||||
|
"@mui/material": "^5.10.13",
|
||||||
|
"@mui/x-data-grid": "^5.17.11",
|
||||||
|
"@tauri-apps/api": "^1.3.0",
|
||||||
|
"ahooks": "^3.7.2",
|
||||||
|
"axios": "^1.1.3",
|
||||||
|
"dayjs": "1.11.5",
|
||||||
|
"i18next": "^22.0.4",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"monaco-editor": "^0.34.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-error-boundary": "^3.1.4",
|
||||||
|
"react-hook-form": "^7.39.5",
|
||||||
|
"react-i18next": "^12.0.0",
|
||||||
|
"react-router-dom": "^6.4.3",
|
||||||
|
"react-virtuoso": "^3.1.3",
|
||||||
|
"recoil": "^0.7.6",
|
||||||
|
"snarkdown": "^2.0.0",
|
||||||
|
"swr": "^1.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@actions/github": "^5.0.3",
|
||||||
|
"@tauri-apps/cli": "^1.3.1",
|
||||||
|
"@types/fs-extra": "^9.0.13",
|
||||||
|
"@types/js-cookie": "^3.0.2",
|
||||||
|
"@types/lodash": "^4.14.180",
|
||||||
|
"@types/lodash-es": "^4.17.7",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@vitejs/plugin-react": "^2.0.1",
|
||||||
|
"adm-zip": "^0.5.9",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"fs-extra": "^10.0.0",
|
||||||
|
"https-proxy-agent": "^5.0.1",
|
||||||
|
"husky": "^7.0.0",
|
||||||
|
"node-fetch": "^3.2.6",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"pretty-quick": "^3.1.3",
|
||||||
|
"sass": "^1.54.0",
|
||||||
|
"typescript": "^4.7.4",
|
||||||
|
"vite": "^3.2.5",
|
||||||
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
|
"vite-plugin-svgr": "^2.2.1"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Build and upload assets
|
||||||
|
* for macOS(aarch)
|
||||||
|
*/
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import path from "path";
|
||||||
|
import { exit } from "process";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
|
||||||
|
// to `meta` tag
|
||||||
|
const META = process.argv.includes("--meta");
|
||||||
|
// to `alpha` tag
|
||||||
|
const ALPHA = process.argv.includes("--alpha");
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
async function resolve() {
|
||||||
|
if (!process.env.GITHUB_TOKEN) {
|
||||||
|
throw new Error("GITHUB_TOKEN is required");
|
||||||
|
}
|
||||||
|
if (!process.env.GITHUB_REPOSITORY) {
|
||||||
|
throw new Error("GITHUB_REPOSITORY is required");
|
||||||
|
}
|
||||||
|
if (!process.env.TAURI_PRIVATE_KEY) {
|
||||||
|
throw new Error("TAURI_PRIVATE_KEY is required");
|
||||||
|
}
|
||||||
|
if (!process.env.TAURI_KEY_PASSWORD) {
|
||||||
|
throw new Error("TAURI_KEY_PASSWORD is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { version } = require("../package.json");
|
||||||
|
|
||||||
|
const tag = META ? "meta" : ALPHA ? "alpha" : `v${version}`;
|
||||||
|
const buildCmd = META ? `yarn build -f default-meta` : `yarn build`;
|
||||||
|
|
||||||
|
console.log(`[INFO]: Upload to tag "${tag}"`);
|
||||||
|
console.log(`[INFO]: Building app. "${buildCmd}"`);
|
||||||
|
|
||||||
|
execSync(buildCmd);
|
||||||
|
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const bundlePath = path.join(cwd, "src-tauri/target/release/bundle");
|
||||||
|
const join = (p) => path.join(bundlePath, p);
|
||||||
|
|
||||||
|
const appPathList = [
|
||||||
|
join("macos/Clash Verge.aarch64.app.tar.gz"),
|
||||||
|
join("macos/Clash Verge.aarch64.app.tar.gz.sig"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const appPath of appPathList) {
|
||||||
|
if (fs.pathExistsSync(appPath)) {
|
||||||
|
fs.removeSync(appPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(join("macos/Clash Verge.app.tar.gz"), appPathList[0]);
|
||||||
|
fs.copyFileSync(join("macos/Clash Verge.app.tar.gz.sig"), appPathList[1]);
|
||||||
|
|
||||||
|
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||||
|
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||||
|
|
||||||
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
tag,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!release.id) throw new Error("failed to find the release");
|
||||||
|
|
||||||
|
await uploadAssets(release.id, [
|
||||||
|
join(`dmg/Clash Verge_${version}_aarch64.dmg`),
|
||||||
|
...appPathList,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// From tauri-apps/tauri-action
|
||||||
|
// https://github.com/tauri-apps/tauri-action/blob/dev/packages/action/src/upload-release-assets.ts
|
||||||
|
async function uploadAssets(releaseId, assets) {
|
||||||
|
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||||
|
|
||||||
|
// Determine content-length for header to upload asset
|
||||||
|
const contentLength = (filePath) => fs.statSync(filePath).size;
|
||||||
|
|
||||||
|
for (const assetPath of assets) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/zip",
|
||||||
|
"content-length": contentLength(assetPath),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ext = path.extname(assetPath);
|
||||||
|
const filename = path.basename(assetPath).replace(ext, "");
|
||||||
|
const assetName = path.dirname(assetPath).includes(`target${path.sep}debug`)
|
||||||
|
? `${filename}-debug${ext}`
|
||||||
|
: `${filename}${ext}`;
|
||||||
|
|
||||||
|
console.log(`[INFO]: Uploading ${assetName}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
|
headers,
|
||||||
|
name: assetName,
|
||||||
|
data: fs.readFileSync(assetPath),
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
release_id: releaseId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "darwin" && process.arch === "arm64") {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
console.error("invalid");
|
||||||
|
exit(1);
|
||||||
|
}
|
@ -0,0 +1,332 @@
|
|||||||
|
import fs from "fs-extra";
|
||||||
|
import zlib from "zlib";
|
||||||
|
import path from "path";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import proxyAgent from "https-proxy-agent";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
|
||||||
|
const FORCE = process.argv.includes("--force");
|
||||||
|
|
||||||
|
const SIDECAR_HOST = execSync("rustc -vV")
|
||||||
|
.toString()
|
||||||
|
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||||
|
|
||||||
|
/* ======= clash ======= */
|
||||||
|
const CLASH_STORAGE_PREFIX = "https://release.dreamacro.workers.dev/";
|
||||||
|
const CLASH_URL_PREFIX =
|
||||||
|
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
||||||
|
const CLASH_LATEST_DATE = "latest";
|
||||||
|
|
||||||
|
const CLASH_MAP = {
|
||||||
|
"win32-x64": "clash-windows-amd64",
|
||||||
|
"darwin-x64": "clash-darwin-amd64",
|
||||||
|
"darwin-arm64": "clash-darwin-arm64",
|
||||||
|
"linux-x64": "clash-linux-amd64",
|
||||||
|
"linux-arm64": "clash-linux-arm64",
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ======= clash meta ======= */
|
||||||
|
const META_URL_PREFIX = `https://github.com/MetaCubeX/Clash.Meta/releases/download/`;
|
||||||
|
const META_VERSION = "v1.16.0";
|
||||||
|
|
||||||
|
const META_MAP = {
|
||||||
|
"win32-x64": "clash.meta-windows-amd64-compatible",
|
||||||
|
"darwin-x64": "clash.meta-darwin-amd64",
|
||||||
|
"darwin-arm64": "clash.meta-darwin-arm64",
|
||||||
|
"linux-x64": "clash.meta-linux-amd64-compatible",
|
||||||
|
"linux-arm64": "clash.meta-linux-arm64",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check available
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { platform, arch } = process;
|
||||||
|
if (!CLASH_MAP[`${platform}-${arch}`]) {
|
||||||
|
throw new Error(`clash unsupported platform "${platform}-${arch}"`);
|
||||||
|
}
|
||||||
|
if (!META_MAP[`${platform}-${arch}`]) {
|
||||||
|
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clash() {
|
||||||
|
const name = CLASH_MAP[`${platform}-${arch}`];
|
||||||
|
|
||||||
|
const isWin = platform === "win32";
|
||||||
|
const urlExt = isWin ? "zip" : "gz";
|
||||||
|
const downloadURL = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${urlExt}`;
|
||||||
|
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||||
|
const zipFile = `${name}.${urlExt}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "clash",
|
||||||
|
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||||
|
exeFile,
|
||||||
|
zipFile,
|
||||||
|
downloadURL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clashS3() {
|
||||||
|
const name = CLASH_MAP[`${platform}-${arch}`];
|
||||||
|
|
||||||
|
const isWin = platform === "win32";
|
||||||
|
const urlExt = isWin ? "zip" : "gz";
|
||||||
|
const downloadURL = `${CLASH_STORAGE_PREFIX}${CLASH_LATEST_DATE}/${name}-${CLASH_LATEST_DATE}.${urlExt}`;
|
||||||
|
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||||
|
const zipFile = `${name}.${urlExt}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "clash",
|
||||||
|
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||||
|
exeFile,
|
||||||
|
zipFile,
|
||||||
|
downloadURL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clashMeta() {
|
||||||
|
const name = META_MAP[`${platform}-${arch}`];
|
||||||
|
const isWin = platform === "win32";
|
||||||
|
const urlExt = isWin ? "zip" : "gz";
|
||||||
|
const downloadURL = `${META_URL_PREFIX}${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
|
||||||
|
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||||
|
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "clash-meta",
|
||||||
|
targetFile: `clash-meta-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||||
|
exeFile,
|
||||||
|
zipFile,
|
||||||
|
downloadURL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* download sidecar and rename
|
||||||
|
*/
|
||||||
|
async function resolveSidecar(binInfo) {
|
||||||
|
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
|
||||||
|
|
||||||
|
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
||||||
|
const sidecarPath = path.join(sidecarDir, targetFile);
|
||||||
|
|
||||||
|
await fs.mkdirp(sidecarDir);
|
||||||
|
if (!FORCE && (await fs.pathExists(sidecarPath))) return;
|
||||||
|
|
||||||
|
const tempDir = path.join(TEMP_DIR, name);
|
||||||
|
const tempZip = path.join(tempDir, zipFile);
|
||||||
|
const tempExe = path.join(tempDir, exeFile);
|
||||||
|
|
||||||
|
await fs.mkdirp(tempDir);
|
||||||
|
try {
|
||||||
|
if (!(await fs.pathExists(tempZip))) {
|
||||||
|
await downloadFile(downloadURL, tempZip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zipFile.endsWith(".zip")) {
|
||||||
|
const zip = new AdmZip(tempZip);
|
||||||
|
zip.getEntries().forEach((entry) => {
|
||||||
|
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
|
||||||
|
});
|
||||||
|
zip.extractAllTo(tempDir, true);
|
||||||
|
await fs.rename(tempExe, sidecarPath);
|
||||||
|
console.log(`[INFO]: "${name}" unzip finished`);
|
||||||
|
} else {
|
||||||
|
// gz
|
||||||
|
const readStream = fs.createReadStream(tempZip);
|
||||||
|
const writeStream = fs.createWriteStream(sidecarPath);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const onError = (error) => {
|
||||||
|
console.error(`[ERROR]: "${name}" gz failed:`, error.message);
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
readStream
|
||||||
|
.pipe(zlib.createGunzip().on("error", onError))
|
||||||
|
.pipe(writeStream)
|
||||||
|
.on("finish", () => {
|
||||||
|
console.log(`[INFO]: "${name}" gunzip finished`);
|
||||||
|
execSync(`chmod 755 ${sidecarPath}`);
|
||||||
|
console.log(`[INFO]: "${name}" chmod binary finished`);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.on("error", onError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 需要删除文件
|
||||||
|
await fs.remove(sidecarPath);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
// delete temp dir
|
||||||
|
await fs.remove(tempDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* prepare clash core
|
||||||
|
* if the core version is not updated in time, use S3 storage as a backup.
|
||||||
|
*/
|
||||||
|
async function resolveClash() {
|
||||||
|
try {
|
||||||
|
return await resolveSidecar(clash());
|
||||||
|
} catch {
|
||||||
|
console.log(`[WARN]: clash core needs to be updated`);
|
||||||
|
return await resolveSidecar(clashS3());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only Windows
|
||||||
|
* get the wintun.dll (not required)
|
||||||
|
*/
|
||||||
|
async function resolveWintun() {
|
||||||
|
const { platform } = process;
|
||||||
|
|
||||||
|
if (platform !== "win32") return;
|
||||||
|
|
||||||
|
const url = "https://www.wintun.net/builds/wintun-0.14.1.zip";
|
||||||
|
|
||||||
|
const tempDir = path.join(TEMP_DIR, "wintun");
|
||||||
|
const tempZip = path.join(tempDir, "wintun.zip");
|
||||||
|
|
||||||
|
const wintunPath = path.join(tempDir, "wintun/bin/amd64/wintun.dll");
|
||||||
|
const targetPath = path.join(cwd, "src-tauri/resources", "wintun.dll");
|
||||||
|
|
||||||
|
if (!FORCE && (await fs.pathExists(targetPath))) return;
|
||||||
|
|
||||||
|
await fs.mkdirp(tempDir);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(tempZip))) {
|
||||||
|
await downloadFile(url, tempZip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// unzip
|
||||||
|
const zip = new AdmZip(tempZip);
|
||||||
|
zip.extractAllTo(tempDir, true);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(wintunPath))) {
|
||||||
|
throw new Error(`path not found "${wintunPath}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.rename(wintunPath, targetPath);
|
||||||
|
await fs.remove(tempDir);
|
||||||
|
|
||||||
|
console.log(`[INFO]: resolve wintun.dll finished`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* download the file to the resources dir
|
||||||
|
*/
|
||||||
|
async function resolveResource(binInfo) {
|
||||||
|
const { file, downloadURL } = binInfo;
|
||||||
|
|
||||||
|
const resDir = path.join(cwd, "src-tauri/resources");
|
||||||
|
const targetPath = path.join(resDir, file);
|
||||||
|
|
||||||
|
if (!FORCE && (await fs.pathExists(targetPath))) return;
|
||||||
|
|
||||||
|
await fs.mkdirp(resDir);
|
||||||
|
await downloadFile(downloadURL, targetPath);
|
||||||
|
|
||||||
|
console.log(`[INFO]: ${file} finished`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* download file and save to `path`
|
||||||
|
*/
|
||||||
|
async function downloadFile(url, path) {
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
const httpProxy =
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy;
|
||||||
|
|
||||||
|
if (httpProxy) {
|
||||||
|
options.agent = proxyAgent(httpProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/octet-stream" },
|
||||||
|
});
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
await fs.writeFile(path, new Uint8Array(buffer));
|
||||||
|
|
||||||
|
console.log(`[INFO]: download finished "${url}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* main
|
||||||
|
*/
|
||||||
|
const SERVICE_URL =
|
||||||
|
"https://github.com/zzzgydi/clash-verge-service/releases/download/latest";
|
||||||
|
|
||||||
|
const resolveService = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "clash-verge-service.exe",
|
||||||
|
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`,
|
||||||
|
});
|
||||||
|
const resolveInstall = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "install-service.exe",
|
||||||
|
downloadURL: `${SERVICE_URL}/install-service.exe`,
|
||||||
|
});
|
||||||
|
const resolveUninstall = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "uninstall-service.exe",
|
||||||
|
downloadURL: `${SERVICE_URL}/uninstall-service.exe`,
|
||||||
|
});
|
||||||
|
const resolveMmdb = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "Country.mmdb",
|
||||||
|
downloadURL: `https://github.com/Dreamacro/maxmind-geoip/releases/download/20230812/Country.mmdb`,
|
||||||
|
});
|
||||||
|
const resolveGeosite = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "geosite.dat",
|
||||||
|
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`,
|
||||||
|
});
|
||||||
|
const resolveGeoIP = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "geoip.dat",
|
||||||
|
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
{ name: "clash", func: () => resolveSidecar(clashS3()), retry: 5 },
|
||||||
|
{ name: "clash-meta", func: () => resolveSidecar(clashMeta()), retry: 5 },
|
||||||
|
{ name: "wintun", func: resolveWintun, retry: 5, winOnly: true },
|
||||||
|
{ name: "service", func: resolveService, retry: 5, winOnly: true },
|
||||||
|
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
|
||||||
|
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
|
||||||
|
{ name: "mmdb", func: resolveMmdb, retry: 5 },
|
||||||
|
{ name: "geosite", func: resolveGeosite, retry: 5 },
|
||||||
|
{ name: "geoip", func: resolveGeoIP, retry: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function runTask() {
|
||||||
|
const task = tasks.shift();
|
||||||
|
if (!task) return;
|
||||||
|
if (task.winOnly && process.platform !== "win32") return runTask();
|
||||||
|
|
||||||
|
for (let i = 0; i < task.retry; i++) {
|
||||||
|
try {
|
||||||
|
await task.func();
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message);
|
||||||
|
if (i === task.retry - 1) throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
runTask();
|
||||||
|
runTask();
|
@ -0,0 +1,59 @@
|
|||||||
|
import fs from "fs-extra";
|
||||||
|
import path from "path";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
|
||||||
|
/// Script for ci
|
||||||
|
/// 打包绿色版/便携版 (only Windows)
|
||||||
|
async function resolvePortable() {
|
||||||
|
if (process.platform !== "win32") return;
|
||||||
|
|
||||||
|
const releaseDir = "./src-tauri/target/release";
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(releaseDir))) {
|
||||||
|
throw new Error("could not found the release dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
const zip = new AdmZip();
|
||||||
|
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "clash.exe"));
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
|
||||||
|
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const packageJson = require("../package.json");
|
||||||
|
const { version } = packageJson;
|
||||||
|
|
||||||
|
const zipFile = `Clash.Verge_${version}_x64_portable.zip`;
|
||||||
|
zip.writeZip(zipFile);
|
||||||
|
|
||||||
|
console.log("[INFO]: create portable zip successfully");
|
||||||
|
|
||||||
|
// push release assets
|
||||||
|
if (process.env.GITHUB_TOKEN === undefined) {
|
||||||
|
throw new Error("GITHUB_TOKEN is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||||
|
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||||
|
|
||||||
|
console.log("[INFO]: upload to ", process.env.TAG_NAME || `v${version}`);
|
||||||
|
|
||||||
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
tag: process.env.TAG_NAME || `v${version}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(release.name);
|
||||||
|
|
||||||
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
|
...options,
|
||||||
|
release_id: release.id,
|
||||||
|
name: zipFile,
|
||||||
|
data: zip.toBuffer(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePortable().catch(console.error);
|
@ -0,0 +1,53 @@
|
|||||||
|
import fs from "fs-extra";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
// publish
|
||||||
|
async function resolvePublish() {
|
||||||
|
const flag = process.argv[2] ?? "patch";
|
||||||
|
const packageJson = require("../package.json");
|
||||||
|
const tauriJson = require("../src-tauri/tauri.conf.json");
|
||||||
|
|
||||||
|
let [a, b, c] = packageJson.version.split(".").map(Number);
|
||||||
|
|
||||||
|
if (flag === "major") {
|
||||||
|
a += 1;
|
||||||
|
b = 0;
|
||||||
|
c = 0;
|
||||||
|
} else if (flag === "minor") {
|
||||||
|
b += 1;
|
||||||
|
c = 0;
|
||||||
|
} else if (flag === "patch") {
|
||||||
|
c += 1;
|
||||||
|
} else throw new Error(`invalid flag "${flag}"`);
|
||||||
|
|
||||||
|
const nextVersion = `${a}.${b}.${c}`;
|
||||||
|
packageJson.version = nextVersion;
|
||||||
|
tauriJson.package.version = nextVersion;
|
||||||
|
|
||||||
|
// 发布更新前先写更新日志
|
||||||
|
const nextTag = `v${nextVersion}`;
|
||||||
|
await resolveUpdateLog(nextTag);
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
"./package.json",
|
||||||
|
JSON.stringify(packageJson, undefined, 2)
|
||||||
|
);
|
||||||
|
await fs.writeFile(
|
||||||
|
"./src-tauri/tauri.conf.json",
|
||||||
|
JSON.stringify(tauriJson, undefined, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
execSync("git add ./package.json");
|
||||||
|
execSync("git add ./src-tauri/tauri.conf.json");
|
||||||
|
execSync(`git commit -m "v${nextVersion}"`);
|
||||||
|
execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
|
||||||
|
execSync(`git push`);
|
||||||
|
execSync(`git push origin v${nextVersion}`);
|
||||||
|
console.log(`Publish Successfully...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePublish();
|
@ -0,0 +1,44 @@
|
|||||||
|
import fs from "fs-extra";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const UPDATE_LOG = "UPDATELOG.md";
|
||||||
|
|
||||||
|
// parse the UPDATELOG.md
|
||||||
|
export async function resolveUpdateLog(tag) {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
const reTitle = /^## v[\d\.]+/;
|
||||||
|
const reEnd = /^---/;
|
||||||
|
|
||||||
|
const file = path.join(cwd, UPDATE_LOG);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(file))) {
|
||||||
|
throw new Error("could not found UPDATELOG.md");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fs.readFile(file).then((d) => d.toString("utf8"));
|
||||||
|
|
||||||
|
const map = {};
|
||||||
|
let p = "";
|
||||||
|
|
||||||
|
data.split("\n").forEach((line) => {
|
||||||
|
if (reTitle.test(line)) {
|
||||||
|
p = line.slice(3).trim();
|
||||||
|
if (!map[p]) {
|
||||||
|
map[p] = [];
|
||||||
|
} else {
|
||||||
|
throw new Error(`Tag ${p} dup`);
|
||||||
|
}
|
||||||
|
} else if (reEnd.test(line)) {
|
||||||
|
p = "";
|
||||||
|
} else if (p) {
|
||||||
|
map[p].push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!map[tag]) {
|
||||||
|
throw new Error(`could not found "${tag}" in UPDATELOG.md`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[tag].join("\n").trim();
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
import fetch from "node-fetch";
|
||||||
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||||
|
|
||||||
|
const UPDATE_TAG_NAME = "updater";
|
||||||
|
const UPDATE_JSON_FILE = "update.json";
|
||||||
|
const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
|
|
||||||
|
/// generate update.json
|
||||||
|
/// upload to update tag's release asset
|
||||||
|
async function resolveUpdater() {
|
||||||
|
if (process.env.GITHUB_TOKEN === undefined) {
|
||||||
|
throw new Error("GITHUB_TOKEN is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||||
|
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||||
|
|
||||||
|
const { data: tags } = await github.rest.repos.listTags({
|
||||||
|
...options,
|
||||||
|
per_page: 10,
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// get the latest publish tag
|
||||||
|
const tag = tags.find((t) => t.name.startsWith("v"));
|
||||||
|
|
||||||
|
console.log(tag);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
tag: tag.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
name: tag.name,
|
||||||
|
notes: await resolveUpdateLog(tag.name), // use updatelog.md
|
||||||
|
pub_date: new Date().toISOString(),
|
||||||
|
platforms: {
|
||||||
|
win64: { signature: "", url: "" }, // compatible with older formats
|
||||||
|
linux: { signature: "", url: "" }, // compatible with older formats
|
||||||
|
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||||
|
"darwin-aarch64": { signature: "", url: "" },
|
||||||
|
"darwin-intel": { signature: "", url: "" },
|
||||||
|
"darwin-x86_64": { signature: "", url: "" },
|
||||||
|
"linux-x86_64": { signature: "", url: "" },
|
||||||
|
"windows-x86_64": { signature: "", url: "" },
|
||||||
|
"windows-i686": { signature: "", url: "" }, // no supported
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const promises = latestRelease.assets.map(async (asset) => {
|
||||||
|
const { name, browser_download_url } = asset;
|
||||||
|
|
||||||
|
// win64 url
|
||||||
|
if (name.endsWith(".msi.zip") && name.includes("en-US")) {
|
||||||
|
updateData.platforms.win64.url = browser_download_url;
|
||||||
|
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win64 signature
|
||||||
|
if (name.endsWith(".msi.zip.sig") && name.includes("en-US")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms.win64.signature = sig;
|
||||||
|
updateData.platforms["windows-x86_64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// darwin url (intel)
|
||||||
|
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
||||||
|
updateData.platforms.darwin.url = browser_download_url;
|
||||||
|
updateData.platforms["darwin-intel"].url = browser_download_url;
|
||||||
|
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// darwin signature (intel)
|
||||||
|
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms.darwin.signature = sig;
|
||||||
|
updateData.platforms["darwin-intel"].signature = sig;
|
||||||
|
updateData.platforms["darwin-x86_64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// darwin url (aarch)
|
||||||
|
if (name.endsWith("aarch64.app.tar.gz")) {
|
||||||
|
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// darwin signature (aarch)
|
||||||
|
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["darwin-aarch64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// linux url
|
||||||
|
if (name.endsWith(".AppImage.tar.gz")) {
|
||||||
|
updateData.platforms.linux.url = browser_download_url;
|
||||||
|
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// linux signature
|
||||||
|
if (name.endsWith(".AppImage.tar.gz.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms.linux.signature = sig;
|
||||||
|
updateData.platforms["linux-x86_64"].signature = sig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
console.log(updateData);
|
||||||
|
|
||||||
|
// maybe should test the signature as well
|
||||||
|
// delete the null field
|
||||||
|
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||||
|
if (!value.url) {
|
||||||
|
console.log(`[Error]: failed to parse release for "${key}"`);
|
||||||
|
delete updateData.platforms[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成一个代理github的更新文件
|
||||||
|
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
|
||||||
|
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
||||||
|
|
||||||
|
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||||
|
if (value.url) {
|
||||||
|
updateDataNew.platforms[key].url = "https://ghproxy.com/" + value.url;
|
||||||
|
} else {
|
||||||
|
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// update the update.json
|
||||||
|
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
tag: UPDATE_TAG_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete the old assets
|
||||||
|
for (let asset of updateRelease.assets) {
|
||||||
|
if (asset.name === UPDATE_JSON_FILE) {
|
||||||
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
|
...options,
|
||||||
|
asset_id: asset.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.name === UPDATE_JSON_PROXY) {
|
||||||
|
await github.rest.repos
|
||||||
|
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||||
|
.catch(console.error); // do not break the pipeline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload new assets
|
||||||
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
|
...options,
|
||||||
|
release_id: updateRelease.id,
|
||||||
|
name: UPDATE_JSON_FILE,
|
||||||
|
data: JSON.stringify(updateData, null, 2),
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
|
...options,
|
||||||
|
release_id: updateRelease.id,
|
||||||
|
name: UPDATE_JSON_PROXY,
|
||||||
|
data: JSON.stringify(updateDataNew, null, 2),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the signature file content
|
||||||
|
async function getSignature(url) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/octet-stream" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveUpdater().catch(console.error);
|
@ -0,0 +1,6 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
WixTools
|
||||||
|
resources
|
||||||
|
sidecar
|
@ -0,0 +1,69 @@
|
|||||||
|
[package]
|
||||||
|
name = "clash-verge"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "clash verge"
|
||||||
|
authors = ["zzzgydi"]
|
||||||
|
license = "GPL-3.0"
|
||||||
|
repository = "https://github.com/zzzgydi/clash-verge.git"
|
||||||
|
default-run = "clash-verge"
|
||||||
|
edition = "2021"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
warp = "0.3"
|
||||||
|
which = "4.2.2"
|
||||||
|
anyhow = "1.0"
|
||||||
|
dirs = "5.0.0"
|
||||||
|
open = "4.0.1"
|
||||||
|
log = "0.4.14"
|
||||||
|
ctrlc = "3.2.3"
|
||||||
|
dunce = "1.0.2"
|
||||||
|
log4rs = "1.0.0"
|
||||||
|
nanoid = "0.4.0"
|
||||||
|
chrono = "0.4.19"
|
||||||
|
sysinfo = "0.29"
|
||||||
|
sysproxy = "0.3"
|
||||||
|
rquickjs = "0.1.7"
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
auto-launch = "0.5"
|
||||||
|
once_cell = "1.14.0"
|
||||||
|
port_scanner = "0.1.5"
|
||||||
|
delay_timer = "0.11.1"
|
||||||
|
parking_lot = "0.12.0"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
reqwest = { version = "0.11", features = ["json","rustls-tls"] }
|
||||||
|
tauri = { version = "1.2.4", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||||
|
window-vibrancy = { version = "0.3.0" }
|
||||||
|
window-shadows = { version = "0.2.0" }
|
||||||
|
wry = { version = "0.24.3" }
|
||||||
|
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
runas = "1.1.0"
|
||||||
|
deelevate = "0.2.0"
|
||||||
|
winreg = { version = "0.50", features = ["transactions"] }
|
||||||
|
windows-sys = { version = "0.48", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies.tauri]
|
||||||
|
features = ["global-shortcut-all", "icon-png", "process-all", "shell-all", "system-tray", "updater", "window-all"]
|
||||||
|
|
||||||
|
[target.'cfg(linux)'.dependencies.tauri]
|
||||||
|
features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "native-tls-vendored", "reqwest-native-tls-vendored"]
|
||||||
|
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["custom-protocol"]
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
verge-dev = []
|
||||||
|
default-meta = []
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
panic = "abort"
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
opt-level = "s"
|
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,14 @@
|
|||||||
|
max_width = 100
|
||||||
|
hard_tabs = false
|
||||||
|
tab_spaces = 4
|
||||||
|
newline_style = "Auto"
|
||||||
|
use_small_heuristics = "Default"
|
||||||
|
reorder_imports = true
|
||||||
|
reorder_modules = true
|
||||||
|
remove_nested_parens = true
|
||||||
|
edition = "2021"
|
||||||
|
merge_derives = true
|
||||||
|
use_try_shorthand = false
|
||||||
|
use_field_init_shorthand = false
|
||||||
|
force_explicit_abi = true
|
||||||
|
imports_granularity = "Crate"
|
@ -0,0 +1,280 @@
|
|||||||
|
use crate::{
|
||||||
|
config::*,
|
||||||
|
core::*,
|
||||||
|
feat,
|
||||||
|
utils::{dirs, help},
|
||||||
|
};
|
||||||
|
use crate::{ret_err, wrap_err};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use sysproxy::Sysproxy;
|
||||||
|
|
||||||
|
type CmdResult<T = ()> = Result<T, String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
|
Ok(Config::profiles().data().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn enhance_profiles() -> CmdResult {
|
||||||
|
wrap_err!(CoreManager::global().update_config().await)?;
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||||
|
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||||
|
wrap_err!(Config::profiles().data().append_item(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||||
|
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||||
|
wrap_err!(Config::profiles().data().append_item(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||||
|
wrap_err!(feat::update_profile(index, option).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_profile(index: String) -> CmdResult {
|
||||||
|
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||||
|
if should_update {
|
||||||
|
wrap_err!(CoreManager::global().update_config().await)?;
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改profiles的
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
||||||
|
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||||
|
|
||||||
|
match CoreManager::global().update_config().await {
|
||||||
|
Ok(_) => {
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
Config::profiles().apply();
|
||||||
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
Config::profiles().discard();
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
Err(format!("{err}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改某个profile item的
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||||
|
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||||
|
wrap_err!(timer::Timer::global().refresh())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn view_profile(index: String) -> CmdResult {
|
||||||
|
let file = {
|
||||||
|
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||||
|
.file
|
||||||
|
.clone()
|
||||||
|
.ok_or("the file field is null")
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
||||||
|
if !path.exists() {
|
||||||
|
ret_err!("the file not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap_err!(help::open_file(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let profiles = profiles.latest();
|
||||||
|
let item = wrap_err!(profiles.get_item(&index))?;
|
||||||
|
let data = wrap_err!(item.read_file())?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||||
|
if file_data.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let profiles = profiles.latest();
|
||||||
|
let item = wrap_err!(profiles.get_item(&index))?;
|
||||||
|
wrap_err!(item.save_file(file_data.unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||||
|
Ok(Config::clash().latest().get_client_info())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||||
|
Ok(Config::runtime().latest().config.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||||
|
let runtime = Config::runtime();
|
||||||
|
let runtime = runtime.latest();
|
||||||
|
let config = runtime.config.as_ref();
|
||||||
|
wrap_err!(config
|
||||||
|
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||||
|
.and_then(
|
||||||
|
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||||
|
Ok(Config::runtime().latest().exists_keys.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||||
|
Ok(Config::runtime().latest().chain_logs.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||||
|
wrap_err!(feat::patch_clash(payload).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_verge_config() -> CmdResult<IVerge> {
|
||||||
|
Ok(Config::verge().data().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||||
|
wrap_err!(feat::patch_verge(payload).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
|
||||||
|
wrap_err!(CoreManager::global().change_core(clash_core).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// restart the sidecar
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn restart_sidecar() -> CmdResult {
|
||||||
|
wrap_err!(CoreManager::global().run_core().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn grant_permission(core: String) -> CmdResult {
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
|
return wrap_err!(manager::grant_permission(core));
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||||
|
return Err("Unsupported target".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the system proxy
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||||
|
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
||||||
|
|
||||||
|
let mut map = Mapping::new();
|
||||||
|
map.insert("enable".into(), current.enable.into());
|
||||||
|
map.insert(
|
||||||
|
"server".into(),
|
||||||
|
format!("{}:{}", current.host, current.port).into(),
|
||||||
|
);
|
||||||
|
map.insert("bypass".into(), current.bypass.into());
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
|
||||||
|
Ok(logger::Logger::global().get_log())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_app_dir() -> CmdResult<()> {
|
||||||
|
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||||
|
wrap_err!(open::that(app_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_core_dir() -> CmdResult<()> {
|
||||||
|
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||||
|
let core_dir = core_dir.parent().ok_or(format!("failed to get core dir"))?;
|
||||||
|
wrap_err!(open::that(core_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_logs_dir() -> CmdResult<()> {
|
||||||
|
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||||
|
wrap_err!(open::that(log_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||||
|
wrap_err!(open::that(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn clash_api_get_proxy_delay(
|
||||||
|
name: String,
|
||||||
|
url: Option<String>,
|
||||||
|
) -> CmdResult<clash_api::DelayRes> {
|
||||||
|
match clash_api::get_proxy_delay(name, url).await {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(format!("{}", err.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub mod service {
|
||||||
|
use super::*;
|
||||||
|
use crate::core::win_service;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_service() -> CmdResult<win_service::JsonResponse> {
|
||||||
|
wrap_err!(win_service::check_service().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn install_service() -> CmdResult {
|
||||||
|
wrap_err!(win_service::install_service().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn uninstall_service() -> CmdResult {
|
||||||
|
wrap_err!(win_service::uninstall_service().await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub mod service {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_service() -> CmdResult {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn install_service() -> CmdResult {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn uninstall_service() -> CmdResult {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,262 @@
|
|||||||
|
use crate::utils::{dirs, help};
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct IClashTemp(pub Mapping);
|
||||||
|
|
||||||
|
impl IClashTemp {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
|
||||||
|
Ok(map) => Self(Self::guard(map)),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
Self::template()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn template() -> Self {
|
||||||
|
let mut map = Mapping::new();
|
||||||
|
|
||||||
|
map.insert(
|
||||||
|
"mixed-port".into(),
|
||||||
|
match cfg!(feature = "default-meta") {
|
||||||
|
false => 7890.into(),
|
||||||
|
true => 7898.into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map.insert("log-level".into(), "info".into());
|
||||||
|
map.insert("allow-lan".into(), false.into());
|
||||||
|
map.insert("mode".into(), "rule".into());
|
||||||
|
map.insert(
|
||||||
|
"external-controller".into(),
|
||||||
|
match cfg!(feature = "default-meta") {
|
||||||
|
false => "127.0.0.1:9090".into(),
|
||||||
|
true => "127.0.0.1:9098".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map.insert("secret".into(), "".into());
|
||||||
|
|
||||||
|
Self(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guard(mut config: Mapping) -> Mapping {
|
||||||
|
let port = Self::guard_mixed_port(&config);
|
||||||
|
let ctrl = Self::guard_server_ctrl(&config);
|
||||||
|
|
||||||
|
config.insert("mixed-port".into(), port.into());
|
||||||
|
config.insert("external-controller".into(), ctrl.into());
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patch_config(&mut self, patch: Mapping) {
|
||||||
|
for (key, value) in patch.into_iter() {
|
||||||
|
self.0.insert(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config(&self) -> Result<()> {
|
||||||
|
help::save_yaml(
|
||||||
|
&dirs::clash_path()?,
|
||||||
|
&self.0,
|
||||||
|
Some("# Generated by Clash Verge"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mixed_port(&self) -> u16 {
|
||||||
|
Self::guard_mixed_port(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client_info(&self) -> ClashInfo {
|
||||||
|
let config = &self.0;
|
||||||
|
|
||||||
|
ClashInfo {
|
||||||
|
port: Self::guard_mixed_port(&config),
|
||||||
|
server: Self::guard_client_ctrl(&config),
|
||||||
|
secret: config.get("secret").and_then(|value| match value {
|
||||||
|
Value::String(val_str) => Some(val_str.clone()),
|
||||||
|
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||||
|
Value::Number(val_num) => Some(val_num.to_string()),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard_mixed_port(config: &Mapping) -> u16 {
|
||||||
|
let mut port = config
|
||||||
|
.get("mixed-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(7890);
|
||||||
|
if port == 0 {
|
||||||
|
port = 7890;
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard_server_ctrl(config: &Mapping) -> String {
|
||||||
|
config
|
||||||
|
.get("external-controller")
|
||||||
|
.and_then(|value| match value.as_str() {
|
||||||
|
Some(val_str) => {
|
||||||
|
let val_str = val_str.trim();
|
||||||
|
|
||||||
|
let val = match val_str.starts_with(":") {
|
||||||
|
true => format!("127.0.0.1{val_str}"),
|
||||||
|
false => val_str.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
SocketAddr::from_str(val.as_str())
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
.unwrap_or("127.0.0.1:9090".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard_client_ctrl(config: &Mapping) -> String {
|
||||||
|
let value = Self::guard_server_ctrl(config);
|
||||||
|
match SocketAddr::from_str(value.as_str()) {
|
||||||
|
Ok(mut socket) => {
|
||||||
|
if socket.ip().is_unspecified() {
|
||||||
|
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||||
|
}
|
||||||
|
socket.to_string()
|
||||||
|
}
|
||||||
|
Err(_) => "127.0.0.1:9090".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct ClashInfo {
|
||||||
|
/// clash core port
|
||||||
|
pub port: u16,
|
||||||
|
/// same as `external-controller`
|
||||||
|
pub server: String,
|
||||||
|
/// clash secret
|
||||||
|
pub secret: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clash_info() {
|
||||||
|
fn get_case<T: Into<Value>, D: Into<Value>>(mp: T, ec: D) -> ClashInfo {
|
||||||
|
let mut map = Mapping::new();
|
||||||
|
map.insert("mixed-port".into(), mp.into());
|
||||||
|
map.insert("external-controller".into(), ec.into());
|
||||||
|
|
||||||
|
IClashTemp(IClashTemp::guard(map)).get_client_info()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
|
||||||
|
ClashInfo {
|
||||||
|
port,
|
||||||
|
server: server.into(),
|
||||||
|
secret: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(),
|
||||||
|
get_result(7890, "127.0.0.1:9090")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(get_case("", ""), get_result(7890, "127.0.0.1:9090"));
|
||||||
|
|
||||||
|
assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9090"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "127.0.0.1:8888"),
|
||||||
|
get_result(8888, "127.0.0.1:8888")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, " :98888 "),
|
||||||
|
get_result(8888, "127.0.0.1:9090")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "0.0.0.0:8080 "),
|
||||||
|
get_result(8888, "127.0.0.1:8080")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "0.0.0.0:8080"),
|
||||||
|
get_result(8888, "127.0.0.1:8080")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "[::]:8080"),
|
||||||
|
get_result(8888, "127.0.0.1:8080")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "192.168.1.1:8080"),
|
||||||
|
get_result(8888, "192.168.1.1:8080")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "192.168.1.1:80800"),
|
||||||
|
get_result(8888, "127.0.0.1:9090")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct IClash {
|
||||||
|
pub mixed_port: Option<u16>,
|
||||||
|
pub allow_lan: Option<bool>,
|
||||||
|
pub log_level: Option<String>,
|
||||||
|
pub ipv6: Option<bool>,
|
||||||
|
pub mode: Option<String>,
|
||||||
|
pub external_controller: Option<String>,
|
||||||
|
pub secret: Option<String>,
|
||||||
|
pub dns: Option<IClashDNS>,
|
||||||
|
pub tun: Option<IClashTUN>,
|
||||||
|
pub interface_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct IClashTUN {
|
||||||
|
pub enable: Option<bool>,
|
||||||
|
pub stack: Option<String>,
|
||||||
|
pub auto_route: Option<bool>,
|
||||||
|
pub auto_detect_interface: Option<bool>,
|
||||||
|
pub dns_hijack: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct IClashDNS {
|
||||||
|
pub enable: Option<bool>,
|
||||||
|
pub listen: Option<String>,
|
||||||
|
pub default_nameserver: Option<Vec<String>>,
|
||||||
|
pub enhanced_mode: Option<String>,
|
||||||
|
pub fake_ip_range: Option<String>,
|
||||||
|
pub use_hosts: Option<bool>,
|
||||||
|
pub fake_ip_filter: Option<Vec<String>>,
|
||||||
|
pub nameserver: Option<Vec<String>>,
|
||||||
|
pub fallback: Option<Vec<String>>,
|
||||||
|
pub fallback_filter: Option<IClashFallbackFilter>,
|
||||||
|
pub nameserver_policy: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct IClashFallbackFilter {
|
||||||
|
pub geoip: Option<bool>,
|
||||||
|
pub geoip_code: Option<String>,
|
||||||
|
pub ipcidr: Option<Vec<String>>,
|
||||||
|
pub domain: Option<Vec<String>>,
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
|
use crate::{
|
||||||
|
enhance,
|
||||||
|
utils::{dirs, help},
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::{env::temp_dir, path::PathBuf};
|
||||||
|
|
||||||
|
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||||
|
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
clash_config: Draft<IClashTemp>,
|
||||||
|
verge_config: Draft<IVerge>,
|
||||||
|
profiles_config: Draft<IProfiles>,
|
||||||
|
runtime_config: Draft<IRuntime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn global() -> &'static Config {
|
||||||
|
static CONFIG: OnceCell<Config> = OnceCell::new();
|
||||||
|
|
||||||
|
CONFIG.get_or_init(|| Config {
|
||||||
|
clash_config: Draft::from(IClashTemp::new()),
|
||||||
|
verge_config: Draft::from(IVerge::new()),
|
||||||
|
profiles_config: Draft::from(IProfiles::new()),
|
||||||
|
runtime_config: Draft::from(IRuntime::new()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clash() -> Draft<IClashTemp> {
|
||||||
|
Self::global().clash_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verge() -> Draft<IVerge> {
|
||||||
|
Self::global().verge_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn profiles() -> Draft<IProfiles> {
|
||||||
|
Self::global().profiles_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runtime() -> Draft<IRuntime> {
|
||||||
|
Self::global().runtime_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化配置
|
||||||
|
pub fn init_config() -> Result<()> {
|
||||||
|
crate::log_err!(Self::generate());
|
||||||
|
if let Err(err) = Self::generate_file(ConfigType::Run) {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
|
||||||
|
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||||
|
// 如果不存在就将默认的clash文件拿过来
|
||||||
|
if !runtime_path.exists() {
|
||||||
|
help::save_yaml(
|
||||||
|
&runtime_path,
|
||||||
|
&Config::clash().latest().0,
|
||||||
|
Some("# Clash Verge Runtime"),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将配置丢到对应的文件中
|
||||||
|
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||||
|
let path = match typ {
|
||||||
|
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
||||||
|
ConfigType::Check => temp_dir().join(CHECK_CONFIG),
|
||||||
|
};
|
||||||
|
|
||||||
|
let runtime = Config::runtime();
|
||||||
|
let runtime = runtime.latest();
|
||||||
|
let config = runtime
|
||||||
|
.config
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(anyhow!("failed to get runtime config"))?;
|
||||||
|
|
||||||
|
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成配置存好
|
||||||
|
pub fn generate() -> Result<()> {
|
||||||
|
let (config, exists_keys, logs) = enhance::enhance();
|
||||||
|
|
||||||
|
*Config::runtime().draft() = IRuntime {
|
||||||
|
config: Some(config),
|
||||||
|
exists_keys,
|
||||||
|
chain_logs: logs,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigType {
|
||||||
|
Run,
|
||||||
|
Check,
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
|
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Draft<T: Clone + ToOwned> {
|
||||||
|
inner: Arc<Mutex<(T, Option<T>)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! draft_define {
|
||||||
|
($id: ident) => {
|
||||||
|
impl Draft<$id> {
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn data(&self) -> MappedMutexGuard<$id> {
|
||||||
|
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest(&self) -> MappedMutexGuard<$id> {
|
||||||
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
|
if inner.1.is_none() {
|
||||||
|
&mut inner.0
|
||||||
|
} else {
|
||||||
|
inner.1.as_mut().unwrap()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
||||||
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
|
if inner.1.is_none() {
|
||||||
|
inner.1 = Some(inner.0.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.1.as_mut().unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(&self) -> Option<$id> {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
|
||||||
|
match inner.1.take() {
|
||||||
|
Some(draft) => {
|
||||||
|
let old_value = inner.0.to_owned();
|
||||||
|
inner.0 = draft.to_owned();
|
||||||
|
Some(old_value)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discard(&self) -> Option<$id> {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
inner.1.take()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$id> for Draft<$id> {
|
||||||
|
fn from(data: $id) -> Self {
|
||||||
|
Draft {
|
||||||
|
inner: Arc::new(Mutex::new((data, None))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// draft_define!(IClash);
|
||||||
|
draft_define!(IClashTemp);
|
||||||
|
draft_define!(IProfiles);
|
||||||
|
draft_define!(IRuntime);
|
||||||
|
draft_define!(IVerge);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draft() {
|
||||||
|
let verge = IVerge {
|
||||||
|
enable_auto_launch: Some(true),
|
||||||
|
enable_tun_mode: Some(false),
|
||||||
|
..IVerge::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let draft = Draft::from(verge);
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||||
|
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
|
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
|
let mut d = draft.draft();
|
||||||
|
d.enable_auto_launch = Some(false);
|
||||||
|
d.enable_tun_mode = Some(true);
|
||||||
|
drop(d);
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||||
|
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
assert_eq!(draft.latest().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.latest().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
assert!(draft.apply().is_some());
|
||||||
|
assert!(draft.apply().is_none());
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.data().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
let mut d = draft.draft();
|
||||||
|
d.enable_auto_launch = Some(true);
|
||||||
|
drop(d);
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
|
|
||||||
|
assert!(draft.discard().is_some());
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
|
assert!(draft.discard().is_none());
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
mod clash;
|
||||||
|
mod config;
|
||||||
|
mod draft;
|
||||||
|
mod prfitem;
|
||||||
|
mod profiles;
|
||||||
|
mod runtime;
|
||||||
|
mod verge;
|
||||||
|
|
||||||
|
pub use self::clash::*;
|
||||||
|
pub use self::config::*;
|
||||||
|
pub use self::draft::*;
|
||||||
|
pub use self::prfitem::*;
|
||||||
|
pub use self::profiles::*;
|
||||||
|
pub use self::runtime::*;
|
||||||
|
pub use self::verge::*;
|
@ -0,0 +1,374 @@
|
|||||||
|
use crate::utils::{dirs, help, tmpl};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::fs;
|
||||||
|
use sysproxy::Sysproxy;
|
||||||
|
|
||||||
|
use super::Config;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct PrfItem {
|
||||||
|
pub uid: Option<String>,
|
||||||
|
|
||||||
|
/// profile item type
|
||||||
|
/// enum value: remote | local | script | merge
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub itype: Option<String>,
|
||||||
|
|
||||||
|
/// profile name
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
/// profile file
|
||||||
|
pub file: Option<String>,
|
||||||
|
|
||||||
|
/// profile description
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub desc: Option<String>,
|
||||||
|
|
||||||
|
/// source url
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub url: Option<String>,
|
||||||
|
|
||||||
|
/// selected information
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub selected: Option<Vec<PrfSelected>>,
|
||||||
|
|
||||||
|
/// subscription user info
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub extra: Option<PrfExtra>,
|
||||||
|
|
||||||
|
/// updated time
|
||||||
|
pub updated: Option<usize>,
|
||||||
|
|
||||||
|
/// some options of the item
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub option: Option<PrfOption>,
|
||||||
|
|
||||||
|
/// the file data
|
||||||
|
#[serde(skip)]
|
||||||
|
pub file_data: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct PrfSelected {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub now: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||||
|
pub struct PrfExtra {
|
||||||
|
pub upload: usize,
|
||||||
|
pub download: usize,
|
||||||
|
pub total: usize,
|
||||||
|
pub expire: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct PrfOption {
|
||||||
|
/// for `remote` profile's http request
|
||||||
|
/// see issue #13
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub user_agent: Option<String>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// use system proxy
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub with_proxy: Option<bool>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// use self proxy
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub self_proxy: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub update_interval: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrfOption {
|
||||||
|
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
|
||||||
|
match (one, other) {
|
||||||
|
(Some(mut a), Some(b)) => {
|
||||||
|
a.user_agent = b.user_agent.or(a.user_agent);
|
||||||
|
a.with_proxy = b.with_proxy.or(a.with_proxy);
|
||||||
|
a.self_proxy = b.self_proxy.or(a.self_proxy);
|
||||||
|
a.update_interval = b.update_interval.or(a.update_interval);
|
||||||
|
Some(a)
|
||||||
|
}
|
||||||
|
t @ _ => t.0.or(t.1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PrfItem {
|
||||||
|
fn default() -> Self {
|
||||||
|
PrfItem {
|
||||||
|
uid: None,
|
||||||
|
itype: None,
|
||||||
|
name: None,
|
||||||
|
desc: None,
|
||||||
|
file: None,
|
||||||
|
url: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
updated: None,
|
||||||
|
option: None,
|
||||||
|
file_data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrfItem {
|
||||||
|
/// From partial item
|
||||||
|
/// must contain `itype`
|
||||||
|
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||||
|
if item.itype.is_none() {
|
||||||
|
bail!("type should not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
match item.itype.unwrap().as_str() {
|
||||||
|
"remote" => {
|
||||||
|
if item.url.is_none() {
|
||||||
|
bail!("url should not be null");
|
||||||
|
}
|
||||||
|
let url = item.url.as_ref().unwrap().as_str();
|
||||||
|
let name = item.name;
|
||||||
|
let desc = item.desc;
|
||||||
|
PrfItem::from_url(url, name, desc, item.option).await
|
||||||
|
}
|
||||||
|
"local" => {
|
||||||
|
let name = item.name.unwrap_or("Local File".into());
|
||||||
|
let desc = item.desc.unwrap_or("".into());
|
||||||
|
PrfItem::from_local(name, desc, file_data)
|
||||||
|
}
|
||||||
|
"merge" => {
|
||||||
|
let name = item.name.unwrap_or("Merge".into());
|
||||||
|
let desc = item.desc.unwrap_or("".into());
|
||||||
|
PrfItem::from_merge(name, desc)
|
||||||
|
}
|
||||||
|
"script" => {
|
||||||
|
let name = item.name.unwrap_or("Script".into());
|
||||||
|
let desc = item.desc.unwrap_or("".into());
|
||||||
|
PrfItem::from_script(name, desc)
|
||||||
|
}
|
||||||
|
typ @ _ => bail!("invalid profile item type \"{typ}\""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Local type
|
||||||
|
/// create a new item from name/desc
|
||||||
|
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("l");
|
||||||
|
let file = format!("{uid}.yaml");
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("local".into()),
|
||||||
|
name: Some(name),
|
||||||
|
desc: Some(desc),
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Remote type
|
||||||
|
/// create a new item from url
|
||||||
|
pub async fn from_url(
|
||||||
|
url: &str,
|
||||||
|
name: Option<String>,
|
||||||
|
desc: Option<String>,
|
||||||
|
option: Option<PrfOption>,
|
||||||
|
) -> Result<PrfItem> {
|
||||||
|
let opt_ref = option.as_ref();
|
||||||
|
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
||||||
|
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
||||||
|
let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
|
||||||
|
|
||||||
|
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||||
|
|
||||||
|
// 使用软件自己的代理
|
||||||
|
if self_proxy {
|
||||||
|
let port = Config::clash().data().get_mixed_port();
|
||||||
|
|
||||||
|
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||||
|
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 使用系统代理
|
||||||
|
else if with_proxy {
|
||||||
|
match Sysproxy::get_system_proxy() {
|
||||||
|
Ok(p @ Sysproxy { enable: true, .. }) => {
|
||||||
|
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
||||||
|
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = unsafe { dirs::APP_VERSION };
|
||||||
|
let version = format!("clash-verge/{version}");
|
||||||
|
builder = builder.user_agent(user_agent.unwrap_or(version));
|
||||||
|
|
||||||
|
let resp = builder.build()?.get(url).send().await?;
|
||||||
|
|
||||||
|
let status_code = resp.status();
|
||||||
|
if !StatusCode::is_success(&status_code) {
|
||||||
|
bail!("failed to fetch remote profile with status {status_code}")
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = resp.headers();
|
||||||
|
|
||||||
|
// parse the Subscription UserInfo
|
||||||
|
let extra = match header.get("Subscription-Userinfo") {
|
||||||
|
Some(value) => {
|
||||||
|
let sub_info = value.to_str().unwrap_or("");
|
||||||
|
|
||||||
|
Some(PrfExtra {
|
||||||
|
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
|
||||||
|
download: help::parse_str(sub_info, "download=").unwrap_or(0),
|
||||||
|
total: help::parse_str(sub_info, "total=").unwrap_or(0),
|
||||||
|
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// parse the Content-Disposition
|
||||||
|
let filename = match header.get("Content-Disposition") {
|
||||||
|
Some(value) => {
|
||||||
|
let filename = value.to_str().unwrap_or("");
|
||||||
|
help::parse_str::<String>(filename, "filename=")
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// parse the profile-update-interval
|
||||||
|
let option = match header.get("profile-update-interval") {
|
||||||
|
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
|
||||||
|
Ok(val) => Some(PrfOption {
|
||||||
|
update_interval: Some(val * 60), // hour -> min
|
||||||
|
..PrfOption::default()
|
||||||
|
}),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let uid = help::get_uid("r");
|
||||||
|
let file = format!("{uid}.yaml");
|
||||||
|
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||||
|
let data = resp.text_with_charset("utf-8").await?;
|
||||||
|
|
||||||
|
// process the charset "UTF-8 with BOM"
|
||||||
|
let data = data.trim_start_matches('\u{feff}');
|
||||||
|
|
||||||
|
// check the data whether the valid yaml format
|
||||||
|
let yaml = serde_yaml::from_str::<Mapping>(data)
|
||||||
|
.context("the remote profile data is invalid yaml")?;
|
||||||
|
|
||||||
|
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
|
||||||
|
bail!("profile does not contain `proxies` or `proxy-providers`");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("remote".into()),
|
||||||
|
name: Some(name),
|
||||||
|
desc,
|
||||||
|
file: Some(file),
|
||||||
|
url: Some(url.into()),
|
||||||
|
selected: None,
|
||||||
|
extra,
|
||||||
|
option,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(data.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Merge type (enhance)
|
||||||
|
/// create the enhanced item by using `merge` rule
|
||||||
|
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("m");
|
||||||
|
let file = format!("{uid}.yaml");
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("merge".into()),
|
||||||
|
name: Some(name),
|
||||||
|
desc: Some(desc),
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(tmpl::ITEM_MERGE.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Script type (enhance)
|
||||||
|
/// create the enhanced item by using javascript quick.js
|
||||||
|
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("s");
|
||||||
|
let file = format!("{uid}.js"); // js ext
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("script".into()),
|
||||||
|
name: Some(name),
|
||||||
|
desc: Some(desc),
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(tmpl::ITEM_SCRIPT.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the file data
|
||||||
|
pub fn read_file(&self) -> Result<String> {
|
||||||
|
if self.file.is_none() {
|
||||||
|
bail!("could not find the file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = self.file.clone().unwrap();
|
||||||
|
let path = dirs::app_profiles_dir()?.join(file);
|
||||||
|
fs::read_to_string(path).context("failed to read the file")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save the file data
|
||||||
|
pub fn save_file(&self, data: String) -> Result<()> {
|
||||||
|
if self.file.is_none() {
|
||||||
|
bail!("could not find the file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = self.file.clone().unwrap();
|
||||||
|
let path = dirs::app_profiles_dir()?.join(file);
|
||||||
|
fs::write(path, data.as_bytes()).context("failed to save the file")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
use crate::config::Config;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// PUT /configs
|
||||||
|
/// path 是绝对路径
|
||||||
|
pub async fn put_configs(path: &str) -> Result<()> {
|
||||||
|
let (url, headers) = clash_client_info()?;
|
||||||
|
let url = format!("{url}/configs");
|
||||||
|
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
data.insert("path", path);
|
||||||
|
|
||||||
|
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||||
|
let builder = client.put(&url).headers(headers).json(&data);
|
||||||
|
let response = builder.send().await?;
|
||||||
|
|
||||||
|
match response.status().as_u16() {
|
||||||
|
204 => Ok(()),
|
||||||
|
status @ _ => {
|
||||||
|
bail!("failed to put configs with status \"{status}\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PATCH /configs
|
||||||
|
pub async fn patch_configs(config: &Mapping) -> Result<()> {
|
||||||
|
let (url, headers) = clash_client_info()?;
|
||||||
|
let url = format!("{url}/configs");
|
||||||
|
|
||||||
|
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||||
|
let builder = client.patch(&url).headers(headers.clone()).json(config);
|
||||||
|
builder.send().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct DelayRes {
|
||||||
|
delay: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /proxies/{name}/delay
|
||||||
|
/// 获取代理延迟
|
||||||
|
pub async fn get_proxy_delay(name: String, test_url: Option<String>) -> Result<DelayRes> {
|
||||||
|
let (url, headers) = clash_client_info()?;
|
||||||
|
let url = format!("{url}/proxies/{name}/delay");
|
||||||
|
|
||||||
|
let default_url = "http://www.gstatic.com/generate_204";
|
||||||
|
let test_url = test_url
|
||||||
|
.map(|s| if s.is_empty() { default_url.into() } else { s })
|
||||||
|
.unwrap_or(default_url.into());
|
||||||
|
|
||||||
|
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||||
|
let builder = client
|
||||||
|
.get(&url)
|
||||||
|
.headers(headers)
|
||||||
|
.query(&[("timeout", "10000"), ("url", &test_url)]);
|
||||||
|
let response = builder.send().await?;
|
||||||
|
|
||||||
|
Ok(response.json::<DelayRes>().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据clash info获取clash服务地址和请求头
|
||||||
|
fn clash_client_info() -> Result<(String, HeaderMap)> {
|
||||||
|
let client = { Config::clash().data().get_client_info() };
|
||||||
|
|
||||||
|
let server = format!("http://{}", client.server);
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("Content-Type", "application/json".parse()?);
|
||||||
|
|
||||||
|
if let Some(secret) = client.secret {
|
||||||
|
let secret = format!("Bearer {}", secret).parse()?;
|
||||||
|
headers.insert("Authorization", secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((server, headers))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 缩短clash的日志
|
||||||
|
pub fn parse_log(log: String) -> String {
|
||||||
|
if log.starts_with("time=") && log.len() > 33 {
|
||||||
|
return (&log[33..]).to_owned();
|
||||||
|
}
|
||||||
|
if log.len() > 9 {
|
||||||
|
return (&log[9..]).to_owned();
|
||||||
|
}
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 缩短clash -t的错误输出
|
||||||
|
/// 仅适配 clash p核 8-26、clash meta 1.13.1
|
||||||
|
pub fn parse_check_output(log: String) -> String {
|
||||||
|
let t = log.find("time=");
|
||||||
|
let m = log.find("msg=");
|
||||||
|
let mr = log.rfind('"');
|
||||||
|
|
||||||
|
if let (Some(_), Some(m), Some(mr)) = (t, m, mr) {
|
||||||
|
let e = match log.find("level=error msg=") {
|
||||||
|
Some(e) => e + 17,
|
||||||
|
None => m + 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
if mr > m {
|
||||||
|
return (&log[e..mr]).to_owned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let l = log.find("error=");
|
||||||
|
let r = log.find("path=").or(Some(log.len()));
|
||||||
|
|
||||||
|
if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
return (&log[(l + 6)..(r - 1)]).to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
log
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_check_output() {
|
||||||
|
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;
|
||||||
|
let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
|
||||||
|
let str3 = r#"
|
||||||
|
"time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress"
|
||||||
|
time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'"
|
||||||
|
configuration file xxx\n
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let res1 = parse_check_output(str1.into());
|
||||||
|
let res2 = parse_check_output(str2.into());
|
||||||
|
let res3 = parse_check_output(str3.into());
|
||||||
|
|
||||||
|
println!("res1: {res1}");
|
||||||
|
println!("res2: {res2}");
|
||||||
|
println!("res3: {res3}");
|
||||||
|
|
||||||
|
assert_eq!(res1, res3);
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
use super::tray::Tray;
|
||||||
|
use crate::log_err;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauri::{AppHandle, Manager, Window};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Handle {
|
||||||
|
pub app_handle: Arc<Mutex<Option<AppHandle>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle {
|
||||||
|
pub fn global() -> &'static Handle {
|
||||||
|
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
||||||
|
|
||||||
|
HANDLE.get_or_init(|| Handle {
|
||||||
|
app_handle: Arc::new(Mutex::new(None)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(&self, app_handle: AppHandle) {
|
||||||
|
*self.app_handle.lock() = Some(app_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_window(&self) -> Option<Window> {
|
||||||
|
self.app_handle
|
||||||
|
.lock()
|
||||||
|
.as_ref()
|
||||||
|
.map_or(None, |a| a.get_window("main"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_clash() {
|
||||||
|
if let Some(window) = Self::global().get_window() {
|
||||||
|
log_err!(window.emit("verge://refresh-clash-config", "yes"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_verge() {
|
||||||
|
if let Some(window) = Self::global().get_window() {
|
||||||
|
log_err!(window.emit("verge://refresh-verge-config", "yes"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn refresh_profiles() {
|
||||||
|
if let Some(window) = Self::global().get_window() {
|
||||||
|
log_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||||
|
if let Some(window) = Self::global().get_window() {
|
||||||
|
log_err!(window.emit("verge://notice-message", (status.into(), msg.into())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_systray() -> Result<()> {
|
||||||
|
let app_handle = Self::global().app_handle.lock();
|
||||||
|
if app_handle.is_none() {
|
||||||
|
bail!("update_systray unhandled error");
|
||||||
|
}
|
||||||
|
Tray::update_systray(app_handle.as_ref().unwrap())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update the system tray state
|
||||||
|
pub fn update_systray_part() -> Result<()> {
|
||||||
|
let app_handle = Self::global().app_handle.lock();
|
||||||
|
if app_handle.is_none() {
|
||||||
|
bail!("update_systray unhandled error");
|
||||||
|
}
|
||||||
|
Tray::update_part(app_handle.as_ref().unwrap())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{collections::VecDeque, sync::Arc};
|
||||||
|
|
||||||
|
const LOGS_QUEUE_LEN: usize = 100;
|
||||||
|
|
||||||
|
pub struct Logger {
|
||||||
|
log_data: Arc<Mutex<VecDeque<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
pub fn global() -> &'static Logger {
|
||||||
|
static LOGGER: OnceCell<Logger> = OnceCell::new();
|
||||||
|
|
||||||
|
LOGGER.get_or_init(|| Logger {
|
||||||
|
log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_log(&self) -> VecDeque<String> {
|
||||||
|
self.log_data.lock().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_log(&self, text: String) {
|
||||||
|
let mut logs = self.log_data.lock();
|
||||||
|
if logs.len() > LOGS_QUEUE_LEN {
|
||||||
|
logs.pop_front();
|
||||||
|
}
|
||||||
|
logs.push_back(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_log(&self) {
|
||||||
|
let mut logs = self.log_data.lock();
|
||||||
|
logs.clear();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
/// 给clash内核的tun模式授权
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
|
pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
||||||
|
use std::process::Command;
|
||||||
|
use tauri::utils::platform::current_exe;
|
||||||
|
|
||||||
|
let path = current_exe()?.with_file_name(core).canonicalize()?;
|
||||||
|
let path = path.display().to_string();
|
||||||
|
|
||||||
|
log::debug!("grant_permission path: {path}");
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let output = {
|
||||||
|
// the path of clash /Applications/Clash Verge.app/Contents/MacOS/clash
|
||||||
|
// https://apple.stackexchange.com/questions/82967/problem-with-empty-spaces-when-executing-shell-commands-in-applescript
|
||||||
|
// let path = escape(&path);
|
||||||
|
let path = path.replace(' ', "\\\\ ");
|
||||||
|
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
|
||||||
|
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
|
||||||
|
Command::new("osascript")
|
||||||
|
.args(vec!["-e", &command])
|
||||||
|
.output()?
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let output = {
|
||||||
|
let path = path.replace(' ', "\\ "); // 避免路径中有空格
|
||||||
|
let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}");
|
||||||
|
|
||||||
|
let sudo = match Command::new("which").arg("pkexec").output() {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.stdout.is_empty() {
|
||||||
|
"sudo"
|
||||||
|
} else {
|
||||||
|
"pkexec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => "sudo",
|
||||||
|
};
|
||||||
|
|
||||||
|
Command::new(sudo).arg("sh").arg("-c").arg(shell).output()?
|
||||||
|
};
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
|
||||||
|
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)) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
pub mod clash_api;
|
||||||
|
mod core;
|
||||||
|
pub mod handle;
|
||||||
|
pub mod hotkey;
|
||||||
|
pub mod logger;
|
||||||
|
pub mod manager;
|
||||||
|
pub mod sysopt;
|
||||||
|
pub mod timer;
|
||||||
|
pub mod tray;
|
||||||
|
pub mod win_service;
|
||||||
|
|
||||||
|
pub use self::core::*;
|
@ -0,0 +1,184 @@
|
|||||||
|
use crate::config::Config;
|
||||||
|
use crate::feat;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
type TaskID = u64;
|
||||||
|
|
||||||
|
pub struct Timer {
|
||||||
|
/// cron manager
|
||||||
|
delay_timer: Arc<Mutex<DelayTimer>>,
|
||||||
|
|
||||||
|
/// save the current state
|
||||||
|
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
|
||||||
|
|
||||||
|
/// increment id
|
||||||
|
timer_count: Arc<Mutex<TaskID>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timer {
|
||||||
|
pub fn global() -> &'static Timer {
|
||||||
|
static TIMER: OnceCell<Timer> = OnceCell::new();
|
||||||
|
|
||||||
|
TIMER.get_or_init(|| Timer {
|
||||||
|
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
|
||||||
|
timer_map: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
timer_count: Arc::new(Mutex::new(1)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// restore timer
|
||||||
|
pub fn init(&self) -> Result<()> {
|
||||||
|
self.refresh()?;
|
||||||
|
|
||||||
|
let cur_timestamp = chrono::Local::now().timestamp();
|
||||||
|
|
||||||
|
let timer_map = self.timer_map.lock();
|
||||||
|
let delay_timer = self.delay_timer.lock();
|
||||||
|
|
||||||
|
Config::profiles().latest().get_items().map(|items| {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| {
|
||||||
|
// mins to seconds
|
||||||
|
let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
|
||||||
|
let updated = item.updated? as i64;
|
||||||
|
|
||||||
|
if interval > 0 && cur_timestamp - updated >= interval {
|
||||||
|
Some(item)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.for_each(|item| {
|
||||||
|
if let Some(uid) = item.uid.as_ref() {
|
||||||
|
if let Some((task_id, _)) = timer_map.get(uid) {
|
||||||
|
crate::log_err!(delay_timer.advance_task(*task_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Correctly update all cron tasks
|
||||||
|
pub fn refresh(&self) -> Result<()> {
|
||||||
|
let diff_map = self.gen_diff();
|
||||||
|
|
||||||
|
let mut timer_map = self.timer_map.lock();
|
||||||
|
let mut delay_timer = self.delay_timer.lock();
|
||||||
|
|
||||||
|
for (uid, diff) in diff_map.into_iter() {
|
||||||
|
match diff {
|
||||||
|
DiffFlag::Del(tid) => {
|
||||||
|
let _ = timer_map.remove(&uid);
|
||||||
|
crate::log_err!(delay_timer.remove_task(tid));
|
||||||
|
}
|
||||||
|
DiffFlag::Add(tid, val) => {
|
||||||
|
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||||
|
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||||
|
}
|
||||||
|
DiffFlag::Mod(tid, val) => {
|
||||||
|
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||||
|
crate::log_err!(delay_timer.remove_task(tid));
|
||||||
|
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generate a uid -> update_interval map
|
||||||
|
fn gen_map(&self) -> HashMap<String, u64> {
|
||||||
|
let mut new_map = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(items) = Config::profiles().latest().get_items() {
|
||||||
|
for item in items.iter() {
|
||||||
|
if item.option.is_some() {
|
||||||
|
let option = item.option.as_ref().unwrap();
|
||||||
|
let interval = option.update_interval.unwrap_or(0);
|
||||||
|
|
||||||
|
if interval > 0 {
|
||||||
|
new_map.insert(item.uid.clone().unwrap(), interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generate the diff map for refresh
|
||||||
|
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
||||||
|
let mut diff_map = HashMap::new();
|
||||||
|
|
||||||
|
let timer_map = self.timer_map.lock();
|
||||||
|
|
||||||
|
let new_map = self.gen_map();
|
||||||
|
let cur_map = &timer_map;
|
||||||
|
|
||||||
|
cur_map.iter().for_each(|(uid, (tid, val))| {
|
||||||
|
let new_val = new_map.get(uid).unwrap_or(&0);
|
||||||
|
|
||||||
|
if *new_val == 0 {
|
||||||
|
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
|
||||||
|
} else if new_val != val {
|
||||||
|
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut count = self.timer_count.lock();
|
||||||
|
|
||||||
|
new_map.iter().for_each(|(uid, val)| {
|
||||||
|
if cur_map.get(uid).is_none() {
|
||||||
|
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
|
||||||
|
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
diff_map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// add a cron task
|
||||||
|
fn add_task(
|
||||||
|
&self,
|
||||||
|
delay_timer: &mut DelayTimer,
|
||||||
|
uid: String,
|
||||||
|
tid: TaskID,
|
||||||
|
minutes: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
let task = TaskBuilder::default()
|
||||||
|
.set_task_id(tid)
|
||||||
|
.set_maximum_parallel_runnable_num(1)
|
||||||
|
.set_frequency_repeated_by_minutes(minutes)
|
||||||
|
// .set_frequency_repeated_by_seconds(minutes) // for test
|
||||||
|
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
|
||||||
|
.context("failed to create timer task")?;
|
||||||
|
|
||||||
|
delay_timer
|
||||||
|
.add_task(task)
|
||||||
|
.context("failed to add timer task")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the task runner
|
||||||
|
async fn async_task(uid: String) {
|
||||||
|
log::info!(target: "app", "running timer task `{uid}`");
|
||||||
|
crate::log_err!(feat::update_profile(uid, None).await);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DiffFlag {
|
||||||
|
Del(TaskID),
|
||||||
|
Add(TaskID, u64),
|
||||||
|
Mod(TaskID, u64),
|
||||||
|
}
|
@ -0,0 +1,175 @@
|
|||||||
|
use crate::{cmds, config::Config, feat, utils::resolve};
|
||||||
|
use anyhow::Result;
|
||||||
|
use tauri::{
|
||||||
|
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||||
|
SystemTraySubmenu,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Tray {}
|
||||||
|
|
||||||
|
impl Tray {
|
||||||
|
pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
|
||||||
|
let zh = { Config::verge().latest().language == Some("zh".into()) };
|
||||||
|
|
||||||
|
let version = app_handle.package_info().version.to_string();
|
||||||
|
|
||||||
|
macro_rules! t {
|
||||||
|
($en: expr, $zh: expr) => {
|
||||||
|
if zh {
|
||||||
|
$zh
|
||||||
|
} else {
|
||||||
|
$en
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemTrayMenu::new()
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"open_window",
|
||||||
|
t!("Dashboard", "打开面板"),
|
||||||
|
))
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"rule_mode",
|
||||||
|
t!("Rule Mode", "规则模式"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"global_mode",
|
||||||
|
t!("Global Mode", "全局模式"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"direct_mode",
|
||||||
|
t!("Direct Mode", "直连模式"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"script_mode",
|
||||||
|
t!("Script Mode", "脚本模式"),
|
||||||
|
))
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"system_proxy",
|
||||||
|
t!("System Proxy", "系统代理"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"copy_env",
|
||||||
|
t!("Copy Env", "复制环境变量"),
|
||||||
|
))
|
||||||
|
.add_submenu(SystemTraySubmenu::new(
|
||||||
|
t!("Open Dir", "打开目录"),
|
||||||
|
SystemTrayMenu::new()
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"open_app_dir",
|
||||||
|
t!("App Dir", "应用目录"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"open_core_dir",
|
||||||
|
t!("Core Dir", "内核目录"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"open_logs_dir",
|
||||||
|
t!("Logs Dir", "日志目录"),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.add_submenu(SystemTraySubmenu::new(
|
||||||
|
t!("More", "更多"),
|
||||||
|
SystemTrayMenu::new()
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"restart_clash",
|
||||||
|
t!("Restart Clash", "重启 Clash"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"restart_app",
|
||||||
|
t!("Restart App", "重启应用"),
|
||||||
|
))
|
||||||
|
.add_item(
|
||||||
|
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
|
||||||
|
app_handle
|
||||||
|
.tray_handle()
|
||||||
|
.set_menu(Tray::tray_menu(app_handle))?;
|
||||||
|
Tray::update_part(app_handle)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_part(app_handle: &AppHandle) -> Result<()> {
|
||||||
|
let mode = {
|
||||||
|
Config::clash()
|
||||||
|
.latest()
|
||||||
|
.0
|
||||||
|
.get("mode")
|
||||||
|
.map(|val| val.as_str().unwrap_or("rule"))
|
||||||
|
.unwrap_or("rule")
|
||||||
|
.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let tray = app_handle.tray_handle();
|
||||||
|
|
||||||
|
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
|
||||||
|
let _ = tray.get_item("global_mode").set_selected(mode == "global");
|
||||||
|
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
|
||||||
|
let _ = tray.get_item("script_mode").set_selected(mode == "script");
|
||||||
|
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.latest();
|
||||||
|
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||||
|
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let indication_icon = if *system_proxy {
|
||||||
|
include_bytes!("../../icons/win-tray-icon-activated.png").to_vec()
|
||||||
|
} else {
|
||||||
|
include_bytes!("../../icons/win-tray-icon.png").to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
|
||||||
|
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
|
||||||
|
match event {
|
||||||
|
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||||
|
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
|
||||||
|
let mode = &mode[0..mode.len() - 5];
|
||||||
|
feat::change_clash_mode(mode.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
"open_window" => resolve::create_window(app_handle),
|
||||||
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
|
"tun_mode" => feat::toggle_tun_mode(),
|
||||||
|
"copy_env" => feat::copy_clash_env(),
|
||||||
|
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
||||||
|
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
||||||
|
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||||
|
"restart_clash" => feat::restart_clash_core(),
|
||||||
|
"restart_app" => api::process::restart(&app_handle.env()),
|
||||||
|
"quit" => {
|
||||||
|
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(target_os = "windows")]
|
||||||
|
SystemTrayEvent::LeftClick { .. } => {
|
||||||
|
resolve::create_window(app_handle);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
function main(params) {
|
||||||
|
if (params.mode === "script") {
|
||||||
|
params.mode = "rule";
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
function main(params) {
|
||||||
|
if (Array.isArray(params.proxies)) {
|
||||||
|
params.proxies.forEach((p, i) => {
|
||||||
|
if (p.type === "hysteria" && typeof p.alpn === "string") {
|
||||||
|
params.proxies[i].alpn = [p.alpn];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
use crate::{
|
||||||
|
config::PrfItem,
|
||||||
|
utils::{dirs, help},
|
||||||
|
};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChainItem {
|
||||||
|
pub uid: String,
|
||||||
|
pub data: ChainType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ChainType {
|
||||||
|
Merge(Mapping),
|
||||||
|
Script(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ChainSupport {
|
||||||
|
Clash,
|
||||||
|
ClashMeta,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PrfItem> for Option<ChainItem> {
|
||||||
|
fn from(item: &PrfItem) -> Self {
|
||||||
|
let itype = item.itype.as_ref()?.as_str();
|
||||||
|
let file = item.file.clone()?;
|
||||||
|
let uid = item.uid.clone().unwrap_or("".into());
|
||||||
|
let path = dirs::app_profiles_dir().ok()?.join(file);
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match itype {
|
||||||
|
"script" => Some(ChainItem {
|
||||||
|
uid,
|
||||||
|
data: ChainType::Script(fs::read_to_string(path).ok()?),
|
||||||
|
}),
|
||||||
|
"merge" => Some(ChainItem {
|
||||||
|
uid,
|
||||||
|
data: ChainType::Merge(help::read_merge_mapping(&path).ok()?),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChainItem {
|
||||||
|
/// 内建支持一些脚本
|
||||||
|
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
|
||||||
|
// meta 的一些处理
|
||||||
|
let meta_guard =
|
||||||
|
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||||
|
|
||||||
|
// meta 1.13.2 alpn string 转 数组
|
||||||
|
let hy_alpn =
|
||||||
|
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||||
|
|
||||||
|
vec![
|
||||||
|
(ChainSupport::ClashMeta, hy_alpn),
|
||||||
|
(ChainSupport::ClashMeta, meta_guard),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_script<U: Into<String>, D: Into<String>>(uid: U, data: D) -> Self {
|
||||||
|
Self {
|
||||||
|
uid: uid.into(),
|
||||||
|
data: ChainType::Script(data.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChainSupport {
|
||||||
|
pub fn is_support(&self, core: Option<&String>) -> bool {
|
||||||
|
match core {
|
||||||
|
Some(core) => match (self, core.as_str()) {
|
||||||
|
(ChainSupport::All, _) => true,
|
||||||
|
(ChainSupport::Clash, "clash") => true,
|
||||||
|
(ChainSupport::ClashMeta, "clash-meta") => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,155 @@
|
|||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
pub const HANDLE_FIELDS: [&str; 9] = [
|
||||||
|
"mode",
|
||||||
|
"port",
|
||||||
|
"socks-port",
|
||||||
|
"mixed-port",
|
||||||
|
"allow-lan",
|
||||||
|
"log-level",
|
||||||
|
"ipv6",
|
||||||
|
"secret",
|
||||||
|
"external-controller",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const DEFAULT_FIELDS: [&str; 5] = [
|
||||||
|
"proxies",
|
||||||
|
"proxy-groups",
|
||||||
|
"proxy-providers",
|
||||||
|
"rules",
|
||||||
|
"rule-providers",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const OTHERS_FIELDS: [&str; 30] = [
|
||||||
|
"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
|
||||||
|
"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();
|
||||||
|
|
||||||
|
for (key, value) in config.into_iter() {
|
||||||
|
if let Some(key) = key.as_str() {
|
||||||
|
if filter.contains(&key.to_string()) {
|
||||||
|
ret.insert(Value::from(key), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_lowercase(config: Mapping) -> Mapping {
|
||||||
|
let mut ret = Mapping::new();
|
||||||
|
|
||||||
|
for (key, value) in config.into_iter() {
|
||||||
|
if let Some(key_str) = key.as_str() {
|
||||||
|
let mut key_str = String::from(key_str);
|
||||||
|
key_str.make_ascii_lowercase();
|
||||||
|
ret.insert(Value::from(key_str), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
|
||||||
|
let mut ret = Mapping::new();
|
||||||
|
|
||||||
|
HANDLE_FIELDS
|
||||||
|
.into_iter()
|
||||||
|
.chain(OTHERS_FIELDS)
|
||||||
|
.chain(DEFAULT_FIELDS)
|
||||||
|
.for_each(|key| {
|
||||||
|
let key = Value::from(key);
|
||||||
|
config.get(&key).map(|value| {
|
||||||
|
ret.insert(key, value.clone());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if !enable_filter {
|
||||||
|
let supported_keys: HashSet<&str> = HANDLE_FIELDS
|
||||||
|
.into_iter()
|
||||||
|
.chain(OTHERS_FIELDS)
|
||||||
|
.chain(DEFAULT_FIELDS)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let config_keys: HashSet<&str> = config
|
||||||
|
.keys()
|
||||||
|
.filter_map(|e| e.as_str())
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
config_keys.difference(&supported_keys).for_each(|&key| {
|
||||||
|
let key = Value::from(key);
|
||||||
|
config.get(&key).map(|value| {
|
||||||
|
ret.insert(key, value.clone());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_keys(config: &Mapping) -> Vec<String> {
|
||||||
|
config
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(key, _)| key.as_str())
|
||||||
|
.map(|s| {
|
||||||
|
let mut s = s.to_string();
|
||||||
|
s.make_ascii_lowercase();
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
use super::{use_filter, use_lowercase};
|
||||||
|
use serde_yaml::{self, Mapping, Sequence, Value};
|
||||||
|
|
||||||
|
const MERGE_FIELDS: [&str; 6] = [
|
||||||
|
"prepend-rules",
|
||||||
|
"append-rules",
|
||||||
|
"prepend-proxies",
|
||||||
|
"append-proxies",
|
||||||
|
"prepend-proxy-groups",
|
||||||
|
"append-proxy-groups",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
|
||||||
|
// 直接覆盖原字段
|
||||||
|
use_lowercase(merge.clone())
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|(key, value)| {
|
||||||
|
config.insert(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
|
||||||
|
let merge = use_filter(merge, &merge_list.collect(), true);
|
||||||
|
|
||||||
|
["rules", "proxies", "proxy-groups"]
|
||||||
|
.iter()
|
||||||
|
.for_each(|key_str| {
|
||||||
|
let key_val = Value::from(key_str.to_string());
|
||||||
|
|
||||||
|
let mut list = Sequence::default();
|
||||||
|
list = config.get(&key_val).map_or(list.clone(), |val| {
|
||||||
|
val.as_sequence().map_or(list, |v| v.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
let pre_key = Value::from(format!("prepend-{key_str}"));
|
||||||
|
let post_key = Value::from(format!("append-{key_str}"));
|
||||||
|
|
||||||
|
if let Some(pre_val) = merge.get(&pre_key) {
|
||||||
|
if pre_val.is_sequence() {
|
||||||
|
let mut pre_val = pre_val.as_sequence().unwrap().clone();
|
||||||
|
pre_val.extend(list);
|
||||||
|
list = pre_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(post_val) = merge.get(&post_key) {
|
||||||
|
if post_val.is_sequence() {
|
||||||
|
list.extend(post_val.as_sequence().unwrap().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.insert(key_val, Value::from(list));
|
||||||
|
});
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge() -> anyhow::Result<()> {
|
||||||
|
let merge = r"
|
||||||
|
prepend-rules:
|
||||||
|
- prepend
|
||||||
|
- 1123123
|
||||||
|
append-rules:
|
||||||
|
- append
|
||||||
|
prepend-proxies:
|
||||||
|
- 9999
|
||||||
|
append-proxies:
|
||||||
|
- 1111
|
||||||
|
rules:
|
||||||
|
- replace
|
||||||
|
proxy-groups:
|
||||||
|
- 123781923810
|
||||||
|
tun:
|
||||||
|
enable: true
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
";
|
||||||
|
|
||||||
|
let config = r"
|
||||||
|
rules:
|
||||||
|
- aaaaa
|
||||||
|
script1: test
|
||||||
|
";
|
||||||
|
|
||||||
|
let merge = serde_yaml::from_str::<Mapping>(merge)?;
|
||||||
|
let config = serde_yaml::from_str::<Mapping>(config)?;
|
||||||
|
|
||||||
|
let result = serde_yaml::to_string(&use_merge(merge, config))?;
|
||||||
|
|
||||||
|
println!("{result}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
mod chain;
|
||||||
|
mod field;
|
||||||
|
mod merge;
|
||||||
|
mod script;
|
||||||
|
mod tun;
|
||||||
|
|
||||||
|
pub(self) use self::field::*;
|
||||||
|
|
||||||
|
use self::chain::*;
|
||||||
|
use self::merge::*;
|
||||||
|
use self::script::*;
|
||||||
|
use self::tun::*;
|
||||||
|
use crate::config::Config;
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
type ResultLog = Vec<(String, String)>;
|
||||||
|
|
||||||
|
/// Enhance mode
|
||||||
|
/// 返回最终配置、该配置包含的键、和script执行的结果
|
||||||
|
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||||
|
// config.yaml 的配置
|
||||||
|
let clash_config = { Config::clash().latest().0.clone() };
|
||||||
|
|
||||||
|
let (clash_core, enable_tun, enable_builtin, enable_filter) = {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.latest();
|
||||||
|
(
|
||||||
|
verge.clash_core.clone(),
|
||||||
|
verge.enable_tun_mode.clone().unwrap_or(false),
|
||||||
|
verge.enable_builtin_enhanced.clone().unwrap_or(true),
|
||||||
|
verge.enable_clash_fields.clone().unwrap_or(true),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从profiles里拿东西
|
||||||
|
let (mut config, chain, valid) = {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let profiles = profiles.latest();
|
||||||
|
|
||||||
|
let current = profiles.current_mapping().unwrap_or(Mapping::new());
|
||||||
|
|
||||||
|
let chain = match profiles.chain.as_ref() {
|
||||||
|
Some(chain) => chain
|
||||||
|
.iter()
|
||||||
|
.filter_map(|uid| profiles.get_item(uid).ok())
|
||||||
|
.filter_map(|item| <Option<ChainItem>>::from(item))
|
||||||
|
.collect::<Vec<ChainItem>>(),
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let valid = profiles.valid.clone().unwrap_or(vec![]);
|
||||||
|
|
||||||
|
(current, chain, valid)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result_map = HashMap::new(); // 保存脚本日志
|
||||||
|
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
||||||
|
|
||||||
|
let valid = use_valid_fields(valid);
|
||||||
|
config = use_filter(config, &valid, enable_filter);
|
||||||
|
|
||||||
|
// 处理用户的profile
|
||||||
|
chain.into_iter().for_each(|item| match item.data {
|
||||||
|
ChainType::Merge(merge) => {
|
||||||
|
exists_keys.extend(use_keys(&merge));
|
||||||
|
config = use_merge(merge, config.to_owned());
|
||||||
|
config = use_filter(config.to_owned(), &valid, enable_filter);
|
||||||
|
}
|
||||||
|
ChainType::Script(script) => {
|
||||||
|
let mut logs = vec![];
|
||||||
|
|
||||||
|
match use_script(script, config.to_owned()) {
|
||||||
|
Ok((res_config, res_logs)) => {
|
||||||
|
exists_keys.extend(use_keys(&res_config));
|
||||||
|
config = use_filter(res_config, &valid, enable_filter);
|
||||||
|
logs.extend(res_logs);
|
||||||
|
}
|
||||||
|
Err(err) => logs.push(("exception".into(), err.to_string())),
|
||||||
|
}
|
||||||
|
|
||||||
|
result_map.insert(item.uid, logs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 合并默认的config
|
||||||
|
for (key, value) in clash_config.into_iter() {
|
||||||
|
config.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let clash_fields = use_clash_fields();
|
||||||
|
|
||||||
|
// 内建脚本最后跑
|
||||||
|
if enable_builtin {
|
||||||
|
ChainItem::builtin()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||||
|
.map(|(_, c)| c)
|
||||||
|
.for_each(|item| {
|
||||||
|
log::debug!(target: "app", "run builtin script {}", item.uid);
|
||||||
|
|
||||||
|
match item.data {
|
||||||
|
ChainType::Script(script) => match use_script(script, config.to_owned()) {
|
||||||
|
Ok((res_config, _)) => {
|
||||||
|
config = use_filter(res_config, &clash_fields, enable_filter);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "builtin script error `{err}`");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config = use_filter(config, &clash_fields, enable_filter);
|
||||||
|
config = use_tun(config, enable_tun);
|
||||||
|
config = use_sort(config, enable_filter);
|
||||||
|
|
||||||
|
let mut exists_set = HashSet::new();
|
||||||
|
exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));
|
||||||
|
exists_keys = exists_set.into_iter().collect();
|
||||||
|
|
||||||
|
(config, exists_keys, result_map)
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
use super::use_lowercase;
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
|
||||||
|
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
|
||||||
|
use rquickjs::{Context, Func, Runtime};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
let runtime = Runtime::new().unwrap();
|
||||||
|
let context = Context::full(&runtime).unwrap();
|
||||||
|
let outputs = Arc::new(Mutex::new(vec![]));
|
||||||
|
|
||||||
|
let copy_outputs = outputs.clone();
|
||||||
|
let result = context.with(|ctx| -> Result<Mapping> {
|
||||||
|
ctx.globals().set(
|
||||||
|
"__verge_log__",
|
||||||
|
Func::from(move |level: String, data: String| {
|
||||||
|
let mut out = copy_outputs.lock().unwrap();
|
||||||
|
out.push((level, data));
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
ctx.eval(
|
||||||
|
r#"var console = Object.freeze({
|
||||||
|
log(data){__verge_log__("log",JSON.stringify(data))},
|
||||||
|
info(data){__verge_log__("info",JSON.stringify(data))},
|
||||||
|
error(data){__verge_log__("error",JSON.stringify(data))},
|
||||||
|
debug(data){__verge_log__("debug",JSON.stringify(data))},
|
||||||
|
});"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let config = use_lowercase(config.clone());
|
||||||
|
let config_str = serde_json::to_string(&config)?;
|
||||||
|
|
||||||
|
let code = format!(
|
||||||
|
r#"try{{
|
||||||
|
{script};
|
||||||
|
JSON.stringify(main({config_str})||'')
|
||||||
|
}} catch(err) {{
|
||||||
|
`__error_flag__ ${{err.toString()}}`
|
||||||
|
}}"#
|
||||||
|
);
|
||||||
|
let result: String = ctx.eval(code.as_str())?;
|
||||||
|
if result.starts_with("__error_flag__") {
|
||||||
|
anyhow::bail!(result[15..].to_owned());
|
||||||
|
}
|
||||||
|
if result == "\"\"" {
|
||||||
|
anyhow::bail!("main function should return object");
|
||||||
|
}
|
||||||
|
return Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut out = outputs.lock().unwrap();
|
||||||
|
match result {
|
||||||
|
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||||
|
Err(err) => {
|
||||||
|
out.push(("exception".into(), err.to_string()));
|
||||||
|
Ok((config, out.to_vec()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_script() {
|
||||||
|
let script = r#"
|
||||||
|
function main(config) {
|
||||||
|
if (Array.isArray(config.rules)) {
|
||||||
|
config.rules = [...config.rules, "add"];
|
||||||
|
}
|
||||||
|
console.log(config);
|
||||||
|
config.proxies = ["111"];
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let config = r#"
|
||||||
|
rules:
|
||||||
|
- 111
|
||||||
|
- 222
|
||||||
|
tun:
|
||||||
|
enable: false
|
||||||
|
dns:
|
||||||
|
enable: false
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let config = serde_yaml::from_str(config).unwrap();
|
||||||
|
let (config, results) = use_script(script.into(), config).unwrap();
|
||||||
|
|
||||||
|
let config_str = serde_yaml::to_string(&config).unwrap();
|
||||||
|
|
||||||
|
println!("{config_str}");
|
||||||
|
|
||||||
|
dbg!(results);
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
|
||||||
|
macro_rules! revise {
|
||||||
|
($map: expr, $key: expr, $val: expr) => {
|
||||||
|
let ret_key = Value::String($key.into());
|
||||||
|
$map.insert(ret_key, Value::from($val));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if key not exists then append value
|
||||||
|
macro_rules! append {
|
||||||
|
($map: expr, $key: expr, $val: expr) => {
|
||||||
|
let ret_key = Value::String($key.into());
|
||||||
|
if !$map.contains_key(&ret_key) {
|
||||||
|
$map.insert(ret_key, Value::from($val));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||||
|
let tun_key = Value::from("tun");
|
||||||
|
let tun_val = config.get(&tun_key);
|
||||||
|
|
||||||
|
if !enable && tun_val.is_none() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
||||||
|
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||||
|
});
|
||||||
|
|
||||||
|
revise!(tun_val, "enable", enable);
|
||||||
|
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);
|
||||||
|
|
||||||
|
if enable {
|
||||||
|
use_dns_for_tun(config)
|
||||||
|
} else {
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_dns_for_tun(mut config: Mapping) -> Mapping {
|
||||||
|
let dns_key = Value::from("dns");
|
||||||
|
let dns_val = config.get(&dns_key);
|
||||||
|
|
||||||
|
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
|
||||||
|
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开启tun将同时开启dns
|
||||||
|
revise!(dns_val, "enable", true);
|
||||||
|
|
||||||
|
append!(dns_val, "enhanced-mode", "fake-ip");
|
||||||
|
append!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
||||||
|
append!(
|
||||||
|
dns_val,
|
||||||
|
"nameserver",
|
||||||
|
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
|
||||||
|
);
|
||||||
|
append!(dns_val, "fallback", vec![] as Vec<&str>);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
append!(
|
||||||
|
dns_val,
|
||||||
|
"fake-ip-filter",
|
||||||
|
vec![
|
||||||
|
"dns.msftncsi.com",
|
||||||
|
"www.msftncsi.com",
|
||||||
|
"www.msftconnecttest.com"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
revise!(config, "dns", dns_val);
|
||||||
|
config
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
|
mod cmds;
|
||||||
|
mod config;
|
||||||
|
mod core;
|
||||||
|
mod enhance;
|
||||||
|
mod feat;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use crate::utils::{init, resolve, server};
|
||||||
|
use tauri::{api, SystemTray};
|
||||||
|
|
||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
// 单例检测
|
||||||
|
if server::check_singleton().is_err() {
|
||||||
|
println!("app exists");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::log_err!(init::init_config());
|
||||||
|
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut builder = tauri::Builder::default()
|
||||||
|
.system_tray(SystemTray::new())
|
||||||
|
.setup(|app| Ok(resolve::resolve_setup(app)))
|
||||||
|
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
// common
|
||||||
|
cmds::get_sys_proxy,
|
||||||
|
cmds::open_app_dir,
|
||||||
|
cmds::open_logs_dir,
|
||||||
|
cmds::open_web_url,
|
||||||
|
cmds::open_core_dir,
|
||||||
|
// cmds::kill_sidecar,
|
||||||
|
cmds::restart_sidecar,
|
||||||
|
cmds::grant_permission,
|
||||||
|
// clash
|
||||||
|
cmds::get_clash_info,
|
||||||
|
cmds::get_clash_logs,
|
||||||
|
cmds::patch_clash_config,
|
||||||
|
cmds::change_clash_core,
|
||||||
|
cmds::get_runtime_config,
|
||||||
|
cmds::get_runtime_yaml,
|
||||||
|
cmds::get_runtime_exists,
|
||||||
|
cmds::get_runtime_logs,
|
||||||
|
// verge
|
||||||
|
cmds::get_verge_config,
|
||||||
|
cmds::patch_verge_config,
|
||||||
|
// cmds::update_hotkeys,
|
||||||
|
// profile
|
||||||
|
cmds::get_profiles,
|
||||||
|
cmds::enhance_profiles,
|
||||||
|
cmds::patch_profiles_config,
|
||||||
|
cmds::view_profile,
|
||||||
|
cmds::patch_profile,
|
||||||
|
cmds::create_profile,
|
||||||
|
cmds::import_profile,
|
||||||
|
cmds::update_profile,
|
||||||
|
cmds::delete_profile,
|
||||||
|
cmds::read_profile_file,
|
||||||
|
cmds::save_profile_file,
|
||||||
|
// service mode
|
||||||
|
cmds::service::check_service,
|
||||||
|
cmds::service::install_service,
|
||||||
|
cmds::service::uninstall_service,
|
||||||
|
// clash api
|
||||||
|
cmds::clash_api_get_proxy_delay
|
||||||
|
]);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
use tauri::{Menu, MenuItem, Submenu};
|
||||||
|
|
||||||
|
builder = builder.menu(
|
||||||
|
Menu::new().add_submenu(Submenu::new(
|
||||||
|
"Edit",
|
||||||
|
Menu::new()
|
||||||
|
.add_native_item(MenuItem::Undo)
|
||||||
|
.add_native_item(MenuItem::Redo)
|
||||||
|
.add_native_item(MenuItem::Copy)
|
||||||
|
.add_native_item(MenuItem::Paste)
|
||||||
|
.add_native_item(MenuItem::Cut)
|
||||||
|
.add_native_item(MenuItem::SelectAll)
|
||||||
|
.add_native_item(MenuItem::CloseWindow)
|
||||||
|
.add_native_item(MenuItem::Quit),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = builder
|
||||||
|
.build(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
|
||||||
|
app.run(|app_handle, e| match e {
|
||||||
|
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||||
|
api.prevent_exit();
|
||||||
|
}
|
||||||
|
tauri::RunEvent::Exit => {
|
||||||
|
resolve::resolve_reset();
|
||||||
|
api::process::kill_children();
|
||||||
|
app_handle.exit(0);
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
if label == "main" {
|
||||||
|
match event {
|
||||||
|
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||||
|
api.prevent_close();
|
||||||
|
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||||
|
|
||||||
|
app_handle.get_window("main").map(|win| {
|
||||||
|
let _ = win.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||||
|
if label == "main" {
|
||||||
|
match event {
|
||||||
|
tauri::WindowEvent::CloseRequested { .. } => {
|
||||||
|
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||||
|
}
|
||||||
|
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
||||||
|
let _ = resolve::save_window_size_position(&app_handle, false);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::{
|
||||||
|
api::path::{home_dir, resource_dir},
|
||||||
|
Env, PackageInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "verge-dev"))]
|
||||||
|
static APP_DIR: &str = "clash-verge";
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
static APP_DIR: &str = "clash-verge-dev";
|
||||||
|
|
||||||
|
static CLASH_CONFIG: &str = "config.yaml";
|
||||||
|
static VERGE_CONFIG: &str = "verge.yaml";
|
||||||
|
static PROFILE_YAML: &str = "profiles.yaml";
|
||||||
|
|
||||||
|
static mut RESOURCE_DIR: Option<PathBuf> = None;
|
||||||
|
|
||||||
|
/// portable flag
|
||||||
|
#[allow(unused)]
|
||||||
|
static mut PORTABLE_FLAG: bool = false;
|
||||||
|
|
||||||
|
pub static mut APP_VERSION: &str = "v1.2.0";
|
||||||
|
|
||||||
|
/// initialize portable flag
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub unsafe fn init_portable_flag() -> Result<()> {
|
||||||
|
use tauri::utils::platform::current_exe;
|
||||||
|
|
||||||
|
let exe = current_exe()?;
|
||||||
|
|
||||||
|
if let Some(dir) = exe.parent() {
|
||||||
|
let dir = PathBuf::from(dir).join(".config/PORTABLE");
|
||||||
|
|
||||||
|
if dir.exists() {
|
||||||
|
PORTABLE_FLAG = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the verge app home dir
|
||||||
|
pub fn app_home_dir() -> Result<PathBuf> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
unsafe {
|
||||||
|
use tauri::utils::platform::current_exe;
|
||||||
|
|
||||||
|
if !PORTABLE_FLAG {
|
||||||
|
Ok(home_dir()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
|
||||||
|
.join(".config")
|
||||||
|
.join(APP_DIR))
|
||||||
|
} else {
|
||||||
|
let app_exe = current_exe()?;
|
||||||
|
let app_exe = dunce::canonicalize(app_exe)?;
|
||||||
|
let app_dir = app_exe
|
||||||
|
.parent()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
||||||
|
Ok(PathBuf::from(app_dir).join(".config").join(APP_DIR))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
Ok(home_dir()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
|
||||||
|
.join(".config")
|
||||||
|
.join(APP_DIR))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the resources dir
|
||||||
|
pub fn app_resources_dir(package_info: &PackageInfo) -> Result<PathBuf> {
|
||||||
|
let res_dir = resource_dir(package_info, &Env::default())
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
||||||
|
.join("resources");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
RESOURCE_DIR = Some(res_dir.clone());
|
||||||
|
|
||||||
|
let ver = package_info.version.to_string();
|
||||||
|
let ver_str = format!("v{ver}");
|
||||||
|
APP_VERSION = Box::leak(Box::new(ver_str));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// profiles dir
|
||||||
|
pub fn app_profiles_dir() -> Result<PathBuf> {
|
||||||
|
Ok(app_home_dir()?.join("profiles"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// logs dir
|
||||||
|
pub fn app_logs_dir() -> Result<PathBuf> {
|
||||||
|
Ok(app_home_dir()?.join("logs"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clash_path() -> Result<PathBuf> {
|
||||||
|
Ok(app_home_dir()?.join(CLASH_CONFIG))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verge_path() -> Result<PathBuf> {
|
||||||
|
Ok(app_home_dir()?.join(VERGE_CONFIG))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn profiles_path() -> Result<PathBuf> {
|
||||||
|
Ok(app_home_dir()?.join(PROFILE_YAML))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn app_res_dir() -> Result<PathBuf> {
|
||||||
|
unsafe {
|
||||||
|
Ok(RESOURCE_DIR
|
||||||
|
.clone()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clash_pid_path() -> Result<PathBuf> {
|
||||||
|
unsafe {
|
||||||
|
Ok(RESOURCE_DIR
|
||||||
|
.clone()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
||||||
|
.join("clash.pid"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn service_path() -> Result<PathBuf> {
|
||||||
|
unsafe {
|
||||||
|
let res_dir = RESOURCE_DIR
|
||||||
|
.clone()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?;
|
||||||
|
Ok(res_dir.join("clash-verge-service.exe"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn service_log_file() -> Result<PathBuf> {
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
|
let log_dir = app_logs_dir()?.join("service");
|
||||||
|
|
||||||
|
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||||
|
let log_file = format!("{}.log", local_time);
|
||||||
|
let log_file = log_dir.join(log_file);
|
||||||
|
|
||||||
|
let _ = std::fs::create_dir_all(&log_dir);
|
||||||
|
|
||||||
|
Ok(log_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
|
||||||
|
let path_str = path
|
||||||
|
.as_os_str()
|
||||||
|
.to_str()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get path from {:?}", path))?;
|
||||||
|
Ok(path_str)
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
use nanoid::nanoid;
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::{fs, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
/// read data from yaml as struct T
|
||||||
|
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||||
|
if !path.exists() {
|
||||||
|
bail!("file not found \"{}\"", path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let yaml_str = fs::read_to_string(&path)
|
||||||
|
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||||
|
|
||||||
|
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to read the file with yaml format \"{}\"",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// read mapping from yaml fix #165
|
||||||
|
pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||||
|
let mut val: Value = read_yaml(path)?;
|
||||||
|
val.apply_merge()
|
||||||
|
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||||
|
|
||||||
|
Ok(val
|
||||||
|
.as_mapping()
|
||||||
|
.ok_or(anyhow!(
|
||||||
|
"failed to transform to yaml mapping \"{}\"",
|
||||||
|
path.display()
|
||||||
|
))?
|
||||||
|
.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save the data to the file
|
||||||
|
/// can set `prefix` string to add some comments
|
||||||
|
pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
|
||||||
|
let data_str = serde_yaml::to_string(data)?;
|
||||||
|
|
||||||
|
let yaml_str = match prefix {
|
||||||
|
Some(prefix) => format!("{prefix}\n\n{data_str}"),
|
||||||
|
None => data_str,
|
||||||
|
};
|
||||||
|
|
||||||
|
let path_str = path.as_os_str().to_string_lossy().to_string();
|
||||||
|
fs::write(path, yaml_str.as_bytes())
|
||||||
|
.with_context(|| format!("failed to save file \"{path_str}\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALPHABET: [char; 62] = [
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
|
||||||
|
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
|
||||||
|
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
|
||||||
|
'V', 'W', 'X', 'Y', 'Z',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// generate the uid
|
||||||
|
pub fn get_uid(prefix: &str) -> String {
|
||||||
|
let id = nanoid!(11, &ALPHABET);
|
||||||
|
format!("{prefix}{id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// parse the string
|
||||||
|
/// xxx=123123; => 123123
|
||||||
|
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||||
|
target.find(key).and_then(|idx| {
|
||||||
|
let idx = idx + key.len();
|
||||||
|
let value = &target[idx..];
|
||||||
|
|
||||||
|
match value.split(';').nth(0) {
|
||||||
|
Some(value) => value.trim().parse(),
|
||||||
|
None => value.trim().parse(),
|
||||||
|
}
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// open file
|
||||||
|
/// use vscode by default
|
||||||
|
pub fn open_file(path: PathBuf) -> Result<()> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let code = "Visual Studio Code";
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let code = "code";
|
||||||
|
|
||||||
|
// use vscode first
|
||||||
|
if let Err(err) = open::with(&path, code) {
|
||||||
|
log::error!(target: "app", "failed to open file with VScode `{err}`");
|
||||||
|
// default open
|
||||||
|
open::that(path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! error {
|
||||||
|
($result: expr) => {
|
||||||
|
log::error!(target: "app", "{}", $result);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_err {
|
||||||
|
($result: expr) => {
|
||||||
|
if let Err(err) = $result {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($result: expr, $err_str: expr) => {
|
||||||
|
if let Err(_) = $result {
|
||||||
|
log::error!(target: "app", "{}", $err_str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! trace_err {
|
||||||
|
($result: expr, $err_str: expr) => {
|
||||||
|
if let Err(err) = $result {
|
||||||
|
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// wrap the anyhow error
|
||||||
|
/// transform the error to String
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! wrap_err {
|
||||||
|
($stat: expr) => {
|
||||||
|
match $stat {
|
||||||
|
Ok(a) => Ok(a),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{}", err.to_string());
|
||||||
|
Err(format!("{}", err.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return the string literal error
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! ret_err {
|
||||||
|
($str: expr) => {
|
||||||
|
return Err($str.into())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_value() {
|
||||||
|
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||||
|
let test_2 = "attachment; filename=Clash.yaml";
|
||||||
|
|
||||||
|
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
|
||||||
|
assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222);
|
||||||
|
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
|
||||||
|
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
|
||||||
|
assert_eq!(
|
||||||
|
parse_str::<String>(test_2, "filename=").unwrap(),
|
||||||
|
format!("Clash.yaml")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(parse_str::<usize>(test_1, "aaa="), None);
|
||||||
|
assert_eq!(parse_str::<usize>(test_1, "upload1="), None);
|
||||||
|
assert_eq!(parse_str::<usize>(test_1, "expire1="), None);
|
||||||
|
assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
|
||||||
|
}
|
@ -0,0 +1,243 @@
|
|||||||
|
use crate::config::*;
|
||||||
|
use crate::utils::{dirs, help};
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use log::LevelFilter;
|
||||||
|
use log4rs::append::console::ConsoleAppender;
|
||||||
|
use log4rs::append::file::FileAppender;
|
||||||
|
use log4rs::config::{Appender, Logger, Root};
|
||||||
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
|
use std::fs::{self, DirEntry};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tauri::PackageInfo;
|
||||||
|
|
||||||
|
/// initialize this instance's log file
|
||||||
|
fn init_log() -> Result<()> {
|
||||||
|
let log_dir = dirs::app_logs_dir()?;
|
||||||
|
if !log_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&log_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let log_level = Config::verge().data().get_log_level();
|
||||||
|
if log_level == LevelFilter::Off {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||||
|
let log_file = format!("{}.log", local_time);
|
||||||
|
let log_file = log_dir.join(log_file);
|
||||||
|
|
||||||
|
let log_pattern = match log_level {
|
||||||
|
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
|
||||||
|
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
|
||||||
|
};
|
||||||
|
|
||||||
|
let encode = Box::new(PatternEncoder::new(log_pattern));
|
||||||
|
|
||||||
|
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
|
||||||
|
let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
|
||||||
|
|
||||||
|
let mut logger_builder = Logger::builder();
|
||||||
|
let mut root_builder = Root::builder();
|
||||||
|
|
||||||
|
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
|
||||||
|
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
{
|
||||||
|
logger_builder = logger_builder.appenders(["file", "stdout"]);
|
||||||
|
if log_more {
|
||||||
|
root_builder = root_builder.appenders(["file", "stdout"]);
|
||||||
|
} else {
|
||||||
|
root_builder = root_builder.appenders(["stdout"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "verge-dev"))]
|
||||||
|
{
|
||||||
|
logger_builder = logger_builder.appenders(["file"]);
|
||||||
|
if log_more {
|
||||||
|
root_builder = root_builder.appenders(["file"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (config, _) = log4rs::config::Config::builder()
|
||||||
|
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||||
|
.appender(Appender::builder().build("file", Box::new(tofile)))
|
||||||
|
.logger(logger_builder.additive(false).build("app", log_level))
|
||||||
|
.build_lossy(root_builder.build(log_level));
|
||||||
|
|
||||||
|
log4rs::init_config(config)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除log文件
|
||||||
|
pub fn delete_log() -> Result<()> {
|
||||||
|
let log_dir = dirs::app_logs_dir()?;
|
||||||
|
if !log_dir.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let auto_log_clean = {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.data();
|
||||||
|
verge.auto_log_clean.clone().unwrap_or(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let day = match auto_log_clean {
|
||||||
|
1 => 7,
|
||||||
|
2 => 30,
|
||||||
|
3 => 90,
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!(target: "app", "try to delete log files, day: {day}");
|
||||||
|
|
||||||
|
// %Y-%m-%d to NaiveDateTime
|
||||||
|
let parse_time_str = |s: &str| {
|
||||||
|
let sa: Vec<&str> = s.split('-').collect();
|
||||||
|
if sa.len() != 4 {
|
||||||
|
return Err(anyhow::anyhow!("invalid time str"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let year = i32::from_str(sa[0])?;
|
||||||
|
let month = u32::from_str(sa[1])?;
|
||||||
|
let day = u32::from_str(sa[2])?;
|
||||||
|
let time = chrono::NaiveDate::from_ymd_opt(year, month, day)
|
||||||
|
.ok_or(anyhow::anyhow!("invalid time str"))?
|
||||||
|
.and_hms_opt(0, 0, 0)
|
||||||
|
.ok_or(anyhow::anyhow!("invalid time str"))?;
|
||||||
|
Ok(time)
|
||||||
|
};
|
||||||
|
|
||||||
|
let process_file = |file: DirEntry| -> Result<()> {
|
||||||
|
let file_name = file.file_name();
|
||||||
|
let file_name = file_name.to_str().unwrap_or_default();
|
||||||
|
|
||||||
|
if file_name.ends_with(".log") {
|
||||||
|
let now = Local::now();
|
||||||
|
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
|
||||||
|
let file_time = DateTime::<Local>::from_local(created_time, now.offset().clone());
|
||||||
|
|
||||||
|
let duration = now.signed_duration_since(file_time);
|
||||||
|
if duration.num_days() > day {
|
||||||
|
let file_path = file.path();
|
||||||
|
let _ = fs::remove_file(file_path);
|
||||||
|
log::info!(target: "app", "delete log file: {file_name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
for file in fs::read_dir(&log_dir)? {
|
||||||
|
if let Ok(file) = file {
|
||||||
|
let _ = process_file(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize all the config files
|
||||||
|
/// before tauri setup
|
||||||
|
pub fn init_config() -> Result<()> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
unsafe {
|
||||||
|
let _ = dirs::init_portable_flag();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = init_log();
|
||||||
|
let _ = delete_log();
|
||||||
|
|
||||||
|
crate::log_err!(dirs::app_home_dir().map(|app_dir| {
|
||||||
|
if !app_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&app_dir);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| {
|
||||||
|
if !profiles_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&profiles_dir);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
crate::log_err!(dirs::clash_path().map(|path| {
|
||||||
|
if !path.exists() {
|
||||||
|
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Verge"))?;
|
||||||
|
}
|
||||||
|
<Result<()>>::Ok(())
|
||||||
|
}));
|
||||||
|
|
||||||
|
crate::log_err!(dirs::verge_path().map(|path| {
|
||||||
|
if !path.exists() {
|
||||||
|
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
|
||||||
|
}
|
||||||
|
<Result<()>>::Ok(())
|
||||||
|
}));
|
||||||
|
|
||||||
|
crate::log_err!(dirs::profiles_path().map(|path| {
|
||||||
|
if !path.exists() {
|
||||||
|
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
|
||||||
|
}
|
||||||
|
<Result<()>>::Ok(())
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// initialize app resources
|
||||||
|
/// after tauri setup
|
||||||
|
pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
||||||
|
let app_dir = dirs::app_home_dir()?;
|
||||||
|
let res_dir = dirs::app_resources_dir(package_info)?;
|
||||||
|
|
||||||
|
if !app_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&app_dir);
|
||||||
|
}
|
||||||
|
if !res_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&res_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"];
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
||||||
|
|
||||||
|
// copy the resource file
|
||||||
|
// if the source file is newer than the destination file, copy it over
|
||||||
|
for file in file_list.iter() {
|
||||||
|
let src_path = res_dir.join(file);
|
||||||
|
let dest_path = app_dir.join(file);
|
||||||
|
|
||||||
|
let handle_copy = || {
|
||||||
|
match fs::copy(&src_path, &dest_path) {
|
||||||
|
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "failed to copy resources '{file}', {err}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if src_path.exists() && !dest_path.exists() {
|
||||||
|
handle_copy();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());
|
||||||
|
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());
|
||||||
|
|
||||||
|
match (src_modified, dest_modified) {
|
||||||
|
(Ok(src_modified), Ok(dest_modified)) => {
|
||||||
|
if src_modified > dest_modified {
|
||||||
|
handle_copy();
|
||||||
|
} else {
|
||||||
|
log::debug!(target: "app", "skipping resource copy '{file}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::debug!(target: "app", "failed to get modified '{file}'");
|
||||||
|
handle_copy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
pub mod dirs;
|
||||||
|
pub mod help;
|
||||||
|
pub mod init;
|
||||||
|
pub mod resolve;
|
||||||
|
pub mod server;
|
||||||
|
pub mod tmpl;
|
||||||
|
// mod winhelp;
|
@ -0,0 +1,179 @@
|
|||||||
|
use crate::{config::Config, core::*, utils::init, utils::server};
|
||||||
|
use crate::{log_err, trace_err};
|
||||||
|
use anyhow::Result;
|
||||||
|
use tauri::{App, AppHandle, Manager};
|
||||||
|
|
||||||
|
/// handle something when start app
|
||||||
|
pub fn resolve_setup(app: &mut App) {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
|
|
||||||
|
handle::Handle::global().init(app.app_handle());
|
||||||
|
|
||||||
|
log_err!(init::init_resources(app.package_info()));
|
||||||
|
|
||||||
|
// 启动核心
|
||||||
|
log::trace!("init config");
|
||||||
|
log_err!(Config::init_config());
|
||||||
|
|
||||||
|
log::trace!("launch core");
|
||||||
|
log_err!(CoreManager::global().init());
|
||||||
|
|
||||||
|
// setup a simple http server for singleton
|
||||||
|
log::trace!("launch embed server");
|
||||||
|
server::embed_server(app.app_handle());
|
||||||
|
|
||||||
|
log::trace!("init system tray");
|
||||||
|
log_err!(tray::Tray::update_systray(&app.app_handle()));
|
||||||
|
|
||||||
|
let silent_start = { Config::verge().data().enable_silent_start.clone() };
|
||||||
|
if !silent_start.unwrap_or(false) {
|
||||||
|
create_window(&app.app_handle());
|
||||||
|
}
|
||||||
|
|
||||||
|
log_err!(sysopt::Sysopt::global().init_launch());
|
||||||
|
log_err!(sysopt::Sysopt::global().init_sysproxy());
|
||||||
|
|
||||||
|
log_err!(handle::Handle::update_systray_part());
|
||||||
|
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
|
||||||
|
log_err!(timer::Timer::global().init());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// reset system proxy
|
||||||
|
pub fn resolve_reset() {
|
||||||
|
log_err!(sysopt::Sysopt::global().reset_sysproxy());
|
||||||
|
log_err!(CoreManager::global().stop_core());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// create main window
|
||||||
|
pub fn create_window(app_handle: &AppHandle) {
|
||||||
|
if let Some(window) = app_handle.get_window("main") {
|
||||||
|
trace_err!(window.unminimize(), "set win unminimize");
|
||||||
|
trace_err!(window.show(), "set win visible");
|
||||||
|
trace_err!(window.set_focus(), "set win focus");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = tauri::window::WindowBuilder::new(
|
||||||
|
app_handle,
|
||||||
|
"main".to_string(),
|
||||||
|
tauri::WindowUrl::App("index.html".into()),
|
||||||
|
)
|
||||||
|
.title("Clash Verge")
|
||||||
|
.fullscreen(false)
|
||||||
|
.min_inner_size(600.0, 520.0);
|
||||||
|
|
||||||
|
match Config::verge().latest().window_size_position.clone() {
|
||||||
|
Some(size_pos) if size_pos.len() == 4 => {
|
||||||
|
let size = (size_pos[0], size_pos[1]);
|
||||||
|
let pos = (size_pos[2], size_pos[3]);
|
||||||
|
let w = size.0.clamp(600.0, f64::INFINITY);
|
||||||
|
let h = size.1.clamp(520.0, f64::INFINITY);
|
||||||
|
builder = builder.inner_size(w, h).position(pos.0, pos.1);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
builder = builder.inner_size(800.0, 636.0).center();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
builder = builder.inner_size(800.0, 642.0).center();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
builder = builder.inner_size(800.0, 642.0).center();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use window_shadows::set_shadow;
|
||||||
|
|
||||||
|
match builder
|
||||||
|
.decorations(false)
|
||||||
|
.transparent(true)
|
||||||
|
.visible(false)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(win) => {
|
||||||
|
log::trace!("try to calculate the monitor size");
|
||||||
|
let center = (|| -> Result<bool> {
|
||||||
|
let mut center = false;
|
||||||
|
let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?;
|
||||||
|
let size = monitor.size();
|
||||||
|
let pos = win.outer_position()?;
|
||||||
|
|
||||||
|
if pos.x < -400
|
||||||
|
|| pos.x > (size.width - 200).try_into()?
|
||||||
|
|| pos.y < -200
|
||||||
|
|| pos.y > (size.height - 200).try_into()?
|
||||||
|
{
|
||||||
|
center = true;
|
||||||
|
}
|
||||||
|
Ok(center)
|
||||||
|
})();
|
||||||
|
|
||||||
|
if center.unwrap_or(true) {
|
||||||
|
trace_err!(win.center(), "set win center");
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("try to create window");
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
|
||||||
|
// 加点延迟避免界面闪一下
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
sleep(Duration::from_millis(888)).await;
|
||||||
|
|
||||||
|
if let Some(window) = app_handle.get_window("main") {
|
||||||
|
trace_err!(set_shadow(&window, true), "set win shadow");
|
||||||
|
trace_err!(window.show(), "set win visible");
|
||||||
|
trace_err!(window.unminimize(), "set win unminimize");
|
||||||
|
trace_err!(window.set_focus(), "set win focus");
|
||||||
|
} else {
|
||||||
|
log::error!(target: "app", "failed to create window, get_window is None")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => log::error!(target: "app", "failed to create window, {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
crate::log_err!(builder
|
||||||
|
.decorations(true)
|
||||||
|
.hidden_title(true)
|
||||||
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
crate::log_err!(builder.decorations(true).transparent(false).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save window size and position
|
||||||
|
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
|
||||||
|
let win = app_handle
|
||||||
|
.get_window("main")
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get window"))?;
|
||||||
|
|
||||||
|
let scale = win.scale_factor()?;
|
||||||
|
let size = win.inner_size()?;
|
||||||
|
let size = size.to_logical::<f64>(scale);
|
||||||
|
let pos = win.outer_position()?;
|
||||||
|
let pos = pos.to_logical::<f64>(scale);
|
||||||
|
|
||||||
|
let verge = Config::verge();
|
||||||
|
let mut verge = verge.latest();
|
||||||
|
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
|
||||||
|
|
||||||
|
if save_to_file {
|
||||||
|
verge.save_file()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
extern crate warp;
|
||||||
|
|
||||||
|
use super::resolve;
|
||||||
|
use crate::config::IVerge;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use port_scanner::local_port_available;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use warp::Filter;
|
||||||
|
|
||||||
|
/// check whether there is already exists
|
||||||
|
pub fn check_singleton() -> Result<()> {
|
||||||
|
let port = IVerge::get_singleton_port();
|
||||||
|
|
||||||
|
if !local_port_available(port) {
|
||||||
|
tauri::async_runtime::block_on(async {
|
||||||
|
let url = format!("http://127.0.0.1:{port}/commands/visible");
|
||||||
|
let resp = reqwest::get(url).await?.text().await?;
|
||||||
|
|
||||||
|
if &resp == "ok" {
|
||||||
|
bail!("app exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
log::error!("failed to setup singleton listen server");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The embed server only be used to implement singleton process
|
||||||
|
/// maybe it can be used as pac server later
|
||||||
|
pub fn embed_server(app_handle: AppHandle) {
|
||||||
|
let port = IVerge::get_singleton_port();
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let commands = warp::path!("commands" / "visible").map(move || {
|
||||||
|
resolve::create_window(&app_handle);
|
||||||
|
format!("ok")
|
||||||
|
});
|
||||||
|
|
||||||
|
warp::serve(commands).bind(([127, 0, 0, 1], port)).await;
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
///! Some config file template
|
||||||
|
|
||||||
|
/// template for new a profile item
|
||||||
|
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
|
||||||
|
|
||||||
|
proxies:
|
||||||
|
|
||||||
|
proxy-groups:
|
||||||
|
|
||||||
|
rules:
|
||||||
|
";
|
||||||
|
|
||||||
|
/// enhanced profile
|
||||||
|
pub const ITEM_MERGE: &str = "# Merge Template for clash verge
|
||||||
|
# The `Merge` format used to enhance profile
|
||||||
|
|
||||||
|
prepend-rules:
|
||||||
|
|
||||||
|
prepend-proxies:
|
||||||
|
|
||||||
|
prepend-proxy-groups:
|
||||||
|
|
||||||
|
append-rules:
|
||||||
|
|
||||||
|
append-proxies:
|
||||||
|
|
||||||
|
append-proxy-groups:
|
||||||
|
";
|
||||||
|
|
||||||
|
/// enhanced profile
|
||||||
|
pub const ITEM_SCRIPT: &str = "// Define the `main` function
|
||||||
|
|
||||||
|
function main(params) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
";
|
@ -0,0 +1,69 @@
|
|||||||
|
#![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());
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"package": {
|
||||||
|
"productName": "Clash Verge",
|
||||||
|
"version": "1.3.8"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"distDir": "../dist",
|
||||||
|
"devPath": "http://localhost:3000/",
|
||||||
|
"beforeDevCommand": "yarn run web:dev",
|
||||||
|
"beforeBuildCommand": "yarn run web:build"
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"systemTray": {
|
||||||
|
"iconPath": "icons/tray-icon.ico",
|
||||||
|
"iconAsTemplate": true
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"identifier": "top.gydi.clashverge",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon-new.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"resources": ["resources"],
|
||||||
|
"externalBin": ["sidecar/clash", "sidecar/clash-meta"],
|
||||||
|
"copyright": "© 2022 zzzgydi All Rights Reserved",
|
||||||
|
"category": "DeveloperTool",
|
||||||
|
"shortDescription": "A Clash GUI based on tauri.",
|
||||||
|
"longDescription": "A Clash GUI based on tauri.",
|
||||||
|
"deb": {
|
||||||
|
"depends": ["openssl"]
|
||||||
|
},
|
||||||
|
"macOS": {
|
||||||
|
"frameworks": [],
|
||||||
|
"minimumSystemVersion": "",
|
||||||
|
"exceptionDomain": "",
|
||||||
|
"signingIdentity": null,
|
||||||
|
"entitlements": null
|
||||||
|
},
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": "",
|
||||||
|
"wix": {
|
||||||
|
"language": ["zh-CN", "en-US", "ru-RU"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"updater": {
|
||||||
|
"active": true,
|
||||||
|
"endpoints": [
|
||||||
|
"https://ghproxy.com/https://github.com/zzzgydi/clash-verge/releases/download/updater/update-proxy.json",
|
||||||
|
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json"
|
||||||
|
],
|
||||||
|
"dialog": false,
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"
|
||||||
|
},
|
||||||
|
"allowlist": {
|
||||||
|
"shell": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"process": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"globalShortcut": {
|
||||||
|
"all": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"windows": [],
|
||||||
|
"security": {
|
||||||
|
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 57 KiB |