Compare commits
233 commits
Author | SHA1 | Date | |
---|---|---|---|
|
9df1115380 | ||
|
f22e360cbb | ||
|
67769af6f4 | ||
|
b1a9a1d6d9 | ||
|
cf0606ecb7 | ||
|
7287edcd6f | ||
|
e0d26203dd | ||
|
7e3a85e9da | ||
|
5a0fed9c93 | ||
|
1f1e743912 | ||
|
b4301ed0d5 | ||
|
b5391560fc | ||
|
718989cbcf | ||
|
d0aee76962 | ||
|
fb08af96bd | ||
|
510a0c5e70 | ||
|
5fe2be031f | ||
|
2ba3aaba47 | ||
|
ad8903991c | ||
|
3e5624c570 | ||
|
f5ee6f3537 | ||
|
afc77e7adc | ||
|
024f42fce6 | ||
|
8a5f12b97c | ||
|
954b21cf39 | ||
|
74d095774d | ||
|
17a2722e6d | ||
|
c843bddbfe | ||
|
3f22a49755 | ||
|
7af2ffcebf | ||
|
de90c959e0 | ||
|
9987dc1eb4 | ||
|
3efd575dd2 | ||
|
f4c7b17a87 | ||
|
16d80718cb | ||
|
ad228d53b7 | ||
|
15ee1e531b | ||
|
1c8fb3392a | ||
|
8647866a32 | ||
|
23351c4f1c | ||
|
1367c304cf | ||
|
26d6bcb074 | ||
|
b0d651ece1 | ||
|
b6d50ba6a4 | ||
|
b3ab6a9166 | ||
|
f39a5ac9c2 | ||
|
38a9a9240d | ||
|
241b22a465 | ||
|
741abc0366 | ||
|
7854775de5 | ||
|
e62eaa6b4b | ||
|
b4cce23ef4 | ||
|
2bcaf90fc8 | ||
|
96ffbe2f84 | ||
|
6f5acee1c3 | ||
|
54e491d8bf | ||
|
ab6374e278 | ||
|
2fda4c9f67 | ||
|
5138a45b0f | ||
|
b224d4fa8a | ||
|
a552e44483 | ||
|
0cf3bba118 | ||
|
2c48ea3508 | ||
|
b9b6212b75 | ||
|
b978aaec21 | ||
|
af704681d9 | ||
|
1443ddfe6c | ||
|
54457a3e1b | ||
|
bf180e6a2c | ||
|
864a5820c9 | ||
|
4d3ca49c3f | ||
|
c49c3cf7f0 | ||
|
5d5ab57469 | ||
|
31978d8de0 | ||
|
e8eb68bf24 | ||
|
9ea08f4fed | ||
|
fe078a5c5b | ||
|
61933954f3 | ||
|
4c243638cb | ||
|
02ba04b5d8 | ||
|
4f158a4829 | ||
|
177a22df59 | ||
|
6b0ca2966e | ||
|
aadfaf7150 | ||
|
b307b9a66b | ||
|
6c1ab6002d | ||
|
9638eefc91 | ||
|
9e9c4ad587 | ||
|
ce231431b9 | ||
|
06e1e14e02 | ||
|
416e7884f5 | ||
|
d579222007 | ||
|
30243c84cd | ||
|
3557a77645 | ||
|
97be28638b | ||
|
aba0826c38 | ||
|
f032228d0e | ||
|
6cf174c5ed | ||
|
c2109d245f | ||
|
6a9745171e | ||
|
f9a68e8b23 | ||
|
6e391df5ee | ||
|
f5edca94d3 | ||
|
60046abec3 | ||
|
cafc2060b8 | ||
|
b1f45752cf | ||
|
ed17551170 | ||
|
ef5adab638 | ||
|
fb653ff99d | ||
|
78fc47a9c4 | ||
|
2a124cea61 | ||
|
4c7cc563dc | ||
|
6114af4f93 | ||
|
8c31629655 | ||
|
03c8a8edb2 | ||
|
3eeaee154f | ||
|
8cf8fa7c80 | ||
|
6b4f6fc71e | ||
|
30c2680b6f | ||
|
fb7b1800cc | ||
|
ff573bf377 | ||
|
0a33bb861e | ||
|
728756289b | ||
|
56ccd3a0ac | ||
|
66f3f0ba07 | ||
|
af5e0d589e | ||
|
533dc99e7d | ||
|
fc5ca965ba | ||
|
9c4a46bcdb | ||
|
52658886e7 | ||
|
8174ab7616 | ||
|
2b6acedae1 | ||
|
d00fe9c5f4 | ||
|
88aa270728 | ||
|
4ae409c7f4 | ||
|
9a29c9abdd | ||
|
66d93ea037 | ||
|
6c0066dbfb | ||
|
4fde644733 | ||
|
e7841c60df | ||
|
94f647b24a | ||
|
630249d22a | ||
|
db99b4cb54 | ||
|
c77db23586 | ||
|
daf66bcec4 | ||
|
8caf36349f | ||
|
6934de58e5 | ||
|
54a5007c01 | ||
|
e25a455698 | ||
|
ab429dfeb6 | ||
|
c5289dc0e8 | ||
|
d191877002 | ||
|
4d979160c2 | ||
|
d00e8f6e19 | ||
|
91b77e5237 | ||
|
5b8c246d53 | ||
|
b374b9b91c | ||
|
403717117e | ||
|
027295d995 | ||
|
c9b7eccbc1 | ||
|
2b6d9348cd | ||
|
692f8c8454 | ||
|
6783355c4d | ||
|
fb9cca1e99 | ||
|
eb770ede1a | ||
|
2643e853af | ||
|
b79456e91b | ||
|
ac66c086f8 | ||
|
ebccf401dd | ||
|
66494845b7 | ||
|
e6c36ad602 | ||
|
26379182db | ||
|
bba03d14d4 | ||
|
23b728a762 | ||
|
819c5207d2 | ||
|
311358544e | ||
|
4480ecc96d | ||
|
6c5f70a205 | ||
|
99adfb4a9e | ||
|
7909cf4067 | ||
|
780ab20aeb | ||
|
73119bb7c5 | ||
|
f20f0f064e | ||
|
1b44ae098c | ||
|
453c230716 | ||
|
439d885ee1 | ||
|
43dee3ef76 | ||
|
c71ba6ff8d | ||
|
fb7a36eb73 | ||
|
e7f294a065 | ||
|
d5037f180e | ||
|
e90158809a | ||
|
0cb802ed9a | ||
|
d0b47204f4 | ||
|
351cb391e5 | ||
|
051be927cd | ||
|
8bad2c2113 | ||
|
2bcf6fb3eb | ||
|
d1ba0ed2b2 | ||
|
6e421e60c5 | ||
|
8385050804 | ||
|
bfe4f08232 | ||
|
132f914b0d | ||
|
97d82b03ab | ||
|
f06fa3f9b7 | ||
|
6337788a22 | ||
|
024db4358b | ||
|
173f35487e | ||
|
74cbe82dd1 | ||
|
a8c30d30a9 | ||
|
1b7a52d5af | ||
|
e5109789bf | ||
|
4d2b35e09d | ||
|
5c5177ec57 | ||
|
d93b00cd15 | ||
|
39ade59174 | ||
|
0309c815b9 | ||
|
ce613098db | ||
|
ab34044196 | ||
|
17f724748f | ||
|
f3a917b5e7 | ||
|
376011ea08 | ||
|
2ce7624c14 | ||
|
c62dddd5b9 | ||
|
490ba9f140 | ||
|
f76890cc56 | ||
|
e1c8f1fed9 | ||
|
6e19a4ab8b | ||
|
b156523a7f | ||
|
d20b745ae5 | ||
|
c51e9e6b2c | ||
|
1a31fa9067 | ||
|
558b8499af |
111 changed files with 5178 additions and 2773 deletions
.github
ISSUE_TEMPLATE
workflows
scripts
src-tauri
src
assets/styles
components
base
connection
layout
layout-control.tsxlayout-item.tsxlayout-traffic.tsxupdate-button.tsxuse-custom-theme.tsuse-log-setup.ts
log
profile
editor-viewer.tsxenhanced.tsxinfo-viewer.tsxprofile-box.tsxprofile-item.tsxprofile-more.tsxprofile-new.tsxprofile-viewer.tsx
proxy
provider-button.tsxproxy-groups.tsxproxy-item-mini.tsxproxy-item.tsxproxy-render.tsxuse-head-state.tsuse-render-list.tsuse-window-width.ts
rule
setting
hooks
locales
main.tsxpages
services
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -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.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -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.
|
64
.github/workflows/alpha.yml
vendored
64
.github/workflows/alpha.yml
vendored
|
@ -16,19 +16,15 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
os: [windows-latest, ubuntu-20.04, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
@ -36,33 +32,39 @@ jobs:
|
|||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
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: matrix.os == 'ubuntu-latest'
|
||||
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
|
||||
|
||||
- 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-
|
||||
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
|
||||
yarn run check
|
||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check --force
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
|
@ -75,13 +77,17 @@ jobs:
|
|||
releaseName: "Clash Verge Alpha"
|
||||
releaseBody: "Alpha Version (include debug)"
|
||||
releaseDraft: false
|
||||
includeDebug: ${{ github.event.inputs.debug }}
|
||||
prerelease: true
|
||||
includeDebug: ${{ github.event.inputs.debug }}
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
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
|
||||
|
|
60
.github/workflows/ci.yml
vendored
60
.github/workflows/ci.yml
vendored
|
@ -1,6 +1,7 @@
|
|||
name: Release CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
@ -15,19 +16,13 @@ jobs:
|
|||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: |
|
||||
startsWith(github.repository, 'zzzgydi') &&
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
@ -35,32 +30,20 @@ jobs:
|
|||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
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
|
||||
|
||||
- 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-
|
||||
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
|
||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
|
@ -79,7 +62,7 @@ jobs:
|
|||
prerelease: true
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
# rebuild with env settings
|
||||
run: |
|
||||
yarn build
|
||||
|
@ -92,29 +75,22 @@ jobs:
|
|||
|
||||
release-update:
|
||||
needs: release
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
startsWith(github.repository, 'zzzgydi') &&
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Yarn install
|
||||
run: yarn install
|
||||
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
|
||||
- name: Release updater file
|
||||
run: yarn run updater
|
||||
|
|
6
.github/workflows/compatible.yml
vendored
6
.github/workflows/compatible.yml
vendored
|
@ -2,9 +2,9 @@ name: Compatible CI
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
# push:
|
||||
# tags:
|
||||
# - v**
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
|
107
.github/workflows/meta.yml
vendored
Normal file
107
.github/workflows/meta.yml
vendored
Normal file
|
@ -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
|
38
.github/workflows/test.yml
vendored
38
.github/workflows/test.yml
vendored
|
@ -35,14 +35,10 @@ jobs:
|
|||
echo ${{ github.event.inputs.os }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
@ -50,32 +46,20 @@ jobs:
|
|||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
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 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-
|
||||
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
|
||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
|
@ -84,3 +68,9 @@ jobs:
|
|||
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
|
||||
|
|
21
.github/workflows/updater.yml
vendored
21
.github/workflows/updater.yml
vendored
|
@ -4,27 +4,20 @@ on: workflow_dispatch
|
|||
|
||||
jobs:
|
||||
release-update:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Yarn install
|
||||
run: yarn install
|
||||
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
|
||||
- name: Release updater file
|
||||
run: yarn run updater
|
||||
|
|
60
README.md
60
README.md
|
@ -17,17 +17,69 @@ A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="h
|
|||
- Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core.
|
||||
- System proxy setting and guard.
|
||||
|
||||
## Promotion
|
||||
|
||||
[狗狗加速 —— 技术流机场 Doggygo VPN](https://dg1.top)
|
||||
|
||||
- High-performance overseas VPN, free trial, discounted packages, unlock streaming media, the world's first to support Hysteria protocol.
|
||||
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
||||
- 使用 Clash Verge 专属邀请链接注册送 15 天,每天 1G 流量免费试用:https://panel.dg1.top/#/register?code=sFCDayZf
|
||||
|
||||
<details>
|
||||
<summary>Promotion Detail</summary>
|
||||
|
||||
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
||||
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
||||
- 海外团队,无跑路风险,高达 50% 返佣
|
||||
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
|
||||
- 全球首家 Hysteria 协议机场,将在今年 10 月上线更快的 `tuic` 协议(Clash Verge 客户端最佳搭配)
|
||||
- 解锁流媒体及 ChatGPT
|
||||
- 官网:https://dg1.top
|
||||
|
||||
</details>
|
||||
|
||||
<br />
|
||||
|
||||
[EEVPN —— 海外运营机场 ※ 支持 ChatGPT](https://www.eejsq.net/#/register?code=yRr6qBO3)
|
||||
|
||||
- 年付低至 9.99 元,价格低,速度不减
|
||||
|
||||
<details>
|
||||
<summary>Promotion Detail</summary>
|
||||
|
||||
- 中国大陆 BGP 网络接入
|
||||
- IEPL 专线网络
|
||||
- 最高 2500Mbps 速率可用
|
||||
- 不限制在线客户端
|
||||
- 解锁流媒体及 ChatGPT
|
||||
- 海外运营 数据安全
|
||||
|
||||
</details>
|
||||
|
||||
## Install
|
||||
|
||||
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
||||
|
||||
- [Windows x64](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64_en-US.msi)
|
||||
- [macOS intel](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64.dmg)
|
||||
- [macOS arm](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_aarch64.dmg)
|
||||
- [Linux AppImage](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.AppImage)
|
||||
- [Linux deb](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.deb)
|
||||
- [Fedora Linux](https://github.com/zzzgydi/clash-verge/issues/352)
|
||||
|
||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
||||
|
||||
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
|
||||
|
||||
open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/Clash\ Verge.app`
|
||||
|
||||
## Development
|
||||
|
||||
You should install Rust and Nodejs, see [here](https://tauri.studio/docs/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||
|
||||
```shell
|
||||
yarn install
|
||||
|
@ -36,6 +88,9 @@ yarn install
|
|||
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
|
||||
|
||||
```shell
|
||||
# force update to latest version
|
||||
# yarn run check --force
|
||||
|
||||
yarn run check
|
||||
```
|
||||
|
||||
|
@ -43,6 +98,9 @@ Then run
|
|||
|
||||
```shell
|
||||
yarn dev
|
||||
|
||||
# run it in another way if app instance exists
|
||||
yarn dev:diff
|
||||
```
|
||||
|
||||
Or you can build it
|
||||
|
|
197
UPDATELOG.md
197
UPDATELOG.md
|
@ -1,3 +1,200 @@
|
|||
## 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
|
||||
|
|
19
package.json
19
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.8",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
|
@ -20,18 +20,21 @@
|
|||
"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.2.0",
|
||||
"@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": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"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",
|
||||
|
@ -41,12 +44,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^5.0.3",
|
||||
"@tauri-apps/cli": "^1.2.0",
|
||||
"@tauri-apps/cli": "^1.3.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@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",
|
||||
|
@ -58,7 +61,7 @@
|
|||
"pretty-quick": "^3.1.3",
|
||||
"sass": "^1.54.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.9",
|
||||
"vite": "^3.2.5",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^2.2.1"
|
||||
},
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
/**
|
||||
* Build and upload assets for macOS(aarch)
|
||||
* 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() {
|
||||
|
@ -25,6 +32,14 @@ async function resolve() {
|
|||
|
||||
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);
|
||||
|
@ -48,7 +63,7 @@ async function resolve() {
|
|||
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: `v${version}`,
|
||||
tag,
|
||||
});
|
||||
|
||||
if (!release.id) throw new Error("failed to find the release");
|
||||
|
|
|
@ -15,28 +15,29 @@ const SIDECAR_HOST = execSync("rustc -vV")
|
|||
.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 = "2022.08.26";
|
||||
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-armv8",
|
||||
"linux-arm64": "clash-linux-arm64",
|
||||
};
|
||||
|
||||
/* ======= clash meta ======= */
|
||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/Clash.Meta/releases/download/`;
|
||||
const META_VERSION = "v1.13.2";
|
||||
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",
|
||||
"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",
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -45,10 +46,10 @@ const META_MAP = {
|
|||
|
||||
const { platform, arch } = process;
|
||||
if (!CLASH_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(`clash unsupport platform "${platform}-${arch}"`);
|
||||
throw new Error(`clash unsupported platform "${platform}-${arch}"`);
|
||||
}
|
||||
if (!META_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(`clash meta unsupport platform "${platform}-${arch}"`);
|
||||
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`);
|
||||
}
|
||||
|
||||
function clash() {
|
||||
|
@ -69,6 +70,24 @@ function clash() {
|
|||
};
|
||||
}
|
||||
|
||||
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";
|
||||
|
@ -103,36 +122,61 @@ async function resolveSidecar(binInfo) {
|
|||
const tempExe = path.join(tempDir, exeFile);
|
||||
|
||||
await fs.mkdirp(tempDir);
|
||||
if (!(await fs.pathExists(tempZip))) await downloadFile(downloadURL, tempZip);
|
||||
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);
|
||||
readStream
|
||||
.pipe(zlib.createGunzip())
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
console.log(`[INFO]: ${name} gunzip finished`);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
console.log(`[INFO]: ${name} chmod binary finished`);
|
||||
})
|
||||
.on("error", (error) => {
|
||||
console.error(`[ERROR]: ${name} gz failed`, error.message);
|
||||
throw error;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -242,21 +286,21 @@ const resolveUninstall = () =>
|
|||
const resolveMmdb = () =>
|
||||
resolveResource({
|
||||
file: "Country.mmdb",
|
||||
downloadURL: `https://github.com/Dreamacro/maxmind-geoip/releases/download/20221112/Country.mmdb`,
|
||||
downloadURL: `https://github.com/Dreamacro/maxmind-geoip/releases/download/20230812/Country.mmdb`,
|
||||
});
|
||||
const resolveGeosite = () =>
|
||||
resolveResource({
|
||||
file: "geosite.dat",
|
||||
downloadURL: `https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/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/Loyalsoldier/geoip/releases/latest/download/geoip.dat`,
|
||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`,
|
||||
});
|
||||
|
||||
const tasks = [
|
||||
{ name: "clash", func: () => resolveSidecar(clash()), retry: 5 },
|
||||
{ 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 },
|
||||
|
@ -277,7 +321,8 @@ async function runTask() {
|
|||
await task.func();
|
||||
break;
|
||||
} catch (err) {
|
||||
console.error(`[ERROR]: task::${task.name} try ${i} == `, err.message);
|
||||
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message);
|
||||
if (i === task.retry - 1) throw err;
|
||||
}
|
||||
}
|
||||
return runTask();
|
||||
|
|
|
@ -43,6 +43,7 @@ async function resolveUpdater() {
|
|||
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
|
||||
|
@ -68,12 +69,14 @@ async function resolveUpdater() {
|
|||
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)
|
||||
|
@ -117,10 +120,7 @@ async function resolveUpdater() {
|
|||
|
||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||
if (value.url) {
|
||||
updateDataNew.platforms[key].url = value.url.replace(
|
||||
"https://github.com/",
|
||||
"https://hub.fastgit.xyz/"
|
||||
);
|
||||
updateDataNew.platforms[key].url = "https://ghproxy.com/" + value.url;
|
||||
} else {
|
||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||
}
|
||||
|
|
2284
src-tauri/Cargo.lock
generated
2284
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -16,43 +16,51 @@ tauri-build = { version = "1", features = [] }
|
|||
warp = "0.3"
|
||||
which = "4.2.2"
|
||||
anyhow = "1.0"
|
||||
dirs = "4.0.0"
|
||||
open = "3.0.3"
|
||||
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.26.2"
|
||||
sysproxy = "0.1"
|
||||
sysinfo = "0.29"
|
||||
sysproxy = "0.3"
|
||||
rquickjs = "0.1.7"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
auto-launch = "0.4"
|
||||
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"] }
|
||||
tauri = { version = "1.1.1", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
tauri-runtime-wry = { version = "0.12" }
|
||||
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 = "0.2.1"
|
||||
runas = "1.1.0"
|
||||
deelevate = "0.2.0"
|
||||
winreg = { version = "0.10", features = ["transactions"] }
|
||||
windows-sys = { version = "0.36", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
|
||||
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 = []
|
||||
debug-yml = []
|
||||
default-meta = []
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
|
BIN
src-tauri/icons/win-tray-icon-activated.png
Normal file
BIN
src-tauri/icons/win-tray-icon-activated.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.3 KiB |
BIN
src-tauri/icons/win-tray-icon.png
Normal file
BIN
src-tauri/icons/win-tray-icon.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.5 KiB |
|
@ -175,6 +175,15 @@ 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> {
|
||||
|
@ -202,6 +211,13 @@ pub fn open_app_dir() -> CmdResult<()> {
|
|||
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())?;
|
||||
|
@ -213,6 +229,17 @@ 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::*;
|
||||
|
|
|
@ -24,11 +24,23 @@ impl IClashTemp {
|
|||
pub fn template() -> Self {
|
||||
let mut map = Mapping::new();
|
||||
|
||||
map.insert("mixed-port".into(), 7890.into());
|
||||
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(), "127.0.0.1:9090".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)
|
||||
|
|
|
@ -26,7 +26,7 @@ macro_rules! draft_define {
|
|||
}
|
||||
|
||||
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
||||
MutexGuard::map(self.inner.lock(), |mut inner| {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
inner.1 = Some(inner.0.clone());
|
||||
}
|
||||
|
|
|
@ -190,7 +190,7 @@ impl PrfItem {
|
|||
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().no_proxy();
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
// 使用软件自己的代理
|
||||
if self_proxy {
|
||||
|
@ -282,8 +282,11 @@ impl PrfItem {
|
|||
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) //
|
||||
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") {
|
||||
|
@ -301,7 +304,7 @@ impl PrfItem {
|
|||
extra,
|
||||
option,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(data),
|
||||
file_data: Some(data.into()),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ impl IProfiles {
|
|||
}
|
||||
// compatible with the old old old version
|
||||
profiles.items.as_mut().map(|items| {
|
||||
for mut item in items.iter_mut() {
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d"));
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ impl IProfiles {
|
|||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
valid: Some(vec!["dns".into()]),
|
||||
valid: Some(vec!["dns".into(), "sub-rules".into(), "unified-delay".into()]),
|
||||
items: Some(vec![]),
|
||||
..Self::default()
|
||||
}
|
||||
|
@ -138,9 +138,9 @@ impl IProfiles {
|
|||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.context(format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.write(file_data.as_bytes())
|
||||
.context(format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
}
|
||||
|
||||
if self.items.is_none() {
|
||||
|
@ -155,7 +155,7 @@ impl IProfiles {
|
|||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
|
||||
for mut each in items.iter_mut() {
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == Some(uid.clone()) {
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
|
@ -189,7 +189,7 @@ impl IProfiles {
|
|||
if let Some(items) = self.items.as_mut() {
|
||||
let some_uid = Some(uid.clone());
|
||||
|
||||
for mut each in items.iter_mut() {
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == some_uid {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
|
@ -207,9 +207,9 @@ impl IProfiles {
|
|||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.context(format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.write(file_data.as_bytes())
|
||||
.context(format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
|
@ -15,4 +15,17 @@ impl IRuntime {
|
|||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// 这里只更改 allow-lan | ipv6 | log-level
|
||||
pub fn patch_config(&mut self, patch: Mapping) {
|
||||
if let Some(config) = self.config.as_mut() {
|
||||
["allow-lan", "ipv6", "log-level"]
|
||||
.into_iter()
|
||||
.for_each(|key| {
|
||||
if let Some(value) = patch.get(key).to_owned() {
|
||||
config.insert(key.into(), value.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
|
@ -8,6 +9,10 @@ pub struct IVerge {
|
|||
/// app listening port for app singleton
|
||||
pub app_singleton_port: Option<u16>,
|
||||
|
||||
/// app log level
|
||||
/// silent | error | warn | info | debug | trace
|
||||
pub app_log_level: Option<String>,
|
||||
|
||||
// i18n
|
||||
pub language: Option<String>,
|
||||
|
||||
|
@ -21,6 +26,9 @@ pub struct IVerge {
|
|||
/// enable traffic graph default is true
|
||||
pub traffic_graph: Option<bool>,
|
||||
|
||||
/// show memory info (only for Clash Meta)
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
|
||||
/// clash tun mode
|
||||
pub enable_tun_mode: Option<bool>,
|
||||
|
||||
|
@ -66,8 +74,22 @@ pub struct IVerge {
|
|||
/// 默认的延迟测试连接
|
||||
pub default_latency_test: Option<String>,
|
||||
|
||||
/// 支持关闭字段过滤,避免meta的新字段都被过滤掉,默认为真
|
||||
pub enable_clash_fields: Option<bool>,
|
||||
|
||||
/// 是否使用内部的脚本支持,默认为真
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
|
||||
/// proxy 页面布局 列数
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
|
||||
/// 日志清理
|
||||
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
||||
pub auto_log_clean: Option<i32>,
|
||||
|
||||
/// window size and position
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_size_position: Option<Vec<f64>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
|
@ -99,11 +121,18 @@ impl IVerge {
|
|||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
clash_core: Some("clash".into()),
|
||||
language: Some("en".into()),
|
||||
clash_core: match cfg!(feature = "default-meta") {
|
||||
false => Some("clash".into()),
|
||||
true => Some("clash-meta".into()),
|
||||
},
|
||||
language: match cfg!(feature = "default-meta") {
|
||||
false => Some("en".into()),
|
||||
true => Some("zh".into()),
|
||||
},
|
||||
theme_mode: Some("system".into()),
|
||||
theme_blur: Some(false),
|
||||
traffic_graph: Some(true),
|
||||
enable_memory_usage: Some(true),
|
||||
enable_auto_launch: Some(false),
|
||||
enable_silent_start: Some(false),
|
||||
enable_system_proxy: Some(false),
|
||||
|
@ -111,6 +140,8 @@ impl IVerge {
|
|||
proxy_guard_duration: Some(30),
|
||||
auto_close_connection: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
enable_clash_fields: Some(true),
|
||||
auto_log_clean: Some(3),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
@ -131,10 +162,12 @@ impl IVerge {
|
|||
};
|
||||
}
|
||||
|
||||
patch!(app_log_level);
|
||||
patch!(language);
|
||||
patch!(theme_mode);
|
||||
patch!(theme_blur);
|
||||
patch!(traffic_graph);
|
||||
patch!(enable_memory_usage);
|
||||
|
||||
patch!(enable_tun_mode);
|
||||
patch!(enable_service_mode);
|
||||
|
@ -153,6 +186,10 @@ impl IVerge {
|
|||
patch!(auto_close_connection);
|
||||
patch!(default_latency_test);
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(enable_clash_fields);
|
||||
patch!(auto_log_clean);
|
||||
patch!(window_size_position);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
|
@ -167,4 +204,21 @@ impl IVerge {
|
|||
Err(_) => SERVER_PORT, // 这里就不log错误了
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取日志等级
|
||||
pub fn get_log_level(&self) -> LevelFilter {
|
||||
if let Some(level) = self.app_log_level.as_ref() {
|
||||
match level.to_lowercase().as_str() {
|
||||
"silent" => LevelFilter::Off,
|
||||
"error" => LevelFilter::Error,
|
||||
"warn" => LevelFilter::Warn,
|
||||
"info" => LevelFilter::Info,
|
||||
"debug" => LevelFilter::Debug,
|
||||
"trace" => LevelFilter::Trace,
|
||||
_ => LevelFilter::Info,
|
||||
}
|
||||
} else {
|
||||
LevelFilter::Info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::config::Config;
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -36,6 +37,32 @@ pub async fn patch_configs(config: &Mapping) -> Result<()> {
|
|||
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() };
|
||||
|
@ -55,10 +82,13 @@ fn clash_client_info() -> Result<(String, HeaderMap)> {
|
|||
|
||||
/// 缩短clash的日志
|
||||
pub fn parse_log(log: String) -> String {
|
||||
if log.starts_with("time=") {
|
||||
if log.starts_with("time=") && log.len() > 33 {
|
||||
return (&log[33..]).to_owned();
|
||||
}
|
||||
(&log[9..]).to_owned()
|
||||
if log.len() > 9 {
|
||||
return (&log[9..]).to_owned();
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
/// 缩短clash -t的错误输出
|
||||
|
|
|
@ -68,6 +68,10 @@ impl CoreManager {
|
|||
|
||||
if !output.status.success() {
|
||||
let error = clash_api::parse_check_output(output.stdout.clone());
|
||||
let error = match error.len() > 0 {
|
||||
true => error,
|
||||
false => output.stdout.clone(),
|
||||
};
|
||||
Logger::global().set_log(output.stdout);
|
||||
bail!("{error}");
|
||||
}
|
||||
|
@ -136,6 +140,7 @@ impl CoreManager {
|
|||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("clash".into());
|
||||
let is_clash = clash_core == "clash";
|
||||
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
|
||||
|
@ -161,18 +166,23 @@ impl CoreManager {
|
|||
|
||||
let mut sidecar = self.sidecar.lock();
|
||||
*sidecar = Some(cmd_child);
|
||||
drop(sidecar);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
let stdout = clash_api::parse_log(line.clone());
|
||||
log::info!(target: "app", "[clash]: {stdout}");
|
||||
if is_clash {
|
||||
let stdout = clash_api::parse_log(line.clone());
|
||||
log::info!(target: "app", "[clash]: {stdout}");
|
||||
} else {
|
||||
log::info!(target: "app", "[clash]: {line}");
|
||||
};
|
||||
Logger::global().set_log(line);
|
||||
}
|
||||
CommandEvent::Stderr(err) => {
|
||||
let stdout = clash_api::parse_log(err.clone());
|
||||
log::error!(target: "app", "[clash]: {stdout}");
|
||||
// let stdout = clash_api::parse_log(err.clone());
|
||||
log::error!(target: "app", "[clash]: {err}");
|
||||
Logger::global().set_log(err);
|
||||
}
|
||||
CommandEvent::Error(err) => {
|
||||
|
@ -181,6 +191,7 @@ impl CoreManager {
|
|||
}
|
||||
CommandEvent::Terminated(_) => {
|
||||
log::info!(target: "app", "clash core terminated");
|
||||
let _ = CoreManager::global().recover_core();
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
|
@ -191,6 +202,40 @@ impl CoreManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// 重启内核
|
||||
pub fn recover_core(&'static self) -> Result<()> {
|
||||
// 服务模式不管
|
||||
#[cfg(target_os = "windows")]
|
||||
if *self.use_service_mode.lock() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 清空原来的sidecar值
|
||||
if let Some(sidecar) = self.sidecar.lock().take() {
|
||||
let _ = sidecar.kill();
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 6秒之后再查看服务是否正常 (时间随便搞的)
|
||||
// terminated 可能是切换内核 (切换内核已经有500ms的延迟)
|
||||
sleep(Duration::from_millis(6666)).await;
|
||||
|
||||
if self.sidecar.lock().is_none() {
|
||||
log::info!(target: "app", "recover clash core");
|
||||
|
||||
// 重新启动app
|
||||
if let Err(err) = self.run_core().await {
|
||||
log::error!(target: "app", "failed to recover clash core");
|
||||
log::error!(target: "app", "{err}");
|
||||
|
||||
let _ = self.recover_core();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止核心运行
|
||||
pub fn stop_core(&self) -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
|
@ -4,7 +4,7 @@ use once_cell::sync::OnceCell;
|
|||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::{AppHandle, GlobalShortcutManager};
|
||||
use tauri_runtime_wry::wry::application::accelerator::Accelerator;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
|
@ -76,6 +76,7 @@ impl Hotkey {
|
|||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
"open_dashboard" => || feat::open_dashboard(),
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
|
|
82
src-tauri/src/core/manager.rs
Normal file
82
src-tauri/src/core/manager.rs
Normal file
|
@ -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)) }
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ mod core;
|
|||
pub mod handle;
|
||||
pub mod hotkey;
|
||||
pub mod logger;
|
||||
pub mod manager;
|
||||
pub mod sysopt;
|
||||
pub mod timer;
|
||||
pub mod tray;
|
||||
|
|
|
@ -25,7 +25,7 @@ pub struct Sysopt {
|
|||
#[cfg(target_os = "windows")]
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;<local>";
|
||||
#[cfg(target_os = "linux")]
|
||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1";
|
||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,::1";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>";
|
||||
|
||||
|
@ -171,6 +171,24 @@ impl Sysopt {
|
|||
})()
|
||||
.unwrap_or(app_path);
|
||||
|
||||
// fix #403
|
||||
#[cfg(target_os = "linux")]
|
||||
let app_path = {
|
||||
use crate::core::handle::Handle;
|
||||
use tauri::Manager;
|
||||
|
||||
let handle = Handle::global();
|
||||
match handle.app_handle.lock().as_ref() {
|
||||
Some(app_handle) => {
|
||||
let appimage = app_handle.env().appimage;
|
||||
appimage
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
.unwrap_or(app_path)
|
||||
}
|
||||
None => app_path,
|
||||
}
|
||||
};
|
||||
|
||||
let auto = AutoLaunchBuilder::new()
|
||||
.set_app_name(app_name)
|
||||
.set_app_path(&app_path)
|
||||
|
@ -182,13 +200,22 @@ impl Sysopt {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// macos每次启动都更新登录项,避免重复设置登录项
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = auto.disable();
|
||||
{
|
||||
if enable && !auto.is_enabled().unwrap_or(false) {
|
||||
// 避免重复设置登录项
|
||||
let _ = auto.disable();
|
||||
auto.enable()?;
|
||||
} else if !enable {
|
||||
let _ = auto.disable();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if enable {
|
||||
auto.enable()?;
|
||||
}
|
||||
|
||||
*self.auto_launch.lock() = Some(auto);
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{config::Config, feat, utils::resolve};
|
||||
use crate::{cmds, config::Config, feat, utils::resolve};
|
||||
use anyhow::Result;
|
||||
use tauri::{
|
||||
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||
|
@ -13,53 +13,81 @@ impl Tray {
|
|||
|
||||
let version = app_handle.package_info().version.to_string();
|
||||
|
||||
if zh {
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_window", "打开面板"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("rule_mode", "规则模式"))
|
||||
.add_item(CustomMenuItem::new("global_mode", "全局模式"))
|
||||
.add_item(CustomMenuItem::new("direct_mode", "直连模式"))
|
||||
.add_item(CustomMenuItem::new("script_mode", "脚本模式"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("system_proxy", "系统代理"))
|
||||
.add_item(CustomMenuItem::new("tun_mode", "TUN 模式"))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
"更多",
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("restart_clash", "重启 Clash"))
|
||||
.add_item(CustomMenuItem::new("restart_app", "重启应用"))
|
||||
.add_item(
|
||||
CustomMenuItem::new("app_version", format!("Version {version}"))
|
||||
.disabled(),
|
||||
),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", "退出").accelerator("CmdOrControl+Q"))
|
||||
} else {
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_window", "Dashboard"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("rule_mode", "Rule Mode"))
|
||||
.add_item(CustomMenuItem::new("global_mode", "Global Mode"))
|
||||
.add_item(CustomMenuItem::new("direct_mode", "Direct Mode"))
|
||||
.add_item(CustomMenuItem::new("script_mode", "Script Mode"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("system_proxy", "System Proxy"))
|
||||
.add_item(CustomMenuItem::new("tun_mode", "Tun Mode"))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
"More",
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
|
||||
.add_item(CustomMenuItem::new("restart_app", "Restart App"))
|
||||
.add_item(
|
||||
CustomMenuItem::new("app_version", format!("Version {version}"))
|
||||
.disabled(),
|
||||
),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"))
|
||||
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<()> {
|
||||
|
@ -93,6 +121,17 @@ impl Tray {
|
|||
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);
|
||||
|
||||
|
@ -110,9 +149,15 @@ impl Tray {
|
|||
"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);
|
||||
|
|
|
@ -1,33 +1,35 @@
|
|||
use serde_yaml::{Mapping, Value};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub const HANDLE_FIELDS: [&str; 9] = [
|
||||
"mode",
|
||||
"port",
|
||||
"socks-port",
|
||||
"mixed-port",
|
||||
"mode",
|
||||
"ipv6",
|
||||
"log-level",
|
||||
"allow-lan",
|
||||
"external-controller",
|
||||
"log-level",
|
||||
"ipv6",
|
||||
"secret",
|
||||
"external-controller",
|
||||
];
|
||||
|
||||
pub const DEFAULT_FIELDS: [&str; 5] = [
|
||||
"proxies",
|
||||
"proxy-groups",
|
||||
"rules",
|
||||
"proxy-providers",
|
||||
"rules",
|
||||
"rule-providers",
|
||||
];
|
||||
|
||||
pub const OTHERS_FIELDS: [&str; 21] = [
|
||||
"tun",
|
||||
pub const OTHERS_FIELDS: [&str; 30] = [
|
||||
"dns",
|
||||
"tun",
|
||||
"ebpf",
|
||||
"hosts",
|
||||
"script",
|
||||
"profile",
|
||||
"payload",
|
||||
"tunnels",
|
||||
"auto-redir",
|
||||
"experimental",
|
||||
"interface-name",
|
||||
|
@ -38,10 +40,18 @@ pub const OTHERS_FIELDS: [&str; 21] = [
|
|||
"external-ui",
|
||||
"bind-address",
|
||||
"authentication",
|
||||
"sniffer", // meta
|
||||
"sub-rules", // meta
|
||||
"geodata-mode", // meta
|
||||
"tcp-concurrent", // meta
|
||||
"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> {
|
||||
|
@ -64,7 +74,11 @@ pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping {
|
||||
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() {
|
||||
|
@ -90,7 +104,7 @@ pub fn use_lowercase(config: Mapping) -> Mapping {
|
|||
ret
|
||||
}
|
||||
|
||||
pub fn use_sort(config: Mapping) -> Mapping {
|
||||
pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
|
||||
let mut ret = Mapping::new();
|
||||
|
||||
HANDLE_FIELDS
|
||||
|
@ -103,6 +117,28 @@ pub fn use_sort(config: Mapping) -> Mapping {
|
|||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use super::{use_filter, use_lowercase};
|
||||
use serde_yaml::{self, Mapping, Sequence, Value};
|
||||
|
||||
#[allow(unused)]
|
||||
const MERGE_FIELDS: [&str; 6] = [
|
||||
"prepend-rules",
|
||||
"append-rules",
|
||||
|
@ -20,7 +19,7 @@ pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
|
|||
});
|
||||
|
||||
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
|
||||
let merge = use_filter(merge, &merge_list.collect());
|
||||
let merge = use_filter(merge, &merge_list.collect(), true);
|
||||
|
||||
["rules", "proxies", "proxy-groups"]
|
||||
.iter()
|
||||
|
|
|
@ -23,13 +23,14 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
// config.yaml 的配置
|
||||
let clash_config = { Config::clash().latest().0.clone() };
|
||||
|
||||
let (clash_core, tun_mode, enable_builtin) = {
|
||||
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(),
|
||||
verge.enable_builtin_enhanced.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),
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -58,14 +59,14 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
||||
|
||||
let valid = use_valid_fields(valid);
|
||||
config = use_filter(config, &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);
|
||||
config = use_filter(config.to_owned(), &valid, enable_filter);
|
||||
}
|
||||
ChainType::Script(script) => {
|
||||
let mut logs = vec![];
|
||||
|
@ -73,7 +74,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
match use_script(script, config.to_owned()) {
|
||||
Ok((res_config, res_logs)) => {
|
||||
exists_keys.extend(use_keys(&res_config));
|
||||
config = use_filter(res_config, &valid);
|
||||
config = use_filter(res_config, &valid, enable_filter);
|
||||
logs.extend(res_logs);
|
||||
}
|
||||
Err(err) => logs.push(("exception".into(), err.to_string())),
|
||||
|
@ -91,7 +92,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
let clash_fields = use_clash_fields();
|
||||
|
||||
// 内建脚本最后跑
|
||||
if enable_builtin.unwrap_or(true) {
|
||||
if enable_builtin {
|
||||
ChainItem::builtin()
|
||||
.into_iter()
|
||||
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||
|
@ -102,7 +103,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
match item.data {
|
||||
ChainType::Script(script) => match use_script(script, config.to_owned()) {
|
||||
Ok((res_config, _)) => {
|
||||
config = use_filter(res_config, &clash_fields);
|
||||
config = use_filter(res_config, &clash_fields, enable_filter);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "builtin script error `{err}`");
|
||||
|
@ -113,9 +114,9 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
});
|
||||
}
|
||||
|
||||
config = use_filter(config, &clash_fields);
|
||||
config = use_tun(config, tun_mode.unwrap_or(false));
|
||||
config = use_sort(config);
|
||||
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)));
|
||||
|
|
|
@ -7,8 +7,19 @@
|
|||
use crate::config::*;
|
||||
use crate::core::*;
|
||||
use crate::log_err;
|
||||
use crate::utils::resolve;
|
||||
use anyhow::{bail, Result};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use wry::application::clipboard::Clipboard;
|
||||
|
||||
// 打开面板
|
||||
pub fn open_dashboard() {
|
||||
let handle = handle::Handle::global();
|
||||
let app_handle = handle.app_handle.lock();
|
||||
if let Some(app_handle) = app_handle.as_ref() {
|
||||
resolve::create_window(app_handle);
|
||||
}
|
||||
}
|
||||
|
||||
// 重启clash
|
||||
pub fn restart_clash_core() {
|
||||
|
@ -183,6 +194,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
|||
log_err!(handle::Handle::update_systray_part());
|
||||
}
|
||||
|
||||
Config::runtime().latest().patch_config(patch);
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
} {
|
||||
Ok(()) => {
|
||||
|
@ -317,3 +330,12 @@ async fn update_core_config() -> Result<()> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// copy env variable
|
||||
pub fn copy_clash_env() {
|
||||
let port = { Config::clash().data().get_client_info().port };
|
||||
let text = format!("export https_proxy=http://127.0.0.1:{port} http_proxy=http://127.0.0.1:{port} all_proxy=socks5://127.0.0.1:{port}");
|
||||
|
||||
let mut cliboard = Clipboard::new();
|
||||
cliboard.write_text(text);
|
||||
}
|
||||
|
|
|
@ -33,8 +33,10 @@ fn main() -> std::io::Result<()> {
|
|||
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,
|
||||
|
@ -64,6 +66,8 @@ fn main() -> std::io::Result<()> {
|
|||
cmds::service::check_service,
|
||||
cmds::service::install_service,
|
||||
cmds::service::uninstall_service,
|
||||
// clash api
|
||||
cmds::clash_api_get_proxy_delay
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
@ -107,6 +111,8 @@ fn main() -> std::io::Result<()> {
|
|||
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();
|
||||
});
|
||||
|
@ -115,6 +121,20 @@ fn main() -> std::io::Result<()> {
|
|||
}
|
||||
}
|
||||
}
|
||||
#[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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ static mut RESOURCE_DIR: Option<PathBuf> = None;
|
|||
#[allow(unused)]
|
||||
static mut PORTABLE_FLAG: bool = false;
|
||||
|
||||
pub static mut APP_VERSION: &str = "v1.1.2";
|
||||
pub static mut APP_VERSION: &str = "v1.2.0";
|
||||
|
||||
/// initialize portable flag
|
||||
#[cfg(target_os = "windows")]
|
||||
|
@ -107,6 +107,7 @@ 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
|
||||
|
|
|
@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result};
|
|||
use nanoid::nanoid;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::{fs, path::PathBuf, process::Command, str::FromStr};
|
||||
use std::{fs, path::PathBuf, str::FromStr};
|
||||
|
||||
/// read data from yaml as struct T
|
||||
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
|
@ -11,19 +11,21 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
|||
}
|
||||
|
||||
let yaml_str = fs::read_to_string(&path)
|
||||
.context(format!("failed to read the file \"{}\"", path.display()))?;
|
||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
|
||||
serde_yaml::from_str::<T>(&yaml_str).context(format!(
|
||||
"failed to read the file with yaml format \"{}\"",
|
||||
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()
|
||||
.context(format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
|
||||
Ok(val
|
||||
.as_mapping()
|
||||
|
@ -45,7 +47,8 @@ pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -
|
|||
};
|
||||
|
||||
let path_str = path.as_os_str().to_string_lossy().to_string();
|
||||
fs::write(path, yaml_str.as_bytes()).context(format!("failed to save file \"{path_str}\""))
|
||||
fs::write(path, yaml_str.as_bytes())
|
||||
.with_context(|| format!("failed to save file \"{path_str}\""))
|
||||
}
|
||||
|
||||
const ALPHABET: [char; 62] = [
|
||||
|
@ -79,29 +82,18 @@ pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
|||
/// 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 Ok(code) = which::which("code") {
|
||||
let mut command = Command::new(&code);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
if let Err(err) = command.creation_flags(0x08000000).arg(&path).spawn() {
|
||||
log::error!(target: "app", "failed to open with VScode `{err}`");
|
||||
open::that(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Err(err) = command.arg(&path).spawn() {
|
||||
log::error!(target: "app", "failed to open with VScode `{err}`");
|
||||
open::that(path)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
if let Err(err) = open::with(&path, code) {
|
||||
log::error!(target: "app", "failed to open file with VScode `{err}`");
|
||||
// default open
|
||||
open::that(path)?;
|
||||
}
|
||||
|
||||
open::that(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -127,6 +119,15 @@ macro_rules! log_err {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace_err {
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// wrap the anyhow error
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
use crate::config::*;
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use chrono::{DateTime, Local};
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Config, Logger, Root};
|
||||
use log4rs::config::{Appender, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use std::fs;
|
||||
use std::fs::{self, DirEntry};
|
||||
use std::str::FromStr;
|
||||
use tauri::PackageInfo;
|
||||
|
||||
/// initialize this instance's log file
|
||||
|
@ -17,42 +18,126 @@ fn init_log() -> Result<()> {
|
|||
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);
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {M} {m}{n}";
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}";
|
||||
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(time_format));
|
||||
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)?;
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let level = LevelFilter::Debug;
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
let level = LevelFilter::Info;
|
||||
let mut logger_builder = Logger::builder();
|
||||
let mut root_builder = Root::builder();
|
||||
|
||||
let config = Config::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()
|
||||
.appenders(["file", "stdout"])
|
||||
.additive(false)
|
||||
.build("app", level),
|
||||
)
|
||||
.build(Root::builder().appender("stdout").build(LevelFilter::Info))?;
|
||||
.logger(logger_builder.additive(false).build("app", log_level))
|
||||
.build_lossy(root_builder.build(log_level));
|
||||
|
||||
log4rs::init_config(config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize all the files from resources
|
||||
/// 删除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 {
|
||||
|
@ -60,6 +145,7 @@ pub fn init_config() -> Result<()> {
|
|||
}
|
||||
|
||||
let _ = init_log();
|
||||
let _ = delete_log();
|
||||
|
||||
crate::log_err!(dirs::app_home_dir().map(|app_dir| {
|
||||
if !app_dir.exists() {
|
||||
|
@ -97,7 +183,8 @@ pub fn init_config() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// initialize app
|
||||
/// 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)?;
|
||||
|
@ -109,13 +196,47 @@ pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
|||
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
|
||||
for file in ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"].iter() {
|
||||
// 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 target_path = app_dir.join(file);
|
||||
if src_path.exists() {
|
||||
let _ = fs::copy(src_path, target_path);
|
||||
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(())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::config::Config;
|
||||
use crate::log_err;
|
||||
use crate::{core::*, utils::init, utils::server};
|
||||
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
|
||||
|
@ -13,12 +13,17 @@ pub fn resolve_setup(app: &mut App) {
|
|||
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() };
|
||||
|
@ -43,22 +48,47 @@ pub fn resolve_reset() {
|
|||
/// create main window
|
||||
pub fn create_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
trace_err!(window.unminimize(), "set win unminimize");
|
||||
trace_err!(window.show(), "set win visible");
|
||||
trace_err!(window.set_focus(), "set win focus");
|
||||
return;
|
||||
}
|
||||
|
||||
let builder = tauri::window::WindowBuilder::new(
|
||||
let mut builder = tauri::window::WindowBuilder::new(
|
||||
app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WindowUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.center()
|
||||
.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;
|
||||
|
@ -68,38 +98,82 @@ pub fn create_window(app_handle: &AppHandle) {
|
|||
match builder
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.inner_size(800.0, 636.0)
|
||||
.visible(false)
|
||||
.build()
|
||||
{
|
||||
Ok(_) => {
|
||||
let app_handle = app_handle.clone();
|
||||
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 let Some(window) = app_handle.get_window("main") {
|
||||
let _ = set_shadow(&window, true);
|
||||
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_secs(1)).await;
|
||||
sleep(Duration::from_millis(888)).await;
|
||||
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.unminimize();
|
||||
let _ = window.set_focus();
|
||||
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", "{err}"),
|
||||
Err(err) => log::error!(target: "app", "failed to create window, {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::log_err!(builder.decorations(true).inner_size(800.0, 642.0).build());
|
||||
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(false)
|
||||
.transparent(true)
|
||||
.inner_size(800.0, 636.0)
|
||||
.build());
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "1.2.0"
|
||||
"version": "1.3.8"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"shortDescription": "A Clash GUI based on tauri.",
|
||||
"longDescription": "A Clash GUI based on tauri.",
|
||||
"deb": {
|
||||
"depends": []
|
||||
"depends": ["openssl"]
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
|
@ -46,15 +46,15 @@
|
|||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "",
|
||||
"wix": {
|
||||
"language": ["zh-CN", "en-US"]
|
||||
"language": ["zh-CN", "en-US", "ru-RU"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json",
|
||||
"https://hub.fastgit.xyz/zzzgydi/clash-verge/releases/download/updater/update-proxy.json"
|
||||
"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"
|
||||
|
|
|
@ -104,3 +104,14 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.macos {
|
||||
&.layout {
|
||||
.layout__left {
|
||||
padding-top: 24px;
|
||||
}
|
||||
.layout__right .the-content {
|
||||
top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { forwardRef, ReactNode, useImperativeHandle, useState } from "react";
|
||||
import { ReactNode } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
|
@ -18,6 +18,7 @@ interface Props {
|
|||
disableCancel?: boolean;
|
||||
disableFooter?: boolean;
|
||||
contentSx?: SxProps<Theme>;
|
||||
children?: ReactNode;
|
||||
onOk?: () => void;
|
||||
onCancel?: () => void;
|
||||
onClose?: () => void;
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
import { ReactNode } from "react";
|
||||
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
||||
|
||||
function ErrorFallback({ error }: FallbackProps) {
|
||||
return (
|
||||
<div role="alert">
|
||||
<p>Something went wrong:(</p>
|
||||
<div role="alert" style={{ padding: 16 }}>
|
||||
<h4>Something went wrong:(</h4>
|
||||
|
||||
<pre>{error.message}</pre>
|
||||
|
||||
<details title="Error Stack">
|
||||
<summary>Error Stack</summary>
|
||||
<pre>{error.stack}</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const BaseErrorBoundary: React.FC = (props) => {
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const BaseErrorBoundary = (props: Props) => {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
{props.children}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material";
|
||||
import { Close, CheckCircleRounded, ErrorRounded } from "@mui/icons-material";
|
||||
|
@ -77,13 +77,14 @@ export const Notice: NoticeInstance = (props) => {
|
|||
|
||||
const container = document.createElement("div");
|
||||
parent.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
const onUnmount = () => {
|
||||
const result = ReactDOM.unmountComponentAtNode(container);
|
||||
if (result && parent) setTimeout(() => parent.removeChild(container), 500);
|
||||
root.unmount();
|
||||
if (parent) setTimeout(() => parent.removeChild(container), 500);
|
||||
};
|
||||
|
||||
ReactDOM.render(<NoticeInner {...props} onClose={onUnmount} />, container);
|
||||
root.render(<NoticeInner {...props} onClose={onUnmount} />);
|
||||
};
|
||||
|
||||
(["info", "error", "success"] as const).forEach((type) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { Typography } from "@mui/material";
|
||||
import { BaseErrorBoundary } from "./base-error-boundary";
|
||||
|
||||
|
@ -6,6 +6,7 @@ interface Props {
|
|||
title?: React.ReactNode; // the page title
|
||||
header?: React.ReactNode; // something behind title
|
||||
contentStyle?: React.CSSProperties;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const BasePage: React.FC<Props> = (props) => {
|
||||
|
|
104
src/components/connection/connection-detail.tsx
Normal file
104
src/components/connection/connection-detail.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import dayjs from "dayjs";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, Button, Snackbar } from "@mui/material";
|
||||
import { deleteConnection } from "@/services/api";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
export interface ConnectionDetailRef {
|
||||
open: (detail: IConnectionsItem) => void;
|
||||
}
|
||||
|
||||
export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
(props, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (detail: IConnectionsItem) => {
|
||||
if (open) return;
|
||||
setOpen(true);
|
||||
setDetail(detail);
|
||||
},
|
||||
}));
|
||||
|
||||
const onClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail data={detail} onClose={onClose} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface InnerProps {
|
||||
data: IConnectionsItem;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const { metadata, rulePayload } = data;
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||
const host = metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`;
|
||||
|
||||
const information = [
|
||||
{ label: "Host", value: host },
|
||||
{ label: "Download", value: parseTraffic(data.download).join(" ") },
|
||||
{ label: "Upload", value: parseTraffic(data.upload).join(" ") },
|
||||
{
|
||||
label: "DL Speed",
|
||||
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
label: "UL Speed",
|
||||
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{ label: "Chains", value: chains },
|
||||
{ label: "Rule", value: rule },
|
||||
{
|
||||
label: "Process",
|
||||
value: truncateStr(metadata.process || metadata.processPath),
|
||||
},
|
||||
{ label: "Time", value: dayjs(data.start).fromNow() },
|
||||
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
|
||||
{ label: "Destination IP", value: metadata.destinationIP },
|
||||
{ label: "Type", value: `${metadata.type}(${metadata.network})` },
|
||||
];
|
||||
|
||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||
|
||||
return (
|
||||
<Box sx={{ userSelect: "text" }}>
|
||||
{information.map((each) => (
|
||||
<div key={each.label}>
|
||||
<b>{each.label}</b>: <span>{each.value}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Box sx={{ textAlign: "right" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
title="Close Connection"
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -24,10 +24,11 @@ const Tag = styled("span")(({ theme }) => ({
|
|||
|
||||
interface Props {
|
||||
value: IConnectionsItem;
|
||||
onShowDetail?: () => void;
|
||||
}
|
||||
|
||||
const ConnectionItem = (props: Props) => {
|
||||
const { value } = props;
|
||||
export const ConnectionItem = (props: Props) => {
|
||||
const { value, onShowDetail } = props;
|
||||
|
||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
||||
|
||||
|
@ -44,8 +45,9 @@ const ConnectionItem = (props: Props) => {
|
|||
}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ userSelect: "text" }}
|
||||
sx={{ userSelect: "text", cursor: "pointer" }}
|
||||
primary={metadata.host || metadata.destinationIP}
|
||||
onClick={onShowDetail}
|
||||
secondary={
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
||||
<Tag sx={{ textTransform: "uppercase", color: "success" }}>
|
||||
|
@ -56,7 +58,7 @@ const ConnectionItem = (props: Props) => {
|
|||
|
||||
{!!metadata.process && <Tag>{metadata.process}</Tag>}
|
||||
|
||||
{chains.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>}
|
||||
{chains?.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>}
|
||||
|
||||
<Tag>{dayjs(start).fromNow()}</Tag>
|
||||
|
||||
|
@ -71,5 +73,3 @@ const ConnectionItem = (props: Props) => {
|
|||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionItem;
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
import dayjs from "dayjs";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
interface Props {
|
||||
connections: IConnectionsItem[];
|
||||
onShowDetail: (data: IConnectionsItem) => void;
|
||||
}
|
||||
|
||||
const ConnectionTable = (props: Props) => {
|
||||
const { connections } = props;
|
||||
export const ConnectionTable = (props: Props) => {
|
||||
const { connections, onShowDetail } = props;
|
||||
|
||||
const [columnVisible, setColumnVisible] = useState<
|
||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||
>({});
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "host",
|
||||
headerName: "Host",
|
||||
flex: 200,
|
||||
minWidth: 200,
|
||||
resizable: false,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "host", headerName: "Host", flex: 220, minWidth: 220 },
|
||||
{
|
||||
field: "download",
|
||||
headerName: "Download",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "upload",
|
||||
|
@ -34,18 +31,13 @@ const ConnectionTable = (props: Props) => {
|
|||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "dlSpeed",
|
||||
headerName: "DL Speed",
|
||||
align: "right",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "ulSpeed",
|
||||
|
@ -53,55 +45,26 @@ const ConnectionTable = (props: Props) => {
|
|||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "chains",
|
||||
headerName: "Chains",
|
||||
width: 360,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "rule",
|
||||
headerName: "Rule",
|
||||
width: 225,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "process",
|
||||
headerName: "Process",
|
||||
width: 120,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "chains", headerName: "Chains", flex: 360, minWidth: 360 },
|
||||
{ field: "rule", headerName: "Rule", flex: 300, minWidth: 250 },
|
||||
{ field: "process", headerName: "Process", flex: 480, minWidth: 480 },
|
||||
{
|
||||
field: "time",
|
||||
headerName: "Time",
|
||||
width: 120,
|
||||
flex: 120,
|
||||
minWidth: 100,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params) => dayjs(params.value).fromNow(),
|
||||
},
|
||||
{
|
||||
field: "source",
|
||||
headerName: "Source",
|
||||
width: 150,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "source", headerName: "Source", flex: 200, minWidth: 130 },
|
||||
{
|
||||
field: "destinationIP",
|
||||
headerName: "Destination IP",
|
||||
width: 125,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "type",
|
||||
headerName: "Type",
|
||||
width: 160,
|
||||
disableColumnMenu: true,
|
||||
flex: 200,
|
||||
minWidth: 130,
|
||||
},
|
||||
{ field: "type", headerName: "Type", flex: 160, minWidth: 100 },
|
||||
];
|
||||
|
||||
const connRows = useMemo(() => {
|
||||
|
@ -115,30 +78,33 @@ const ConnectionTable = (props: Props) => {
|
|||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
ulSpeed: each.curUpload,
|
||||
download: parseTraffic(each.download).join(" "),
|
||||
upload: parseTraffic(each.upload).join(" "),
|
||||
dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s",
|
||||
ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s",
|
||||
chains,
|
||||
rule,
|
||||
process: metadata.process || metadata.processPath,
|
||||
time: each.start,
|
||||
process: truncateStr(metadata.process || metadata.processPath),
|
||||
time: dayjs(each.start).fromNow(),
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
destinationIP: metadata.destinationIP,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
|
||||
connectionData: each,
|
||||
};
|
||||
});
|
||||
}, [connections]);
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
hideFooter
|
||||
rows={connRows}
|
||||
columns={columns}
|
||||
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
||||
density="compact"
|
||||
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
|
||||
hideFooter
|
||||
columnVisibilityModel={columnVisible}
|
||||
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionTable;
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
HorizontalRuleRounded,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const LayoutControl = () => {
|
||||
export const LayoutControl = () => {
|
||||
const minWidth = 40;
|
||||
|
||||
return (
|
||||
|
@ -37,5 +37,3 @@ const LayoutControl = () => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutControl;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { alpha, ListItem, ListItemButton, ListItemText } from "@mui/material";
|
|||
import { useMatch, useResolvedPath, useNavigate } from "react-router-dom";
|
||||
import type { LinkProps } from "react-router-dom";
|
||||
|
||||
const LayoutItem = (props: LinkProps) => {
|
||||
export const LayoutItem = (props: LinkProps) => {
|
||||
const { to, children } = props;
|
||||
|
||||
const resolved = useResolvedPath(to);
|
||||
|
@ -40,5 +40,3 @@ const LayoutItem = (props: LinkProps) => {
|
|||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutItem;
|
||||
|
|
|
@ -1,56 +1,90 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { ArrowDownward, ArrowUpward } from "@mui/icons-material";
|
||||
import {
|
||||
ArrowDownward,
|
||||
ArrowUpward,
|
||||
MemoryOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
|
||||
import { useLogSetup } from "./use-log-setup";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { useWebsocket } from "@/hooks/use-websocket";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
// setup the traffic
|
||||
const LayoutTraffic = () => {
|
||||
export const LayoutTraffic = () => {
|
||||
const { clashInfo } = useClashInfo();
|
||||
const { verge } = useVerge();
|
||||
|
||||
// whether hide traffic graph
|
||||
const { verge } = useVerge();
|
||||
const trafficGraph = verge?.traffic_graph ?? true;
|
||||
|
||||
const trafficRef = useRef<TrafficRef>(null);
|
||||
const [traffic, setTraffic] = useState({ up: 0, down: 0 });
|
||||
const [memory, setMemory] = useState({ inuse: 0 });
|
||||
const pageVisible = useVisibility();
|
||||
|
||||
// setup log ws during layout
|
||||
useLogSetup();
|
||||
|
||||
const { connect, disconnect } = useWebsocket((event) => {
|
||||
const data = JSON.parse(event.data) as ITrafficItem;
|
||||
trafficRef.current?.appendData(data);
|
||||
setTraffic(data);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!clashInfo) return;
|
||||
if (!clashInfo || !pageVisible) return;
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
const ws = new WebSocket(`ws://${server}/traffic?token=${secret}`);
|
||||
connect(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`);
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const data = JSON.parse(event.data) as ITrafficItem;
|
||||
trafficRef.current?.appendData(data);
|
||||
setTraffic(data);
|
||||
});
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [clashInfo, pageVisible]);
|
||||
|
||||
return () => ws?.close();
|
||||
}, [clashInfo]);
|
||||
/* --------- meta memory information --------- */
|
||||
const isMetaCore = verge?.clash_core === "clash-meta";
|
||||
const displayMemory = isMetaCore && (verge?.enable_memory_usage ?? true);
|
||||
|
||||
const memoryWs = useWebsocket(
|
||||
(event) => {
|
||||
setMemory(JSON.parse(event.data));
|
||||
},
|
||||
{ onError: () => setMemory({ inuse: 0 }) }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clashInfo || !pageVisible || !displayMemory) return;
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
memoryWs.connect(
|
||||
`ws://${server}/memory?token=${encodeURIComponent(secret)}`
|
||||
);
|
||||
return () => memoryWs.disconnect();
|
||||
}, [clashInfo, pageVisible, displayMemory]);
|
||||
|
||||
const [up, upUnit] = parseTraffic(traffic.up);
|
||||
const [down, downUnit] = parseTraffic(traffic.down);
|
||||
const [inuse, inuseUnit] = parseTraffic(memory.inuse);
|
||||
|
||||
const iconStyle: any = {
|
||||
sx: { mr: "8px", fontSize: 16 },
|
||||
};
|
||||
const valStyle: any = {
|
||||
component: "span",
|
||||
color: "primary",
|
||||
textAlign: "center",
|
||||
sx: { flex: "1 1 54px", userSelect: "none" },
|
||||
sx: { flex: "1 1 56px", userSelect: "none" },
|
||||
};
|
||||
const unitStyle: any = {
|
||||
component: "span",
|
||||
color: "grey.500",
|
||||
fontSize: "12px",
|
||||
textAlign: "right",
|
||||
sx: { flex: "0 1 28px", userSelect: "none" },
|
||||
sx: { flex: "0 1 27px", userSelect: "none" },
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -59,31 +93,44 @@ const LayoutTraffic = () => {
|
|||
position="relative"
|
||||
onClick={trafficRef.current?.toggleStyle}
|
||||
>
|
||||
{trafficGraph && (
|
||||
{trafficGraph && pageVisible && (
|
||||
<div style={{ width: "100%", height: 60, marginBottom: 6 }}>
|
||||
<TrafficGraph ref={trafficRef} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Box mb={1.5} display="flex" alignItems="center" whiteSpace="nowrap">
|
||||
<ArrowUpward
|
||||
fontSize="small"
|
||||
color={+up > 0 ? "primary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle}>{up}</Typography>
|
||||
<Typography {...unitStyle}>{upUnit}/s</Typography>
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="column" gap={0.75}>
|
||||
<Box display="flex" alignItems="center" whiteSpace="nowrap">
|
||||
<ArrowUpward
|
||||
{...iconStyle}
|
||||
color={+up > 0 ? "primary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle}>{up}</Typography>
|
||||
<Typography {...unitStyle}>{upUnit}/s</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" whiteSpace="nowrap">
|
||||
<ArrowDownward
|
||||
fontSize="small"
|
||||
color={+down > 0 ? "primary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle}>{down}</Typography>
|
||||
<Typography {...unitStyle}>{downUnit}/s</Typography>
|
||||
<Box display="flex" alignItems="center" whiteSpace="nowrap">
|
||||
<ArrowDownward
|
||||
{...iconStyle}
|
||||
color={+down > 0 ? "primary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle}>{down}</Typography>
|
||||
<Typography {...unitStyle}>{downUnit}/s</Typography>
|
||||
</Box>
|
||||
|
||||
{displayMemory && (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
whiteSpace="nowrap"
|
||||
title="Memory Usage"
|
||||
>
|
||||
<MemoryOutlined {...iconStyle} color="disabled" />
|
||||
<Typography {...valStyle}>{inuse}</Typography>
|
||||
<Typography {...unitStyle}>{inuseUnit}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutTraffic;
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { Button } from "@mui/material";
|
||||
import { checkUpdate } from "@tauri-apps/api/updater";
|
||||
import UpdateDialog from "./update-dialog";
|
||||
import { UpdateViewer } from "../setting/mods/update-viewer";
|
||||
import { DialogRef } from "../base";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const UpdateButton = (props: Props) => {
|
||||
export const UpdateButton = (props: Props) => {
|
||||
const { className } = props;
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const viewerRef = useRef<DialogRef>(null);
|
||||
|
||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
|
@ -22,21 +24,17 @@ const UpdateButton = (props: Props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<UpdateViewer ref={viewerRef} />
|
||||
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={className}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
onClick={() => viewerRef.current?.open()}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
|
||||
{dialogOpen && (
|
||||
<UpdateDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateButton;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useVerge } from "@/hooks/use-verge";
|
|||
/**
|
||||
* custom theme
|
||||
*/
|
||||
export default function useCustomTheme() {
|
||||
export const useCustomTheme = () => {
|
||||
const { verge } = useVerge();
|
||||
const { theme_mode, theme_setting } = verge ?? {};
|
||||
const [mode, setMode] = useRecoilState(atomThemeMode);
|
||||
|
@ -121,4 +121,4 @@ export default function useCustomTheme() {
|
|||
}, [mode, theme_setting]);
|
||||
|
||||
return { theme };
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useRecoilValue, useSetRecoilState } from "recoil";
|
|||
import { getClashLogs } from "@/services/cmds";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { atomEnableLog, atomLogData } from "@/services/states";
|
||||
import { useWebsocket } from "@/hooks/use-websocket";
|
||||
|
||||
const MAX_LOG_NUM = 1000;
|
||||
|
||||
|
@ -14,23 +15,25 @@ export const useLogSetup = () => {
|
|||
const enableLog = useRecoilValue(atomEnableLog);
|
||||
const setLogData = useSetRecoilState(atomLogData);
|
||||
|
||||
const { connect, disconnect } = useWebsocket((event) => {
|
||||
const data = JSON.parse(event.data) as ILogItem;
|
||||
const time = dayjs().format("MM-DD HH:mm:ss");
|
||||
setLogData((l) => {
|
||||
if (l.length >= MAX_LOG_NUM) l.shift();
|
||||
return [...l, { ...data, time }];
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableLog || !clashInfo) return;
|
||||
|
||||
getClashLogs().then(setLogData);
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
const ws = new WebSocket(`ws://${server}/logs?token=${secret}`);
|
||||
connect(`ws://${server}/logs?token=${encodeURIComponent(secret)}`);
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const data = JSON.parse(event.data) as ILogItem;
|
||||
const time = dayjs().format("MM-DD HH:mm:ss");
|
||||
setLogData((l) => {
|
||||
if (l.length >= MAX_LOG_NUM) l.shift();
|
||||
return [...l, { ...data, time }];
|
||||
});
|
||||
});
|
||||
|
||||
return () => ws?.close();
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [clashInfo, enableLog]);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { styled, Box } from "@mui/material";
|
||||
|
||||
const Item = styled(Box)(({ theme }) => ({
|
||||
const Item = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||
padding: "8px 0",
|
||||
margin: "0 12px",
|
||||
lineHeight: 1.35,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
borderBottom: `1px solid ${palette.divider}`,
|
||||
fontSize: "0.875rem",
|
||||
fontFamily: typography.fontFamily,
|
||||
userSelect: "text",
|
||||
"& .time": {},
|
||||
"& .time": {
|
||||
color: palette.text.secondary,
|
||||
},
|
||||
"& .type": {
|
||||
display: "inline-block",
|
||||
marginLeft: 8,
|
||||
|
@ -16,7 +19,18 @@ const Item = styled(Box)(({ theme }) => ({
|
|||
textTransform: "uppercase",
|
||||
fontWeight: "600",
|
||||
},
|
||||
"& .data": {},
|
||||
'& .type[data-type="error"], & .type[data-type="err"]': {
|
||||
color: palette.error.main,
|
||||
},
|
||||
'& .type[data-type="warning"], & .type[data-type="warn"]': {
|
||||
color: palette.warning.main,
|
||||
},
|
||||
'& .type[data-type="info"], & .type[data-type="inf"]': {
|
||||
color: palette.info.main,
|
||||
},
|
||||
"& .data": {
|
||||
color: palette.text.primary,
|
||||
},
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
|
@ -30,7 +44,9 @@ const LogItem = (props: Props) => {
|
|||
<Item>
|
||||
<div>
|
||||
<span className="time">{value.time}</span>
|
||||
<span className="type">{value.type}</span>
|
||||
<span className="type" data-type={value.type.toLowerCase()}>
|
||||
{value.type}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="data">{value.payload}</span>
|
||||
|
|
|
@ -77,7 +77,7 @@ export const EditorViewer = (props: Props) => {
|
|||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t("Edit File")}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ width: 520, pb: 1 }}>
|
||||
<DialogContent sx={{ width: 520, pb: 1, userSelect: "text" }}>
|
||||
<div style={{ width: "100%", height: "420px" }} ref={editorRef} />
|
||||
</DialogContent>
|
||||
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
import useSWR from "swr";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Grid } from "@mui/material";
|
||||
import {
|
||||
getProfiles,
|
||||
deleteProfile,
|
||||
patchProfilesConfig,
|
||||
getRuntimeLogs,
|
||||
} from "@/services/cmds";
|
||||
import { Notice } from "@/components/base";
|
||||
import { ProfileMore } from "./profile-more";
|
||||
|
||||
interface Props {
|
||||
items: IProfileItem[];
|
||||
chain: string[];
|
||||
}
|
||||
|
||||
export const EnhancedMode = (props: Props) => {
|
||||
const { items, chain } = props;
|
||||
|
||||
const { mutate: mutateProfiles } = useSWR("getProfiles", getProfiles);
|
||||
const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
|
||||
"getRuntimeLogs",
|
||||
getRuntimeLogs
|
||||
);
|
||||
|
||||
const onEnhanceEnable = useLockFn(async (uid: string) => {
|
||||
if (chain.includes(uid)) return;
|
||||
|
||||
const newChain = [...chain, uid];
|
||||
await patchProfilesConfig({ chain: newChain });
|
||||
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
|
||||
mutateLogs();
|
||||
});
|
||||
|
||||
const onEnhanceDisable = useLockFn(async (uid: string) => {
|
||||
if (!chain.includes(uid)) return;
|
||||
|
||||
const newChain = chain.filter((i) => i !== uid);
|
||||
await patchProfilesConfig({ chain: newChain });
|
||||
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
|
||||
mutateLogs();
|
||||
});
|
||||
|
||||
const onEnhanceDelete = useLockFn(async (uid: string) => {
|
||||
try {
|
||||
await onEnhanceDisable(uid);
|
||||
await deleteProfile(uid);
|
||||
mutateProfiles();
|
||||
mutateLogs();
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const onMoveTop = useLockFn(async (uid: string) => {
|
||||
if (!chain.includes(uid)) return;
|
||||
|
||||
const newChain = [uid].concat(chain.filter((i) => i !== uid));
|
||||
await patchProfilesConfig({ chain: newChain });
|
||||
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
|
||||
mutateLogs();
|
||||
});
|
||||
|
||||
const onMoveEnd = useLockFn(async (uid: string) => {
|
||||
if (!chain.includes(uid)) return;
|
||||
|
||||
const newChain = chain.filter((i) => i !== uid).concat([uid]);
|
||||
await patchProfilesConfig({ chain: newChain });
|
||||
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
|
||||
mutateLogs();
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid container spacing={{ xs: 2, lg: 3 }}>
|
||||
{items.map((item) => (
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
|
||||
<ProfileMore
|
||||
selected={!!chain.includes(item.uid)}
|
||||
itemData={item}
|
||||
enableNum={chain.length}
|
||||
logInfo={chainLogs[item.uid]}
|
||||
onEnable={() => onEnhanceEnable(item.uid)}
|
||||
onDisable={() => onEnhanceDisable(item.uid)}
|
||||
onDelete={() => onEnhanceDelete(item.uid)}
|
||||
onMoveTop={() => onMoveTop(item.uid)}
|
||||
onMoveEnd={() => onMoveEnd(item.uid)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
|
@ -1,211 +0,0 @@
|
|||
import { mutate } from "swr";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLockFn, useSetState } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Switch,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { Settings } from "@mui/icons-material";
|
||||
import { patchProfile } from "@/services/cmds";
|
||||
import { version } from "@root/package.json";
|
||||
import { Notice } from "@/components/base";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
itemData: IProfileItem;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// edit the profile item
|
||||
// remote / local file / merge / script
|
||||
export const InfoViewer = (props: Props) => {
|
||||
const { open, itemData, onClose } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [form, setForm] = useSetState({ ...itemData });
|
||||
const [option, setOption] = useSetState(itemData.option ?? {});
|
||||
const [showOpt, setShowOpt] = useState(!!itemData.option);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemData) {
|
||||
const { option } = itemData;
|
||||
setForm({ ...itemData });
|
||||
setOption(option ?? {});
|
||||
setShowOpt(
|
||||
itemData.type === "remote" &&
|
||||
(!!option?.user_agent ||
|
||||
!!option?.update_interval ||
|
||||
!!option?.self_proxy ||
|
||||
!!option?.with_proxy)
|
||||
);
|
||||
}
|
||||
}, [itemData]);
|
||||
|
||||
const onUpdate = useLockFn(async () => {
|
||||
try {
|
||||
const { uid } = itemData;
|
||||
const { name, desc, url } = form;
|
||||
const option_ =
|
||||
itemData.type === "remote" || itemData.type === "local"
|
||||
? option
|
||||
: undefined;
|
||||
|
||||
if (itemData.type === "remote" && !url) {
|
||||
throw new Error("Remote URL should not be null");
|
||||
}
|
||||
|
||||
await patchProfile(uid, { uid, name, desc, url, option: option_ });
|
||||
mutate("getProfiles");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const textFieldProps = {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
margin: "normal",
|
||||
variant: "outlined",
|
||||
} as const;
|
||||
|
||||
const type =
|
||||
form.type ||
|
||||
(form.url ? "remote" : form.file?.endsWith(".js") ? "script" : "local");
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle sx={{ pb: 0.5 }}>{t("Edit Info")}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ width: 336, pb: 1 }}>
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
disabled
|
||||
label={t("Type")}
|
||||
value={type}
|
||||
sx={{ input: { textTransform: "capitalize" } }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
autoFocus
|
||||
label={t("Name")}
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ name: e.target.value })}
|
||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
label={t("Descriptions")}
|
||||
value={form.desc}
|
||||
onChange={(e) => setForm({ desc: e.target.value })}
|
||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
||||
/>
|
||||
|
||||
{type === "remote" && (
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
label={t("Subscription URL")}
|
||||
value={form.url}
|
||||
onChange={(e) => setForm({ url: e.target.value })}
|
||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(type === "remote" || type === "local") && (
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
label={t("Update Interval(mins)")}
|
||||
value={option.update_interval}
|
||||
onChange={(e) => {
|
||||
const str = e.target.value?.replace(/\D/, "");
|
||||
setOption({ update_interval: !!str ? +str : undefined });
|
||||
}}
|
||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Collapse
|
||||
in={type === "remote" && showOpt}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
>
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
label="User Agent"
|
||||
value={option.user_agent}
|
||||
placeholder={`clash-verge/v${version}`}
|
||||
onChange={(e) => setOption({ user_agent: e.target.value })}
|
||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
label={t("Use System Proxy")}
|
||||
labelPlacement="start"
|
||||
sx={{ ml: 0, my: 1 }}
|
||||
control={
|
||||
<Switch
|
||||
color="primary"
|
||||
checked={option.with_proxy ?? false}
|
||||
onChange={(_e, c) =>
|
||||
setOption((o) => ({
|
||||
self_proxy: c ? false : o.self_proxy ?? false,
|
||||
with_proxy: c,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
label={t("Use Clash Proxy")}
|
||||
labelPlacement="start"
|
||||
sx={{ ml: 0, my: 1 }}
|
||||
control={
|
||||
<Switch
|
||||
color="primary"
|
||||
checked={option.self_proxy ?? false}
|
||||
onChange={(_e, c) =>
|
||||
setOption((o) => ({
|
||||
with_proxy: c ? false : o.with_proxy ?? false,
|
||||
self_proxy: c,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Collapse>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 2, pb: 2, position: "relative" }}>
|
||||
{form.type === "remote" && (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ position: "absolute", left: 18 }}
|
||||
onClick={() => setShowOpt((o) => !o)}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={onUpdate} variant="contained">
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
|
@ -27,6 +27,7 @@ export const ProfileBox = styled(Box)(
|
|||
}[key]!;
|
||||
|
||||
return {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
display: "block",
|
||||
cursor: "pointer",
|
||||
|
|
|
@ -12,12 +12,12 @@ import {
|
|||
keyframes,
|
||||
MenuItem,
|
||||
Menu,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import { RefreshRounded } from "@mui/icons-material";
|
||||
import { atomLoadingCache } from "@/services/states";
|
||||
import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
|
||||
import { Notice } from "@/components/base";
|
||||
import { InfoViewer } from "./info-viewer";
|
||||
import { EditorViewer } from "./editor-viewer";
|
||||
import { ProfileBox } from "./profile-box";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
@ -29,12 +29,14 @@ const round = keyframes`
|
|||
|
||||
interface Props {
|
||||
selected: boolean;
|
||||
activating: boolean;
|
||||
itemData: IProfileItem;
|
||||
onSelect: (force: boolean) => void;
|
||||
onEdit: () => void;
|
||||
}
|
||||
|
||||
export const ProfileItem = (props: Props) => {
|
||||
const { selected, itemData, onSelect } = props;
|
||||
const { selected, activating, itemData, onSelect, onEdit } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
|
@ -55,7 +57,7 @@ export const ProfileItem = (props: Props) => {
|
|||
|
||||
const loading = loadingCache[itemData.uid] ?? false;
|
||||
|
||||
// interval update from now field
|
||||
// interval update fromNow field
|
||||
const [, setRefresh] = useState({});
|
||||
useEffect(() => {
|
||||
if (!hasUrl) return;
|
||||
|
@ -83,12 +85,11 @@ export const ProfileItem = (props: Props) => {
|
|||
};
|
||||
}, [hasUrl, updated]);
|
||||
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [fileOpen, setFileOpen] = useState(false);
|
||||
|
||||
const onEditInfo = () => {
|
||||
setAnchorEl(null);
|
||||
setEditOpen(true);
|
||||
onEdit();
|
||||
};
|
||||
|
||||
const onEditFile = () => {
|
||||
|
@ -193,6 +194,25 @@ export const ProfileItem = (props: Props) => {
|
|||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{activating && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
top: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 2,
|
||||
zIndex: 10,
|
||||
backdropFilter: "blur(2px)",
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={20} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box position="relative">
|
||||
<Typography
|
||||
width="calc(100% - 36px)"
|
||||
|
@ -298,12 +318,6 @@ export const ProfileItem = (props: Props) => {
|
|||
))}
|
||||
</Menu>
|
||||
|
||||
<InfoViewer
|
||||
open={editOpen}
|
||||
itemData={itemData}
|
||||
onClose={() => setEditOpen(false)}
|
||||
/>
|
||||
|
||||
<EditorViewer
|
||||
uid={uid}
|
||||
open={fileOpen}
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
import { FeaturedPlayListRounded } from "@mui/icons-material";
|
||||
import { viewProfile } from "@/services/cmds";
|
||||
import { Notice } from "@/components/base";
|
||||
import { InfoViewer } from "./info-viewer";
|
||||
import { EditorViewer } from "./editor-viewer";
|
||||
import { ProfileBox } from "./profile-box";
|
||||
import { LogViewer } from "./log-viewer";
|
||||
|
@ -29,6 +28,7 @@ interface Props {
|
|||
onMoveTop: () => void;
|
||||
onMoveEnd: () => void;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
}
|
||||
|
||||
// profile enhanced item
|
||||
|
@ -43,19 +43,19 @@ export const ProfileMore = (props: Props) => {
|
|||
onMoveTop,
|
||||
onMoveEnd,
|
||||
onDelete,
|
||||
onEdit,
|
||||
} = props;
|
||||
|
||||
const { uid, type } = itemData;
|
||||
const { t, i18n } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [fileOpen, setFileOpen] = useState(false);
|
||||
const [logOpen, setLogOpen] = useState(false);
|
||||
|
||||
const onEditInfo = () => {
|
||||
setAnchorEl(null);
|
||||
setEditOpen(true);
|
||||
onEdit();
|
||||
};
|
||||
|
||||
const onEditFile = () => {
|
||||
|
@ -219,12 +219,6 @@ export const ProfileMore = (props: Props) => {
|
|||
))}
|
||||
</Menu>
|
||||
|
||||
<InfoViewer
|
||||
open={editOpen}
|
||||
itemData={itemData}
|
||||
onClose={() => setEditOpen(false)}
|
||||
/>
|
||||
|
||||
<EditorViewer
|
||||
uid={uid}
|
||||
open={fileOpen}
|
||||
|
|
|
@ -1,212 +0,0 @@
|
|||
import { useRef, useState } from "react";
|
||||
import { mutate } from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn, useSetState } from "ahooks";
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { Settings } from "@mui/icons-material";
|
||||
import { createProfile } from "@/services/cmds";
|
||||
import { Notice } from "@/components/base";
|
||||
import { FileInput } from "./file-input";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// create a new profile
|
||||
// remote / local file / merge / script
|
||||
export const ProfileNew = (props: Props) => {
|
||||
const { open, onClose } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [form, setForm] = useSetState({
|
||||
type: "remote",
|
||||
name: "",
|
||||
desc: "",
|
||||
url: "",
|
||||
});
|
||||
|
||||
const [showOpt, setShowOpt] = useState(false);
|
||||
// can add more option
|
||||
const [option, setOption] = useSetState({
|
||||
user_agent: "",
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
});
|
||||
// file input
|
||||
const fileDataRef = useRef<string | null>(null);
|
||||
|
||||
const onCreate = useLockFn(async () => {
|
||||
if (!form.type) {
|
||||
Notice.error("`Type` should not be null");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const name = form.name || `${form.type} file`;
|
||||
|
||||
if (form.type === "remote" && !form.url) {
|
||||
throw new Error("The URL should not be null");
|
||||
}
|
||||
|
||||
const option_ = form.type === "remote" ? option : undefined;
|
||||
const item = { ...form, name, option: option_ };
|
||||
const fileData = form.type === "local" ? fileDataRef.current : null;
|
||||
|
||||
await createProfile(item, fileData);
|
||||
|
||||
setForm({ type: "remote", name: "", desc: "", url: "" });
|
||||
setOption({ user_agent: "" });
|
||||
setShowOpt(false);
|
||||
fileDataRef.current = null;
|
||||
|
||||
mutate("getProfiles");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const textFieldProps = {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
margin: "normal",
|
||||
variant: "outlined",
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle sx={{ pb: 0.5 }}>{t("Create Profile")}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ width: 336, pb: 1 }}>
|
||||
<FormControl size="small" fullWidth sx={{ mt: 2, mb: 1 }}>
|
||||
<InputLabel>Type</InputLabel>
|
||||
<Select
|
||||
autoFocus
|
||||
label={t("Type")}
|
||||
value={form.type}
|
||||
onChange={(e) => setForm({ type: e.target.value })}
|
||||
>
|
||||
<MenuItem value="remote">Remote</MenuItem>
|
||||
<MenuItem value="local">Local</MenuItem>
|
||||
<MenuItem value="script">Script</MenuItem>
|
||||
<MenuItem value="merge">Merge</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
label={t("Name")}
|
||||
autoComplete="off"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ name: e.target.value })}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
label={t("Descriptions")}
|
||||
autoComplete="off"
|
||||
value={form.desc}
|
||||
onChange={(e) => setForm({ desc: e.target.value })}
|
||||
/>
|
||||
|
||||
{form.type === "remote" && (
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
label={t("Subscription URL")}
|
||||
autoComplete="off"
|
||||
value={form.url}
|
||||
onChange={(e) => setForm({ url: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{form.type === "local" && (
|
||||
<FileInput onChange={(val) => (fileDataRef.current = val)} />
|
||||
)}
|
||||
|
||||
<Collapse
|
||||
in={form.type === "remote" && showOpt}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
>
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
label="User Agent"
|
||||
autoComplete="off"
|
||||
value={option.user_agent}
|
||||
onChange={(e) => setOption({ user_agent: e.target.value })}
|
||||
/>
|
||||
<FormControlLabel
|
||||
label={t("Use System Proxy")}
|
||||
labelPlacement="start"
|
||||
sx={{ ml: 0, my: 1 }}
|
||||
control={
|
||||
<Switch
|
||||
color="primary"
|
||||
checked={option.with_proxy}
|
||||
onChange={(_e, c) =>
|
||||
setOption((o) => ({
|
||||
self_proxy: c ? false : o.self_proxy,
|
||||
with_proxy: c,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
label={t("Use Clash Proxy")}
|
||||
labelPlacement="start"
|
||||
sx={{ ml: 0, my: 1 }}
|
||||
control={
|
||||
<Switch
|
||||
color="primary"
|
||||
checked={option.self_proxy}
|
||||
onChange={(_e, c) =>
|
||||
setOption((o) => ({
|
||||
with_proxy: c ? false : o.with_proxy,
|
||||
self_proxy: c,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Collapse>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 2, pb: 2, position: "relative" }}>
|
||||
{form.type === "remote" && (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ position: "absolute", left: 18 }}
|
||||
onClick={() => setShowOpt((o) => !o)}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={onCreate} variant="contained">
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
279
src/components/profile/profile-viewer.tsx
Normal file
279
src/components/profile/profile-viewer.tsx
Normal file
|
@ -0,0 +1,279 @@
|
|||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
styled,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { createProfile, patchProfile } from "@/services/cmds";
|
||||
import { BaseDialog, Notice } from "@/components/base";
|
||||
import { version } from "@root/package.json";
|
||||
import { FileInput } from "./file-input";
|
||||
|
||||
interface Props {
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export interface ProfileViewerRef {
|
||||
create: () => void;
|
||||
edit: (item: IProfileItem) => void;
|
||||
}
|
||||
|
||||
// create or edit the profile
|
||||
// remote / local / merge / script
|
||||
export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
||||
|
||||
// file input
|
||||
const fileDataRef = useRef<string | null>(null);
|
||||
|
||||
const { control, watch, register, ...formIns } = useForm<IProfileItem>({
|
||||
defaultValues: {
|
||||
type: "remote",
|
||||
name: "Remote File",
|
||||
desc: "",
|
||||
url: "",
|
||||
option: {
|
||||
// user_agent: "",
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
create: () => {
|
||||
setOpenType("new");
|
||||
setOpen(true);
|
||||
},
|
||||
edit: (item) => {
|
||||
if (item) {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
formIns.setValue(key as any, value);
|
||||
});
|
||||
}
|
||||
setOpenType("edit");
|
||||
setOpen(true);
|
||||
},
|
||||
}));
|
||||
|
||||
const selfProxy = watch("option.self_proxy");
|
||||
const withProxy = watch("option.with_proxy");
|
||||
|
||||
useEffect(() => {
|
||||
if (selfProxy) formIns.setValue("option.with_proxy", false);
|
||||
}, [selfProxy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (withProxy) formIns.setValue("option.self_proxy", false);
|
||||
}, [withProxy]);
|
||||
|
||||
const handleOk = useLockFn(
|
||||
formIns.handleSubmit(async (form) => {
|
||||
try {
|
||||
if (!form.type) throw new Error("`Type` should not be null");
|
||||
if (form.type === "remote" && !form.url) {
|
||||
throw new Error("The URL should not be null");
|
||||
}
|
||||
if (form.type !== "remote" && form.type !== "local") {
|
||||
delete form.option;
|
||||
}
|
||||
if (form.option?.update_interval) {
|
||||
form.option.update_interval = +form.option.update_interval;
|
||||
}
|
||||
const name = form.name || `${form.type} file`;
|
||||
const item = { ...form, name };
|
||||
|
||||
// 创建
|
||||
if (openType === "new") {
|
||||
await createProfile(item, fileDataRef.current);
|
||||
}
|
||||
// 编辑
|
||||
else {
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
await patchProfile(form.uid, item);
|
||||
}
|
||||
setOpen(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
fileDataRef.current = null;
|
||||
props.onChange();
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
fileDataRef.current = null;
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
};
|
||||
|
||||
const text = {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
margin: "normal",
|
||||
variant: "outlined",
|
||||
autoComplete: "off",
|
||||
autoCorrect: "off",
|
||||
} as const;
|
||||
|
||||
const formType = watch("type");
|
||||
const isRemote = formType === "remote";
|
||||
const isLocal = formType === "local";
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
|
||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
onOk={handleOk}
|
||||
>
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
||||
<InputLabel>{t("Type")}</InputLabel>
|
||||
<Select {...field} autoFocus label={t("Type")}>
|
||||
<MenuItem value="remote">Remote</MenuItem>
|
||||
<MenuItem value="local">Local</MenuItem>
|
||||
<MenuItem value="script">Script</MenuItem>
|
||||
<MenuItem value="merge">Merge</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Name")} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="desc"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Descriptions")} />
|
||||
)}
|
||||
/>
|
||||
|
||||
{isRemote && (
|
||||
<>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
label={t("Subscription URL")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="option.user_agent"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
placeholder={`clash-verge/v${version}`}
|
||||
label="User Agent"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(isRemote || isLocal) && (
|
||||
<Controller
|
||||
name="option.update_interval"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
e.target.value = e.target.value
|
||||
?.replace(/\D/, "")
|
||||
.slice(0, 10);
|
||||
field.onChange(e);
|
||||
}}
|
||||
label={t("Update Interval")}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">mins</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLocal && openType === "new" && (
|
||||
<FileInput onChange={(val) => (fileDataRef.current = val)} />
|
||||
)}
|
||||
|
||||
{isRemote && (
|
||||
<>
|
||||
<Controller
|
||||
name="option.with_proxy"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use System Proxy")}</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="option.self_proxy"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use Clash Proxy")}</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const StyledBox = styled(Box)(() => ({
|
||||
margin: "8px 0 8px 8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}));
|
86
src/components/proxy/provider-button.tsx
Normal file
86
src/components/proxy/provider-button.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import dayjs from "dayjs";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { RefreshRounded } from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { getProviders, providerUpdate } from "@/services/api";
|
||||
import { BaseDialog } from "../base";
|
||||
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useSWR("getProviders", getProviders);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const hasProvider = Object.keys(data || {}).length > 0;
|
||||
|
||||
const handleUpdate = useLockFn(async (key: string) => {
|
||||
await providerUpdate(key);
|
||||
await mutate("getProxies");
|
||||
await mutate("getProviders");
|
||||
});
|
||||
|
||||
if (!hasProvider) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t("Provider")}
|
||||
</Button>
|
||||
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Proxy Provider")}
|
||||
contentSx={{ width: 400 }}
|
||||
disableOk
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(data || {}).map(([key, item]) => {
|
||||
const time = dayjs(item.updatedAt);
|
||||
return (
|
||||
<ListItem sx={{ p: 0 }} key={key}>
|
||||
<ListItemText
|
||||
primary={key}
|
||||
secondary={
|
||||
<>
|
||||
<span style={{ marginRight: "4em" }}>
|
||||
Type: {item.vehicleType}
|
||||
</span>
|
||||
<span title={time.format("YYYY-MM-DD HH:mm:ss")}>
|
||||
Updated: {time.fromNow()}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="Update Provider"
|
||||
onClick={() => handleUpdate(key)}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from "@/services/api";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseEmpty } from "../base";
|
||||
import { useRenderList } from "./use-render-list";
|
||||
import { ProxyRender } from "./proxy-render";
|
||||
import delayManager from "@/services/delay";
|
||||
|
@ -67,8 +68,10 @@ export const ProxyGroups = (props: Props) => {
|
|||
// 测全部延迟
|
||||
const handleCheckAll = useLockFn(async (groupName: string) => {
|
||||
const proxies = renderList
|
||||
.filter((e) => e.type === 2 && e.group?.name === groupName)
|
||||
.map((e) => e.proxy!)
|
||||
.filter(
|
||||
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4)
|
||||
)
|
||||
.flatMap((e) => e.proxyCol || e.proxy!)
|
||||
.filter(Boolean);
|
||||
|
||||
const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean));
|
||||
|
@ -80,7 +83,7 @@ export const ProxyGroups = (props: Props) => {
|
|||
}
|
||||
|
||||
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
|
||||
await delayManager.checkListDelay(names, groupName, 24);
|
||||
await delayManager.checkListDelay(names, groupName);
|
||||
|
||||
onProxies();
|
||||
});
|
||||
|
@ -91,7 +94,10 @@ export const ProxyGroups = (props: Props) => {
|
|||
const { name, now } = group;
|
||||
|
||||
const index = renderList.findIndex(
|
||||
(e) => e.type === 2 && e.group?.name === name && e.proxy?.name === now
|
||||
(e) =>
|
||||
e.group?.name === name &&
|
||||
((e.type === 2 && e.proxy?.name === now) ||
|
||||
(e.type === 4 && e.proxyCol?.some((p) => p.name === now)))
|
||||
);
|
||||
|
||||
if (index >= 0) {
|
||||
|
@ -103,11 +109,16 @@ export const ProxyGroups = (props: Props) => {
|
|||
}
|
||||
};
|
||||
|
||||
if (mode === "direct") {
|
||||
return <BaseEmpty text="Direct Mode" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ height: "100%" }}
|
||||
totalCount={renderList.length}
|
||||
increaseViewportBy={256}
|
||||
itemContent={(index) => (
|
||||
<ProxyRender
|
||||
key={renderList[index].key}
|
||||
|
|
181
src/components/proxy/proxy-item-mini.tsx
Normal file
181
src/components/proxy/proxy-item-mini.tsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { CheckCircleOutlineRounded } from "@mui/icons-material";
|
||||
import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
|
||||
import { BaseLoading } from "@/components/base";
|
||||
import delayManager from "@/services/delay";
|
||||
|
||||
interface Props {
|
||||
groupName: string;
|
||||
proxy: IProxyItem;
|
||||
selected: boolean;
|
||||
showType?: boolean;
|
||||
onClick?: (name: string) => void;
|
||||
}
|
||||
|
||||
// 多列布局
|
||||
export const ProxyItemMini = (props: Props) => {
|
||||
const { groupName, proxy, selected, showType = true, onClick } = props;
|
||||
|
||||
// -1/<=0 为 不显示
|
||||
// -2 为 loading
|
||||
const [delay, setDelay] = useState(-1);
|
||||
|
||||
useEffect(() => {
|
||||
delayManager.setListener(proxy.name, groupName, setDelay);
|
||||
|
||||
return () => {
|
||||
delayManager.removeListener(proxy.name, groupName);
|
||||
};
|
||||
}, [proxy.name, groupName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!proxy) return;
|
||||
setDelay(delayManager.getDelayFix(proxy, groupName));
|
||||
}, [proxy]);
|
||||
|
||||
const onDelay = useLockFn(async () => {
|
||||
setDelay(-2);
|
||||
setDelay(await delayManager.checkDelay(proxy.name, groupName));
|
||||
});
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
dense
|
||||
selected={selected}
|
||||
onClick={() => onClick?.(proxy.name)}
|
||||
sx={[
|
||||
{
|
||||
height: 56,
|
||||
borderRadius: 1,
|
||||
pl: 1.5,
|
||||
pr: 1,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
({ palette: { mode, primary } }) => {
|
||||
const bgcolor =
|
||||
mode === "light"
|
||||
? alpha(primary.main, 0.15)
|
||||
: alpha(primary.main, 0.35);
|
||||
const color = mode === "light" ? primary.main : primary.light;
|
||||
const showDelay = delay > 0;
|
||||
|
||||
const shadowColor =
|
||||
mode === "light" ? "rgba(0,0,0,0.04)" : "rgba(255,255,255,0.08)";
|
||||
|
||||
return {
|
||||
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
|
||||
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
|
||||
"&:hover .the-icon": { display: "none" },
|
||||
"&.Mui-selected": { bgcolor, boxShadow: `0 0 0 1px ${bgcolor}` },
|
||||
"&.Mui-selected .MuiListItemText-secondary": { color },
|
||||
boxShadow: `0 0 0 1px ${shadowColor}`,
|
||||
};
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Box title={proxy.name} sx={{ overflow: "hidden" }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="div"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
display: "block",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-all",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{proxy.name}
|
||||
</Typography>
|
||||
|
||||
{showType && (
|
||||
<Box sx={{ display: "flex", flexWrap: "nowrap", flex: "none" }}>
|
||||
{!!proxy.provider && (
|
||||
<TypeBox component="span">{proxy.provider}</TypeBox>
|
||||
)}
|
||||
<TypeBox component="span">{proxy.type}</TypeBox>
|
||||
{proxy.udp && <TypeBox component="span">UDP</TypeBox>}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ ml: 0.5, color: "primary.main" }}>
|
||||
{delay === -2 && (
|
||||
<Widget>
|
||||
<BaseLoading />
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{!proxy.provider && delay !== -2 && (
|
||||
// provider的节点不支持检测
|
||||
<Widget
|
||||
className="the-check"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
sx={({ palette }) => ({
|
||||
display: "none", // hover才显示
|
||||
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||
})}
|
||||
>
|
||||
Check
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{delay > 0 && (
|
||||
// 显示延迟
|
||||
<Widget
|
||||
className="the-delay"
|
||||
onClick={(e) => {
|
||||
if (proxy.provider) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={delayManager.formatDelayColor(delay)}
|
||||
sx={({ palette }) =>
|
||||
!proxy.provider
|
||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{delayManager.formatDelay(delay)}
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{delay !== -2 && delay <= 0 && selected && (
|
||||
// 展示已选择的icon
|
||||
<CheckCircleOutlineRounded
|
||||
className="the-icon"
|
||||
sx={{ fontSize: 16, mr: 0.5, display: "block" }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
);
|
||||
};
|
||||
|
||||
const Widget = styled(Box)(({ theme: { typography } }) => ({
|
||||
padding: "3px 6px",
|
||||
fontSize: 14,
|
||||
fontFamily: typography.fontFamily,
|
||||
borderRadius: "4px",
|
||||
}));
|
||||
|
||||
const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(palette.text.secondary, 0.36),
|
||||
color: alpha(palette.text.secondary, 0.42),
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
fontFamily: typography.fontFamily,
|
||||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
}));
|
|
@ -145,20 +145,14 @@ export const ProxyItem = (props: Props) => {
|
|||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={
|
||||
delay > 500
|
||||
? "error.main"
|
||||
: delay < 100
|
||||
? "success.main"
|
||||
: "text.secondary"
|
||||
}
|
||||
color={delayManager.formatDelayColor(delay)}
|
||||
sx={({ palette }) =>
|
||||
!proxy.provider
|
||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{delay > 1e5 ? "Error" : delay > 3000 ? "Timeout" : `${delay}ms`}
|
||||
{delayManager.formatDelay(delay)}
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {
|
||||
alpha,
|
||||
Box,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemButton,
|
||||
Typography,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
|
@ -14,6 +14,7 @@ import {
|
|||
import { HeadState } from "./use-head-state";
|
||||
import { ProxyHead } from "./proxy-head";
|
||||
import { ProxyItem } from "./proxy-item";
|
||||
import { ProxyItemMini } from "./proxy-item-mini";
|
||||
import type { IRenderItem } from "./use-render-list";
|
||||
|
||||
interface RenderProps {
|
||||
|
@ -28,19 +29,18 @@ interface RenderProps {
|
|||
export const ProxyRender = (props: RenderProps) => {
|
||||
const { indent, item, onLocation, onCheckAll, onHeadState, onChangeProxy } =
|
||||
props;
|
||||
const { type, group, headState, proxy } = item;
|
||||
const { type, group, headState, proxy, proxyCol } = item;
|
||||
|
||||
if (type === 0) {
|
||||
return (
|
||||
<ListItem
|
||||
button
|
||||
<ListItemButton
|
||||
dense
|
||||
onClick={() => onHeadState(group.name, { open: !headState?.open })}
|
||||
>
|
||||
<ListItemText
|
||||
primary={group.name}
|
||||
secondary={
|
||||
<Box
|
||||
<ListItemTextChild
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
|
@ -50,14 +50,14 @@ export const ProxyRender = (props: RenderProps) => {
|
|||
>
|
||||
<StyledTypeBox>{group.type}</StyledTypeBox>
|
||||
<StyledSubtitle>{group.now}</StyledSubtitle>
|
||||
</Box>
|
||||
</ListItemTextChild>
|
||||
}
|
||||
secondaryTypographyProps={{
|
||||
sx: { display: "flex", alignItems: "center" },
|
||||
}}
|
||||
/>
|
||||
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -105,6 +105,33 @@ export const ProxyRender = (props: RenderProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (type === 4) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: 56,
|
||||
display: "grid",
|
||||
gap: 1,
|
||||
pl: indent ? 4 : 2,
|
||||
pr: 2,
|
||||
pb: 1,
|
||||
gridTemplateColumns: `repeat(${item.col! || 2}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{proxyCol?.map((proxy) => (
|
||||
<ProxyItemMini
|
||||
key={item.key + proxy.name}
|
||||
groupName={group.name}
|
||||
proxy={proxy!}
|
||||
selected={group.now === proxy.name}
|
||||
showType={headState?.showType}
|
||||
onClick={() => onChangeProxy(group, proxy!)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
@ -115,7 +142,11 @@ const StyledSubtitle = styled("span")`
|
|||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||
const ListItemTextChild = styled("span")`
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { atomCurrentProfile } from "@/services/states";
|
||||
import { ProxySortType } from "./use-filter-sort";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
|
||||
export interface HeadState {
|
||||
open?: boolean;
|
||||
|
@ -25,7 +24,8 @@ export const DEFAULT_STATE: HeadState = {
|
|||
};
|
||||
|
||||
export function useHeadStateNew() {
|
||||
const current = useRecoilValue(atomCurrentProfile);
|
||||
const { profiles } = useProfiles();
|
||||
const current = profiles?.current || "";
|
||||
|
||||
const [state, setState] = useState<Record<string, HeadState>>({});
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import useSWR from "swr";
|
||||
import { getProxies } from "@/services/api";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { getProxies } from "@/services/api";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { filterSort } from "./use-filter-sort";
|
||||
import { useWindowWidth } from "./use-window-width";
|
||||
import {
|
||||
useHeadStateNew,
|
||||
DEFAULT_STATE,
|
||||
|
@ -9,10 +11,13 @@ import {
|
|||
} from "./use-head-state";
|
||||
|
||||
export interface IRenderItem {
|
||||
type: 0 | 1 | 2 | 3; // 组 | head | item | empty
|
||||
// 组 | head | item | empty | item col
|
||||
type: 0 | 1 | 2 | 3 | 4;
|
||||
key: string;
|
||||
group: IProxyGroupItem;
|
||||
proxy?: IProxyItem;
|
||||
col?: number;
|
||||
proxyCol?: IProxyItem[];
|
||||
headState?: HeadState;
|
||||
}
|
||||
|
||||
|
@ -23,6 +28,20 @@ export const useRenderList = (mode: string) => {
|
|||
{ refreshInterval: 45000 }
|
||||
);
|
||||
|
||||
const { verge } = useVerge();
|
||||
const { width } = useWindowWidth();
|
||||
|
||||
let col = Math.floor(verge?.proxy_layout_column || 6);
|
||||
|
||||
// 自适应
|
||||
if (col >= 6 || col <= 0) {
|
||||
if (width > 1450) col = 5;
|
||||
else if (width > 1024) col = 4;
|
||||
else if (width > 900) col = 3;
|
||||
else if (width >= 600) col = 2;
|
||||
else col = 1;
|
||||
}
|
||||
|
||||
const [headStates, setHeadState] = useHeadStateNew();
|
||||
|
||||
// make sure that fetch the proxies successfully
|
||||
|
@ -62,10 +81,24 @@ export const useRenderList = (mode: string) => {
|
|||
headState.sortType
|
||||
);
|
||||
|
||||
ret.push({ type: 1, key: `head${group.name}`, group, headState });
|
||||
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
|
||||
|
||||
if (!proxies.length) {
|
||||
ret.push({ type: 3, key: `empty${group.name}`, group, headState });
|
||||
ret.push({ type: 3, key: `empty-${group.name}`, group, headState });
|
||||
}
|
||||
|
||||
// 支持多列布局
|
||||
if (col > 1) {
|
||||
return ret.concat(
|
||||
groupList(proxies, col).map((proxyCol) => ({
|
||||
type: 4,
|
||||
key: `col-${group.name}-${proxyCol[0].name}`,
|
||||
group,
|
||||
headState,
|
||||
col,
|
||||
proxyCol,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return ret.concat(
|
||||
|
@ -83,7 +116,7 @@ export const useRenderList = (mode: string) => {
|
|||
|
||||
if (!useRule) return retList.slice(1);
|
||||
return retList;
|
||||
}, [headStates, proxiesData, mode]);
|
||||
}, [headStates, proxiesData, mode, col]);
|
||||
|
||||
return {
|
||||
renderList,
|
||||
|
@ -91,3 +124,18 @@ export const useRenderList = (mode: string) => {
|
|||
onHeadState: setHeadState,
|
||||
};
|
||||
};
|
||||
|
||||
function groupList<T = any>(list: T[], size: number): T[][] {
|
||||
return list.reduce((p, n) => {
|
||||
if (!p.length) return [[n]];
|
||||
|
||||
const i = p.length - 1;
|
||||
if (p[i].length < size) {
|
||||
p[i].push(n);
|
||||
return p;
|
||||
}
|
||||
|
||||
p.push([n]);
|
||||
return p;
|
||||
}, [] as T[][]);
|
||||
}
|
||||
|
|
16
src/components/proxy/use-window-width.ts
Normal file
16
src/components/proxy/use-window-width.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useWindowWidth = () => {
|
||||
const [width, setWidth] = useState(() => document.body.clientWidth);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setWidth(document.body.clientWidth);
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { width };
|
||||
};
|
|
@ -9,11 +9,9 @@ const Item = styled(Box)(({ theme }) => ({
|
|||
const COLOR = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"info",
|
||||
"warning",
|
||||
"error",
|
||||
"success",
|
||||
"text",
|
||||
"info.main",
|
||||
"warning.main",
|
||||
"success.main",
|
||||
];
|
||||
|
||||
interface Props {
|
||||
|
@ -22,6 +20,9 @@ interface Props {
|
|||
}
|
||||
|
||||
const parseColor = (text: string) => {
|
||||
if (text === "REJECT") return "error.main";
|
||||
if (text === "DIRECT") return "text.primary";
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
sum += text.charCodeAt(i);
|
||||
|
@ -42,7 +43,7 @@ const RuleItem = (props: Props) => {
|
|||
{index}
|
||||
</Typography>
|
||||
|
||||
<Box>
|
||||
<Box sx={{ userSelect: "text" }}>
|
||||
<Typography component="h6" variant="subtitle1" color="text.primary">
|
||||
{value.payload || "-"}
|
||||
</Typography>
|
||||
|
|
132
src/components/setting/mods/clash-core-viewer.tsx
Normal file
132
src/components/setting/mods/clash-core-viewer.tsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
import { mutate } from "swr";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Lock } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { changeClashCore, restartSidecar } from "@/services/cmds";
|
||||
import { closeAllConnections } from "@/services/api";
|
||||
import { grantPermission } from "@/services/cmds";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
const VALID_CORE = [
|
||||
{ name: "Clash", core: "clash" },
|
||||
{ name: "Clash Meta", core: "clash-meta" },
|
||||
];
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { verge, mutateVerge } = useVerge();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const { clash_core = "clash" } = verge ?? {};
|
||||
|
||||
const onCoreChange = useLockFn(async (core: string) => {
|
||||
if (core === clash_core) return;
|
||||
|
||||
try {
|
||||
closeAllConnections();
|
||||
await changeClashCore(core);
|
||||
mutateVerge();
|
||||
setTimeout(() => {
|
||||
mutate("getClashConfig");
|
||||
mutate("getVersion");
|
||||
}, 100);
|
||||
Notice.success(`Successfully switch to ${core}`, 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const onGrant = useLockFn(async (core: string) => {
|
||||
try {
|
||||
await grantPermission(core);
|
||||
// 自动重启
|
||||
if (core === clash_core) await restartSidecar();
|
||||
Notice.success(`Successfully grant permission to ${core}`, 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const onRestart = useLockFn(async () => {
|
||||
try {
|
||||
await restartSidecar();
|
||||
Notice.success(`Successfully restart core`, 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Clash Core")}
|
||||
|
||||
<Button variant="contained" size="small" onClick={onRestart}>
|
||||
{t("Restart")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{
|
||||
pb: 0,
|
||||
width: 320,
|
||||
height: 200,
|
||||
overflowY: "auto",
|
||||
userSelect: "text",
|
||||
marginTop: "-8px",
|
||||
}}
|
||||
disableOk
|
||||
cancelBtn={t("Back")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List component="nav">
|
||||
{VALID_CORE.map((each) => (
|
||||
<ListItemButton
|
||||
key={each.core}
|
||||
selected={each.core === clash_core}
|
||||
onClick={() => onCoreChange(each.core)}
|
||||
>
|
||||
<ListItemText primary={each.name} secondary={`/${each.core}`} />
|
||||
|
||||
{(OS === "macos" || OS === "linux") && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
edge="end"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onGrant(each.core);
|
||||
}}
|
||||
>
|
||||
<Lock fontSize="inherit" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
|
@ -13,17 +13,8 @@ import { BaseDialog, DialogRef } from "@/components/base";
|
|||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { Notice } from "@/components/base";
|
||||
|
||||
const fieldSorter = (a: string, b: string) => {
|
||||
if (a.includes("-") === a.includes("-")) {
|
||||
if (a.length === b.length) return a.localeCompare(b);
|
||||
return a.length - b.length;
|
||||
} else if (a.includes("-")) return 1;
|
||||
else if (b.includes("-")) return -1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const otherFields = [...OTHERS_FIELDS].sort(fieldSorter);
|
||||
const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS].sort(fieldSorter);
|
||||
const otherFields = [...OTHERS_FIELDS];
|
||||
const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS];
|
||||
|
||||
export const ClashFieldViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
|
@ -64,7 +64,7 @@ export const ConfigViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
{t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" />
|
||||
</>
|
||||
}
|
||||
contentSx={{ width: 520, pb: 1 }}
|
||||
contentSx={{ width: 520, pb: 1, userSelect: "text" }}
|
||||
cancelBtn={t("Back")}
|
||||
disableOk
|
||||
onClose={() => setOpen(false)}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
import { mutate } from "swr";
|
||||
import { useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { IconButton, Menu, MenuItem } from "@mui/material";
|
||||
import { Settings } from "@mui/icons-material";
|
||||
import { changeClashCore } from "@/services/cmds";
|
||||
import { closeAllConnections } from "@/services/api";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { Notice } from "@/components/base";
|
||||
|
||||
const VALID_CORE = [
|
||||
{ name: "Clash", core: "clash" },
|
||||
{ name: "Clash Meta", core: "clash-meta" },
|
||||
];
|
||||
|
||||
export const CoreSwitch = () => {
|
||||
const { verge, mutateVerge } = useVerge();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||
|
||||
const { clash_core = "clash" } = verge ?? {};
|
||||
|
||||
const onCoreChange = useLockFn(async (core: string) => {
|
||||
if (core === clash_core) return;
|
||||
|
||||
try {
|
||||
closeAllConnections();
|
||||
await changeClashCore(core);
|
||||
mutateVerge();
|
||||
setTimeout(() => {
|
||||
mutate("getClashConfig");
|
||||
mutate("getVersion");
|
||||
}, 100);
|
||||
setAnchorEl(null);
|
||||
Notice.success(`Successfully switch to ${core}`, 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
const { clientX, clientY } = event;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
setAnchorEl(event.currentTarget);
|
||||
}}
|
||||
>
|
||||
<Settings
|
||||
fontSize="inherit"
|
||||
style={{ cursor: "pointer", opacity: 0.75 }}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
open={!!anchorEl}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
anchorPosition={position}
|
||||
anchorReference="anchorPosition"
|
||||
transitionDuration={225}
|
||||
onContextMenu={(e) => {
|
||||
setAnchorEl(null);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{VALID_CORE.map((each) => (
|
||||
<MenuItem
|
||||
key={each.core}
|
||||
sx={{ minWidth: 125 }}
|
||||
selected={each.core === clash_core}
|
||||
onClick={() => onCoreChange(each.core)}
|
||||
>
|
||||
{each.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -14,6 +14,7 @@ const ItemWrapper = styled("div")`
|
|||
`;
|
||||
|
||||
const HOTKEY_FUNC = [
|
||||
"open_dashboard",
|
||||
"clash_mode_rule",
|
||||
"clash_mode_global",
|
||||
"clash_mode_direct",
|
||||
|
|
80
src/components/setting/mods/layout-viewer.tsx
Normal file
80
src/components/setting/mods/layout-viewer.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { List, Switch } from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { SettingItem } from "./setting-comp";
|
||||
import { GuardState } from "./guard-state";
|
||||
|
||||
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onError = (err: any) => {
|
||||
Notice.error(err.message || err.toString());
|
||||
};
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Layout Setting")}
|
||||
contentSx={{ width: 450 }}
|
||||
disableOk
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List>
|
||||
<SettingItem label={t("Theme Blur")}>
|
||||
<GuardState
|
||||
value={verge?.theme_blur ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ theme_blur: e })}
|
||||
onGuard={(e) => patchVerge({ theme_blur: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Traffic Graph")}>
|
||||
<GuardState
|
||||
value={verge?.traffic_graph ?? true}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ traffic_graph: e })}
|
||||
onGuard={(e) => patchVerge({ traffic_graph: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Memory Usage")}>
|
||||
<GuardState
|
||||
value={verge?.enable_memory_usage ?? true}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ enable_memory_usage: e })}
|
||||
onGuard={(e) => patchVerge({ enable_memory_usage: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
</List>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
|
@ -1,7 +1,15 @@
|
|||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { List, ListItem, ListItemText, Switch, TextField } from "@mui/material";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
|
||||
|
@ -11,16 +19,26 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [values, setValues] = useState({
|
||||
appLogLevel: "info",
|
||||
autoCloseConnection: false,
|
||||
enableClashFields: true,
|
||||
enableBuiltinEnhanced: true,
|
||||
proxyLayoutColumn: 6,
|
||||
defaultLatencyTest: "",
|
||||
autoLogClean: 0,
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setOpen(true);
|
||||
setValues({
|
||||
autoCloseConnection: verge?.auto_close_connection || false,
|
||||
appLogLevel: verge?.app_log_level ?? "info",
|
||||
autoCloseConnection: verge?.auto_close_connection ?? false,
|
||||
enableClashFields: verge?.enable_clash_fields ?? true,
|
||||
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
||||
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
||||
defaultLatencyTest: verge?.default_latency_test || "",
|
||||
autoLogClean: verge?.auto_log_clean || 0,
|
||||
});
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
|
@ -29,8 +47,13 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
const onSave = useLockFn(async () => {
|
||||
try {
|
||||
await patchVerge({
|
||||
app_log_level: values.appLogLevel,
|
||||
auto_close_connection: values.autoCloseConnection,
|
||||
enable_clash_fields: values.enableClashFields,
|
||||
enable_builtin_enhanced: values.enableBuiltinEnhanced,
|
||||
proxy_layout_column: values.proxyLayoutColumn,
|
||||
default_latency_test: values.defaultLatencyTest,
|
||||
auto_log_clean: values.autoLogClean as any,
|
||||
});
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
|
@ -42,7 +65,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Miscellaneous")}
|
||||
contentSx={{ width: 420 }}
|
||||
contentSx={{ width: 450 }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
|
@ -51,7 +74,28 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
>
|
||||
<List>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Auto Close Connections" />
|
||||
<ListItemText primary={t("App Log Level")} />
|
||||
<Select
|
||||
size="small"
|
||||
sx={{ width: 100, "> div": { py: "7.5px" } }}
|
||||
value={values.appLogLevel}
|
||||
onChange={(e) => {
|
||||
setValues((v) => ({
|
||||
...v,
|
||||
appLogLevel: e.target.value as string,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{["trace", "debug", "info", "warn", "error", "silent"].map((i) => (
|
||||
<MenuItem value={i} key={i}>
|
||||
{i[0].toUpperCase() + i.slice(1).toLowerCase()}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Auto Close Connections")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.autoCloseConnection}
|
||||
|
@ -62,14 +106,86 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Default Latency Test" />
|
||||
<ListItemText primary={t("Enable Clash Fields Filter")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.enableClashFields}
|
||||
onChange={(_, c) =>
|
||||
setValues((v) => ({ ...v, enableClashFields: c }))
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Enable Builtin Enhanced")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.enableBuiltinEnhanced}
|
||||
onChange={(_, c) =>
|
||||
setValues((v) => ({ ...v, enableBuiltinEnhanced: c }))
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Proxy Layout Column")} />
|
||||
<Select
|
||||
size="small"
|
||||
sx={{ width: 135, "> div": { py: "7.5px" } }}
|
||||
value={values.proxyLayoutColumn}
|
||||
onChange={(e) => {
|
||||
setValues((v) => ({
|
||||
...v,
|
||||
proxyLayoutColumn: e.target.value as number,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<MenuItem value={6} key={6}>
|
||||
Auto
|
||||
</MenuItem>
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<MenuItem value={i} key={i}>
|
||||
{i}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Auto Log Clean")} />
|
||||
<Select
|
||||
size="small"
|
||||
sx={{ width: 135, "> div": { py: "7.5px" } }}
|
||||
value={values.autoLogClean}
|
||||
onChange={(e) => {
|
||||
setValues((v) => ({
|
||||
...v,
|
||||
autoLogClean: e.target.value as number,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ key: "Never Clean", value: 0 },
|
||||
{ key: "Retain 7 Days", value: 1 },
|
||||
{ key: "Retain 30 Days", value: 2 },
|
||||
{ key: "Retain 90 Days", value: 3 },
|
||||
].map((i) => (
|
||||
<MenuItem key={i.value} value={i.value}>
|
||||
{t(i.key)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Default Latency Test")} />
|
||||
<TextField
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
sx={{ width: 200 }}
|
||||
sx={{ width: 250 }}
|
||||
value={values.defaultLatencyTest}
|
||||
placeholder="http://www.gstatic.com/generate_204"
|
||||
onChange={(e) =>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
|
@ -8,12 +8,14 @@ import {
|
|||
} from "@mui/material";
|
||||
|
||||
interface ItemProps {
|
||||
label: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
label: ReactNode;
|
||||
extra?: ReactNode;
|
||||
children?: ReactNode;
|
||||
secondary?: ReactNode;
|
||||
}
|
||||
|
||||
export const SettingItem: React.FC<ItemProps> = (props) => {
|
||||
const { label, extra, children } = props;
|
||||
const { label, extra, children, secondary } = props;
|
||||
|
||||
const primary = !extra ? (
|
||||
label
|
||||
|
@ -26,13 +28,16 @@ export const SettingItem: React.FC<ItemProps> = (props) => {
|
|||
|
||||
return (
|
||||
<ListItem sx={{ pt: "5px", pb: "5px" }}>
|
||||
<ListItemText primary={primary} />
|
||||
<ListItemText primary={primary} secondary={secondary} />
|
||||
{children}
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingList: React.FC<{ title: string }> = (props) => (
|
||||
export const SettingList: React.FC<{
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}> = (props) => (
|
||||
<List>
|
||||
<ListSubheader sx={{ background: "transparent" }} disableSticky>
|
||||
{props.title}
|
||||
|
|
|
@ -1,43 +1,45 @@
|
|||
import useSWR from "swr";
|
||||
import snarkdown from "snarkdown";
|
||||
import { useMemo } from "react";
|
||||
import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, styled } from "@mui/material";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
import { relaunch } from "@tauri-apps/api/process";
|
||||
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { atomUpdateState } from "@/services/states";
|
||||
import { Notice } from "@/components/base";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const UpdateLog = styled(Box)(() => ({
|
||||
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
|
||||
}));
|
||||
|
||||
const UpdateDialog = (props: Props) => {
|
||||
const { open, onClose } = props;
|
||||
export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
|
||||
|
||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
});
|
||||
|
||||
const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const onUpdate = async () => {
|
||||
// markdown parser
|
||||
const parseContent = useMemo(() => {
|
||||
if (!updateInfo?.manifest?.body) {
|
||||
return "New Version is available";
|
||||
}
|
||||
return snarkdown(updateInfo?.manifest?.body);
|
||||
}, [updateInfo]);
|
||||
|
||||
const onUpdate = useLockFn(async () => {
|
||||
if (updateState) return;
|
||||
setUpdateState(true);
|
||||
|
||||
|
@ -49,39 +51,20 @@ const UpdateDialog = (props: Props) => {
|
|||
} finally {
|
||||
setUpdateState(false);
|
||||
}
|
||||
};
|
||||
|
||||
// markdown parser
|
||||
const parseContent = useMemo(() => {
|
||||
if (!updateInfo?.manifest?.body) {
|
||||
return "New Version is available";
|
||||
}
|
||||
return snarkdown(updateInfo?.manifest?.body);
|
||||
}, [updateInfo]);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>New Version v{updateInfo?.manifest?.version}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}>
|
||||
<UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={onClose}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
autoFocus
|
||||
variant="contained"
|
||||
disabled={updateState}
|
||||
onClick={onUpdate}
|
||||
>
|
||||
{t("Update")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={`New Version v${updateInfo?.manifest?.version}`}
|
||||
contentSx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}
|
||||
okBtn={t("Update")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onUpdate}
|
||||
>
|
||||
<UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateDialog;
|
||||
});
|
|
@ -60,7 +60,10 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
.trim();
|
||||
|
||||
url = url.replaceAll("%port", port || "9090");
|
||||
url = url.replaceAll("%secret", clashInfo.secret || "");
|
||||
url = url.replaceAll(
|
||||
"%secret",
|
||||
encodeURIComponent(clashInfo.secret || "")
|
||||
);
|
||||
}
|
||||
|
||||
await openWebUrl(url);
|
||||
|
|
|
@ -8,16 +8,16 @@ import {
|
|||
Typography,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import { ArrowForward } from "@mui/icons-material";
|
||||
import { ArrowForward, Settings } from "@mui/icons-material";
|
||||
import { DialogRef } from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { GuardState } from "./mods/guard-state";
|
||||
import { CoreSwitch } from "./mods/core-switch";
|
||||
import { WebUIViewer } from "./mods/web-ui-viewer";
|
||||
import { ClashFieldViewer } from "./mods/clash-field-viewer";
|
||||
import { ClashPortViewer } from "./mods/clash-port-viewer";
|
||||
import { ControllerViewer } from "./mods/controller-viewer";
|
||||
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||
import { ClashCoreViewer } from "./mods/clash-core-viewer";
|
||||
|
||||
interface Props {
|
||||
onError: (err: Error) => void;
|
||||
|
@ -39,6 +39,7 @@ const SettingClash = ({ onError }: Props) => {
|
|||
const fieldRef = useRef<DialogRef>(null);
|
||||
const portRef = useRef<DialogRef>(null);
|
||||
const ctrlRef = useRef<DialogRef>(null);
|
||||
const coreRef = useRef<DialogRef>(null);
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onChangeData = (patch: Partial<IConfigData>) => {
|
||||
|
@ -51,6 +52,7 @@ const SettingClash = ({ onError }: Props) => {
|
|||
<ClashFieldViewer ref={fieldRef} />
|
||||
<ClashPortViewer ref={portRef} />
|
||||
<ControllerViewer ref={ctrlRef} />
|
||||
<ClashCoreViewer ref={coreRef} />
|
||||
|
||||
<SettingItem label={t("Allow Lan")}>
|
||||
<GuardState
|
||||
|
@ -143,7 +145,21 @@ const SettingClash = ({ onError }: Props) => {
|
|||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Clash Core")} extra={<CoreSwitch />}>
|
||||
<SettingItem
|
||||
label={t("Clash Core")}
|
||||
extra={
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => coreRef.current?.open()}
|
||||
>
|
||||
<Settings
|
||||
fontSize="inherit"
|
||||
style={{ cursor: "pointer", opacity: 0.75 }}
|
||||
/>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Typography sx={{ py: "7px", pr: 1 }}>{version}</Typography>
|
||||
</SettingItem>
|
||||
</SettingList>
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
import { useRef } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { openAppDir, openLogsDir, patchVergeConfig } from "@/services/cmds";
|
||||
import { IconButton, MenuItem, Select, Typography } from "@mui/material";
|
||||
import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
|
||||
import { ArrowForward } from "@mui/icons-material";
|
||||
import { checkUpdate } from "@tauri-apps/api/updater";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { version } from "@root/package.json";
|
||||
import { DialogRef } from "@/components/base";
|
||||
import { DialogRef, Notice } from "@/components/base";
|
||||
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||
import { ThemeModeSwitch } from "./mods/theme-mode-switch";
|
||||
import { ConfigViewer } from "./mods/config-viewer";
|
||||
|
@ -19,34 +15,54 @@ import { HotkeyViewer } from "./mods/hotkey-viewer";
|
|||
import { MiscViewer } from "./mods/misc-viewer";
|
||||
import { ThemeViewer } from "./mods/theme-viewer";
|
||||
import { GuardState } from "./mods/guard-state";
|
||||
import { LayoutViewer } from "./mods/layout-viewer";
|
||||
import { UpdateViewer } from "./mods/update-viewer";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
interface Props {
|
||||
onError?: (err: Error) => void;
|
||||
}
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
const SettingVerge = ({ onError }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||
|
||||
const { theme_mode, theme_blur, traffic_graph, language } = verge ?? {};
|
||||
const { theme_mode, language } = verge ?? {};
|
||||
|
||||
const configRef = useRef<DialogRef>(null);
|
||||
const hotkeyRef = useRef<DialogRef>(null);
|
||||
const miscRef = useRef<DialogRef>(null);
|
||||
const themeRef = useRef<DialogRef>(null);
|
||||
const layoutRef = useRef<DialogRef>(null);
|
||||
const updateRef = useRef<DialogRef>(null);
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
};
|
||||
|
||||
const onCheckUpdate = useLockFn(async () => {
|
||||
try {
|
||||
const info = await checkUpdate();
|
||||
if (!info?.shouldUpdate) {
|
||||
Notice.success("No Updates Available");
|
||||
} else {
|
||||
updateRef.current?.open();
|
||||
}
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingList title={t("Verge Setting")}>
|
||||
<ThemeViewer ref={themeRef} />
|
||||
<ConfigViewer ref={configRef} />
|
||||
<HotkeyViewer ref={hotkeyRef} />
|
||||
<MiscViewer ref={miscRef} />
|
||||
<LayoutViewer ref={layoutRef} />
|
||||
<UpdateViewer ref={updateRef} />
|
||||
|
||||
<SettingItem label={t("Language")}>
|
||||
<GuardState
|
||||
|
@ -59,6 +75,7 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
<Select size="small" sx={{ width: 100, "> div": { py: "7.5px" } }}>
|
||||
<MenuItem value="zh">中文</MenuItem>
|
||||
<MenuItem value="en">English</MenuItem>
|
||||
<MenuItem value="ru">Русский</MenuItem>
|
||||
</Select>
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
@ -74,30 +91,26 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Theme Blur")}>
|
||||
<GuardState
|
||||
value={theme_blur ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ theme_blur: e })}
|
||||
onGuard={(e) => patchVerge({ theme_blur: e })}
|
||||
<SettingItem label={t("Theme Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => themeRef.current?.open()}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Traffic Graph")}>
|
||||
<GuardState
|
||||
value={traffic_graph ?? true}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ traffic_graph: e })}
|
||||
onGuard={(e) => patchVergeConfig({ traffic_graph: e })}
|
||||
<SettingItem label={t("Layout Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => layoutRef.current?.open()}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Miscellaneous")}>
|
||||
|
@ -111,17 +124,6 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Theme Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => themeRef.current?.open()}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Hotkey Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
|
@ -155,6 +157,17 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Open Core Dir")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={openCoreDir}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Open Logs Dir")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
|
@ -166,6 +179,19 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
{!(OS === "windows" && WIN_PORTABLE) && (
|
||||
<SettingItem label={t("Check for Updates")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={onCheckUpdate}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem label={t("Verge Version")}>
|
||||
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
|
||||
</SettingItem>
|
||||
|
|
|
@ -1,29 +1,74 @@
|
|||
import useSWR from "swr";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import {
|
||||
getProfiles,
|
||||
patchProfile,
|
||||
patchProfilesConfig,
|
||||
} from "@/services/cmds";
|
||||
import { getProxies, updateProxy } from "@/services/api";
|
||||
|
||||
export const useProfiles = () => {
|
||||
const { data: profiles, mutate } = useSWR("getProfiles", getProfiles);
|
||||
const { data: profiles, mutate: mutateProfiles } = useSWR(
|
||||
"getProfiles",
|
||||
getProfiles
|
||||
);
|
||||
|
||||
const patchProfiles = async (value: Partial<IProfilesConfig>) => {
|
||||
await patchProfilesConfig(value);
|
||||
mutate();
|
||||
mutateProfiles();
|
||||
};
|
||||
|
||||
const patchCurrent = async (value: Partial<IProfileItem>) => {
|
||||
if (profiles?.current) {
|
||||
await patchProfile(profiles.current, value);
|
||||
mutate();
|
||||
mutateProfiles();
|
||||
}
|
||||
};
|
||||
|
||||
// 根据selected的节点选择
|
||||
const activateSelected = async () => {
|
||||
const proxiesData = await getProxies();
|
||||
const profileData = await getProfiles();
|
||||
|
||||
if (!profileData || !proxiesData) return;
|
||||
|
||||
const current = profileData.items?.find(
|
||||
(e) => e && e.uid === profileData.current
|
||||
);
|
||||
|
||||
if (!current) return;
|
||||
|
||||
// init selected array
|
||||
const { selected = [] } = current;
|
||||
const selectedMap = Object.fromEntries(
|
||||
selected.map((each) => [each.name!, each.now!])
|
||||
);
|
||||
|
||||
let hasChange = false;
|
||||
|
||||
const newSelected: typeof selected = [];
|
||||
const { global, groups } = proxiesData;
|
||||
|
||||
[global, ...groups].forEach(({ type, name, now }) => {
|
||||
if (!now || type !== "Selector") return;
|
||||
if (selectedMap[name] != null && selectedMap[name] !== now) {
|
||||
hasChange = true;
|
||||
updateProxy(name, selectedMap[name]);
|
||||
}
|
||||
newSelected.push({ name, now: selectedMap[name] });
|
||||
});
|
||||
|
||||
if (hasChange) {
|
||||
patchProfile(profileData.current!, { selected: newSelected });
|
||||
mutate("getProxies", getProxies());
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
profiles,
|
||||
current: profiles?.items?.find((p) => p.uid === profiles.current),
|
||||
current: profiles?.items?.find((p) => p && p.uid === profiles.current),
|
||||
activateSelected,
|
||||
patchProfiles,
|
||||
patchCurrent,
|
||||
mutateProfiles,
|
||||
};
|
||||
};
|
||||
|
|
27
src/hooks/use-visibility.ts
Normal file
27
src/hooks/use-visibility.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useVisibility = () => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
setVisible(document.visibilityState === "visible");
|
||||
};
|
||||
|
||||
const handleFocus = () => setVisible(true);
|
||||
const handleClick = () => setVisible(true);
|
||||
|
||||
handleVisibilityChange();
|
||||
document.addEventListener("focus", handleFocus);
|
||||
document.addEventListener("pointerdown", handleClick);
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("focus", handleFocus);
|
||||
document.removeEventListener("pointerdown", handleClick);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return visible;
|
||||
};
|
53
src/hooks/use-websocket.ts
Normal file
53
src/hooks/use-websocket.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { useRef } from "react";
|
||||
|
||||
export type WsMsgFn = (event: MessageEvent<any>) => void;
|
||||
|
||||
export interface WsOptions {
|
||||
errorCount?: number; // default is 5
|
||||
retryInterval?: number; // default is 2500
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const timerRef = useRef<any>(null);
|
||||
|
||||
const disconnect = () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const connect = (url: string) => {
|
||||
let errorCount = options?.errorCount ?? 5;
|
||||
|
||||
if (!url) return;
|
||||
|
||||
const connectHelper = () => {
|
||||
disconnect();
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.addEventListener("message", onMessage);
|
||||
ws.addEventListener("error", () => {
|
||||
errorCount -= 1;
|
||||
|
||||
if (errorCount >= 0) {
|
||||
timerRef.current = setTimeout(connectHelper, 2500);
|
||||
} else {
|
||||
disconnect();
|
||||
options?.onError?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
connectHelper();
|
||||
};
|
||||
|
||||
return { connect, disconnect };
|
||||
};
|
|
@ -15,6 +15,7 @@
|
|||
"global": "global",
|
||||
"direct": "direct",
|
||||
"script": "script",
|
||||
|
||||
"Profiles": "Profiles",
|
||||
"Profile URL": "Profile URL",
|
||||
"Import": "Import",
|
||||
|
@ -34,6 +35,9 @@
|
|||
"Refresh": "Refresh",
|
||||
"To Top": "To Top",
|
||||
"To End": "To End",
|
||||
"Update All Profiles": "Update All Profiles",
|
||||
"View Runtime Config": "View Runtime Config",
|
||||
"Reactivate Profiles": "Reactivate Profiles",
|
||||
|
||||
"Location": "Location",
|
||||
"Delay check": "Delay check",
|
||||
|
@ -50,7 +54,9 @@
|
|||
"Name": "Name",
|
||||
"Descriptions": "Descriptions",
|
||||
"Subscription URL": "Subscription URL",
|
||||
"Update Interval(mins)": "Update Interval(mins)",
|
||||
"Update Interval": "Update Interval",
|
||||
"Use System Proxy": "Use System Proxy",
|
||||
"Use Clash Proxy": "Use Clash Proxy",
|
||||
|
||||
"Settings": "Settings",
|
||||
"Clash Setting": "Clash Setting",
|
||||
|
@ -60,6 +66,7 @@
|
|||
"IPv6": "IPv6",
|
||||
"Log Level": "Log Level",
|
||||
"Mixed Port": "Mixed Port",
|
||||
"External": "External",
|
||||
"Clash Core": "Clash Core",
|
||||
"Tun Mode": "Tun Mode",
|
||||
"Service Mode": "Service Mode",
|
||||
|
@ -74,11 +81,16 @@
|
|||
"Theme Mode": "Theme Mode",
|
||||
"Theme Blur": "Theme Blur",
|
||||
"Theme Setting": "Theme Setting",
|
||||
"Layout Setting": "Layout Setting",
|
||||
"Miscellaneous": "Miscellaneous",
|
||||
"Hotkey Setting": "Hotkey Setting",
|
||||
"Traffic Graph": "Traffic Graph",
|
||||
"Memory Usage": "Memory Usage",
|
||||
"Language": "Language",
|
||||
"Open App Dir": "Open App Dir",
|
||||
"Open Core Dir": "Open Core Dir",
|
||||
"Open Logs Dir": "Open Logs Dir",
|
||||
"Check for Updates": "Check for Updates",
|
||||
"Verge Version": "Verge Version",
|
||||
"theme.light": "Light",
|
||||
"theme.dark": "Dark",
|
||||
|
@ -86,11 +98,17 @@
|
|||
"Clash Field": "Clash Field",
|
||||
"Runtime Config": "Runtime Config",
|
||||
"ReadOnly": "ReadOnly",
|
||||
"Restart": "Restart",
|
||||
|
||||
"Back": "Back",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
|
||||
"Default": "Default",
|
||||
"Download Speed": "Download Speed",
|
||||
"Upload Speed": "Upload Speed",
|
||||
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"clash_mode_rule": "Rule Mode",
|
||||
"clash_mode_global": "Global Mode",
|
||||
"clash_mode_direct": "Direct Mode",
|
||||
|
@ -100,5 +118,18 @@
|
|||
"disable_system_proxy": "Disable System Proxy",
|
||||
"toggle_tun_mode": "Toggle Tun Mode",
|
||||
"enable_tun_mode": "Enable Tun Mode",
|
||||
"disable_tun_mode": "Disable Tun Mode"
|
||||
"disable_tun_mode": "Disable Tun Mode",
|
||||
|
||||
"App Log Level": "App Log Level",
|
||||
"Auto Close Connections": "Auto Close Connections",
|
||||
"Enable Clash Fields Filter": "Enable Clash Fields Filter",
|
||||
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
|
||||
"Proxy Layout Column": "Proxy Layout Column",
|
||||
"Default Latency Test": "Default Latency Test",
|
||||
|
||||
"Auto Log Clean": "Auto Log Clean",
|
||||
"Never Clean": "Never Clean",
|
||||
"Retain 7 Days": "Retain 7 Days",
|
||||
"Retain 30 Days": "Retain 30 Days",
|
||||
"Retain 90 Days": "Retain 90 Days"
|
||||
}
|
||||
|
|
111
src/locales/ru.json
Normal file
111
src/locales/ru.json
Normal file
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"Label-Proxies": "Прокси",
|
||||
"Label-Profiles": "Профили",
|
||||
"Label-Connections": "Соединения",
|
||||
"Label-Logs": "Логи",
|
||||
"Label-Rules": "Правила",
|
||||
"Label-Settings": "Настройки",
|
||||
|
||||
"Connections": "Соединения",
|
||||
"Logs": "Логи",
|
||||
"Clear": "Очистить",
|
||||
"Proxies": "Прокси",
|
||||
"Proxy Groups": "Группы прокси",
|
||||
"rule": "правила",
|
||||
"global": "глобальный",
|
||||
"direct": "прямой",
|
||||
"script": "скриптовый",
|
||||
|
||||
"Profiles": "Профили",
|
||||
"Profile URL": "URL профиля",
|
||||
"Import": "Импорт",
|
||||
"New": "Новый",
|
||||
"Create Profile": "Создать профиль",
|
||||
"Choose File": "Выбрать файл",
|
||||
"Close All": "Закрыть всё",
|
||||
"Select": "Выбрать",
|
||||
"Edit Info": "Изменить информацию",
|
||||
"Edit File": "Изменить файл",
|
||||
"Open File": "Открыть файл",
|
||||
"Update": "Обновить",
|
||||
"Update(Proxy)": "Обновить (прокси)",
|
||||
"Delete": "Удалить",
|
||||
"Enable": "Включить",
|
||||
"Disable": "Отключить",
|
||||
"Refresh": "Обновить",
|
||||
"To Top": "Наверх",
|
||||
"To End": "Вниз",
|
||||
"Update All Profiles": "Обновить все профили",
|
||||
"View Runtime Config": "Просмотреть используемый конфиг",
|
||||
"Reactivate Profiles": "Реактивировать профили",
|
||||
|
||||
"Location": "Местоположение",
|
||||
"Delay check": "Проверка задержки",
|
||||
"Sort by default": "Сортировать по умолчанию",
|
||||
"Sort by delay": "Сортировать по задержке",
|
||||
"Sort by name": "Сортировать по названию",
|
||||
"Delay check URL": "URL проверки задержки",
|
||||
"Proxy detail": "Подробности о прокси",
|
||||
"Filter": "Фильтр",
|
||||
"Filter conditions": "Условия фильтрации",
|
||||
"Refresh profiles": "Обновить профили",
|
||||
|
||||
"Type": "Тип",
|
||||
"Name": "Название",
|
||||
"Descriptions": "Описания",
|
||||
"Subscription URL": "URL подписки",
|
||||
"Update Interval": "Интервал обновления",
|
||||
|
||||
"Settings": "Настройки",
|
||||
"Clash Setting": "Настройки Clash",
|
||||
"System Setting": "Настройки системы",
|
||||
"Verge Setting": "Настройки Verge",
|
||||
"Allow Lan": "Разрешить локальную сеть",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "Уровень логов",
|
||||
"Mixed Port": "Смешанный порт",
|
||||
"Clash Core": "Ядро Clash",
|
||||
"Tun Mode": "Режим туннеля",
|
||||
"Service Mode": "Режим сервиса",
|
||||
"Auto Launch": "Автозапуск",
|
||||
"Silent Start": "Тихий запуск",
|
||||
"System Proxy": "Системный прокси",
|
||||
"System Proxy Setting": "Настройка системного прокси",
|
||||
"Proxy Guard": "Защита прокси",
|
||||
"Guard Duration": "Период защиты",
|
||||
"Proxy Bypass": "Игнорирование прокси",
|
||||
"Current System Proxy": "Текущий системный прокси",
|
||||
"Theme Mode": "Режим темы",
|
||||
"Theme Blur": "Размытие темы",
|
||||
"Theme Setting": "Настройка темы",
|
||||
"Hotkey Setting": "Настройка клавиатурных сокращений",
|
||||
"Traffic Graph": "График трафика",
|
||||
"Language": "Язык",
|
||||
"Open App Dir": "Открыть папку приложения",
|
||||
"Open Core Dir": "Открыть папку ядра",
|
||||
"Open Logs Dir": "Открыть папку логов",
|
||||
"Verge Version": "Версия Verge",
|
||||
"theme.light": "Светлая",
|
||||
"theme.dark": "Тёмная",
|
||||
"theme.system": "Системная",
|
||||
"Clash Field": "Используемые настройки Clash",
|
||||
"Runtime Config": "Используемый конфиг",
|
||||
"ReadOnly": "Только для чтения",
|
||||
"Restart": "Перезапуск",
|
||||
|
||||
"Back": "Назад",
|
||||
"Save": "Сохранить",
|
||||
"Cancel": "Отмена",
|
||||
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"clash_mode_rule": "Режим правил",
|
||||
"clash_mode_global": "Глобальный режим",
|
||||
"clash_mode_direct": "Прямой режим",
|
||||
"clash_mode_script": "Скриптовый режим",
|
||||
"toggle_system_proxy": "Переключить режим системного прокси",
|
||||
"enable_system_proxy": "Включить системный прокси",
|
||||
"disable_system_proxy": "Отключить системный прокси",
|
||||
"toggle_tun_mode": "Переключить режим туннеля",
|
||||
"enable_tun_mode": "Включить режим туннеля",
|
||||
"disable_tun_mode": "Отключить режим туннеля"
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
"global": "全局",
|
||||
"direct": "直连",
|
||||
"script": "脚本",
|
||||
|
||||
"Profiles": "配置",
|
||||
"Profile URL": "配置文件链接",
|
||||
"Import": "导入",
|
||||
|
@ -34,6 +35,9 @@
|
|||
"Refresh": "刷新",
|
||||
"To Top": "移到最前",
|
||||
"To End": "移到末尾",
|
||||
"Update All Profiles": "更新所有配置",
|
||||
"View Runtime Config": "查看运行时配置",
|
||||
"Reactivate Profiles": "重新激活配置",
|
||||
|
||||
"Location": "当前节点",
|
||||
"Delay check": "延迟测试",
|
||||
|
@ -50,7 +54,9 @@
|
|||
"Name": "名称",
|
||||
"Descriptions": "描述",
|
||||
"Subscription URL": "订阅链接",
|
||||
"Update Interval(mins)": "更新间隔(分钟)",
|
||||
"Update Interval": "更新间隔",
|
||||
"Use System Proxy": "使用系统代理更新",
|
||||
"Use Clash Proxy": "使用Clash代理更新",
|
||||
|
||||
"Settings": "设置",
|
||||
"Clash Setting": "Clash 设置",
|
||||
|
@ -74,13 +80,17 @@
|
|||
"Current System Proxy": "当前系统代理",
|
||||
"Theme Mode": "主题模式",
|
||||
"Theme Blur": "背景模糊",
|
||||
"Miscellaneous": "杂项设置",
|
||||
"Theme Setting": "主题设置",
|
||||
"Layout Setting": "界面设置",
|
||||
"Miscellaneous": "杂项设置",
|
||||
"Hotkey Setting": "热键设置",
|
||||
"Traffic Graph": "流量图显",
|
||||
"Memory Usage": "内存使用",
|
||||
"Language": "语言设置",
|
||||
"Open App Dir": "应用目录",
|
||||
"Open Core Dir": "内核目录",
|
||||
"Open Logs Dir": "日志目录",
|
||||
"Check for Updates": "检查更新",
|
||||
"Verge Version": "应用版本",
|
||||
"theme.light": "浅色",
|
||||
"theme.dark": "深色",
|
||||
|
@ -88,6 +98,7 @@
|
|||
"Clash Field": "Clash 字段",
|
||||
"Runtime Config": "运行配置",
|
||||
"ReadOnly": "只读",
|
||||
"Restart": "重启内核",
|
||||
|
||||
"Back": "返回",
|
||||
"Save": "保存",
|
||||
|
@ -97,6 +108,7 @@
|
|||
"Download Speed": "下载速度",
|
||||
"Upload Speed": "上传速度",
|
||||
|
||||
"open_dashboard": "打开面板",
|
||||
"clash_mode_rule": "规则模式",
|
||||
"clash_mode_global": "全局模式",
|
||||
"clash_mode_direct": "直连模式",
|
||||
|
@ -106,5 +118,18 @@
|
|||
"disable_system_proxy": "关闭系统代理",
|
||||
"toggle_tun_mode": "切换Tun模式",
|
||||
"enable_tun_mode": "开启Tun模式",
|
||||
"disable_tun_mode": "关闭Tun模式"
|
||||
"disable_tun_mode": "关闭Tun模式",
|
||||
|
||||
"App Log Level": "App日志等级",
|
||||
"Auto Close Connections": "自动关闭连接",
|
||||
"Enable Clash Fields Filter": "开启Clash字段过滤",
|
||||
"Enable Builtin Enhanced": "开启内建增强功能",
|
||||
"Proxy Layout Column": "代理页布局列数",
|
||||
"Default Latency Test": "默认测试链接",
|
||||
|
||||
"Auto Log Clean": "自动清理日志",
|
||||
"Never Clean": "不清理",
|
||||
"Retain 7 Days": "保留7天",
|
||||
"Retain 30 Days": "保留30天",
|
||||
"Retain 90 Days": "保留90天"
|
||||
}
|
||||
|
|
30
src/main.tsx
30
src/main.tsx
|
@ -2,20 +2,36 @@
|
|||
/// <reference types="vite-plugin-svgr/client" />
|
||||
import "./assets/styles/index.scss";
|
||||
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
if (!window.ResizeObserver) {
|
||||
window.ResizeObserver = ResizeObserver;
|
||||
}
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { BaseErrorBoundary } from "./components/base";
|
||||
import Layout from "./pages/_layout";
|
||||
import "./services/i18n";
|
||||
|
||||
ReactDOM.render(
|
||||
const mainElementId = "root";
|
||||
const container = document.getElementById(mainElementId);
|
||||
|
||||
if (!container) {
|
||||
throw new Error(
|
||||
`No container '${mainElementId}' found to render application`
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(container).render(
|
||||
<React.StrictMode>
|
||||
<RecoilRoot>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
<BaseErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</BaseErrorBoundary>
|
||||
</RecoilRoot>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,6 @@ import i18next from "i18next";
|
|||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { SWRConfig, mutate } from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { alpha, List, Paper, ThemeProvider } from "@mui/material";
|
||||
|
@ -11,23 +10,18 @@ import { listen } from "@tauri-apps/api/event";
|
|||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { routers } from "./_routers";
|
||||
import { getAxios } from "@/services/api";
|
||||
import { atomCurrentProfile } from "@/services/states";
|
||||
import { getProfiles } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { ReactComponent as LogoSvg } from "@/assets/image/logo.svg";
|
||||
import { BaseErrorBoundary, Notice } from "@/components/base";
|
||||
import LayoutItem from "@/components/layout/layout-item";
|
||||
import LayoutControl from "@/components/layout/layout-control";
|
||||
import LayoutTraffic from "@/components/layout/layout-traffic";
|
||||
import UpdateButton from "@/components/layout/update-button";
|
||||
import useCustomTheme from "@/components/layout/use-custom-theme";
|
||||
import { LayoutItem } from "@/components/layout/layout-item";
|
||||
import { LayoutControl } from "@/components/layout/layout-control";
|
||||
import { LayoutTraffic } from "@/components/layout/layout-traffic";
|
||||
import { UpdateButton } from "@/components/layout/update-button";
|
||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/zh-cn";
|
||||
|
||||
declare global {
|
||||
const WIN_PORTABLE: boolean;
|
||||
}
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const OS = getSystem();
|
||||
|
@ -40,13 +34,11 @@ const Layout = () => {
|
|||
const { verge } = useVerge();
|
||||
const { theme_blur, language } = verge || {};
|
||||
|
||||
const setCurrentProfile = useSetRecoilState(atomCurrentProfile);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
if (OS === "macos") appWindow.hide();
|
||||
else appWindow.close();
|
||||
// macOS有cmd+w
|
||||
if (e.key === "Escape" && OS !== "macos") {
|
||||
appWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -56,6 +48,7 @@ const Layout = () => {
|
|||
mutate("getProxies");
|
||||
mutate("getVersion");
|
||||
mutate("getClashConfig");
|
||||
mutate("getProviders");
|
||||
});
|
||||
|
||||
// update the verge config
|
||||
|
@ -75,9 +68,6 @@ const Layout = () => {
|
|||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// set current profile uid
|
||||
getProfiles().then((data) => setCurrentProfile(data.current ?? ""));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -88,7 +78,7 @@ const Layout = () => {
|
|||
}, [language]);
|
||||
|
||||
return (
|
||||
<SWRConfig value={{}}>
|
||||
<SWRConfig value={{ errorRetryCount: 3 }}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Paper
|
||||
square
|
||||
|
@ -99,7 +89,17 @@ const Layout = () => {
|
|||
}}
|
||||
onContextMenu={(e) => {
|
||||
// only prevent it on Windows
|
||||
if (OS === "windows") e.preventDefault();
|
||||
const validList = ["input", "textarea"];
|
||||
const target = e.currentTarget;
|
||||
if (
|
||||
OS === "windows" &&
|
||||
!(
|
||||
validList.includes(target.tagName.toLowerCase()) ||
|
||||
target.isContentEditable
|
||||
)
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
sx={[
|
||||
({ palette }) => ({
|
||||
|
@ -130,20 +130,26 @@ const Layout = () => {
|
|||
</div>
|
||||
|
||||
<div className="layout__right" data-windrag>
|
||||
{OS !== "macos" && (
|
||||
{OS === "windows" && (
|
||||
<div className="the-bar">
|
||||
<LayoutControl />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="the-content">
|
||||
<BaseErrorBoundary>
|
||||
<Routes>
|
||||
{routers.map(({ label, link, ele: Ele }) => (
|
||||
<Route key={label} path={link} element={<Ele />} />
|
||||
))}
|
||||
</Routes>
|
||||
</BaseErrorBoundary>
|
||||
<Routes>
|
||||
{routers.map(({ label, link, ele: Ele }) => (
|
||||
<Route
|
||||
key={label}
|
||||
path={link}
|
||||
element={
|
||||
<BaseErrorBoundary key={label}>
|
||||
<Ele />
|
||||
</BaseErrorBoundary>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
Box,
|
||||
|
@ -17,8 +17,13 @@ import { closeAllConnections } from "@/services/api";
|
|||
import { atomConnectionSetting } from "@/services/states";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { BaseEmpty, BasePage } from "@/components/base";
|
||||
import ConnectionItem from "@/components/connection/connection-item";
|
||||
import ConnectionTable from "@/components/connection/connection-table";
|
||||
import { useWebsocket } from "@/hooks/use-websocket";
|
||||
import { ConnectionItem } from "@/components/connection/connection-item";
|
||||
import { ConnectionTable } from "@/components/connection/connection-table";
|
||||
import {
|
||||
ConnectionDetail,
|
||||
ConnectionDetailRef,
|
||||
} from "@/components/connection/connection-detail";
|
||||
|
||||
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
||||
|
||||
|
@ -53,23 +58,18 @@ const ConnectionsPage = () => {
|
|||
return connections;
|
||||
}, [connData, filterText, curOrderOpt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clashInfo) return;
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
const ws = new WebSocket(`ws://${server}/connections?token=${secret}`);
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const { connect, disconnect } = useWebsocket(
|
||||
(event) => {
|
||||
// meta v1.15.0 出现data.connections为null的情况
|
||||
const data = JSON.parse(event.data) as IConnections;
|
||||
|
||||
// 尽量与前一次connections的展示顺序保持一致
|
||||
setConnData((old) => {
|
||||
const oldConn = old.connections;
|
||||
const maxLen = data.connections.length;
|
||||
const maxLen = data.connections?.length;
|
||||
|
||||
const connections: typeof oldConn = [];
|
||||
|
||||
const rest = data.connections.filter((each) => {
|
||||
const rest = (data.connections || []).filter((each) => {
|
||||
const index = oldConn.findIndex((o) => o.id === each.id);
|
||||
|
||||
if (index >= 0 && index < maxLen) {
|
||||
|
@ -93,23 +93,34 @@ const ConnectionsPage = () => {
|
|||
|
||||
return { ...data, connections };
|
||||
});
|
||||
});
|
||||
},
|
||||
{ errorCount: 3, retryInterval: 1000 }
|
||||
);
|
||||
|
||||
return () => ws?.close();
|
||||
useEffect(() => {
|
||||
if (!clashInfo) return;
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
connect(`ws://${server}/connections?token=${encodeURIComponent(secret)}`);
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [clashInfo]);
|
||||
|
||||
const onCloseAll = useLockFn(closeAllConnections);
|
||||
|
||||
const detailRef = useRef<ConnectionDetailRef>(null!);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Connections")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() =>
|
||||
setSetting((o) =>
|
||||
o.layout === "list"
|
||||
|
@ -181,14 +192,24 @@ const ConnectionsPage = () => {
|
|||
{filterConn.length === 0 ? (
|
||||
<BaseEmpty text="No Connections" />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable connections={filterConn} />
|
||||
<ConnectionTable
|
||||
connections={filterConn}
|
||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||
/>
|
||||
) : (
|
||||
<Virtuoso
|
||||
data={filterConn}
|
||||
itemContent={(index, item) => <ConnectionItem value={item} />}
|
||||
itemContent={(index, item) => (
|
||||
<ConnectionItem
|
||||
value={item}
|
||||
onShowDetail={() => detailRef.current?.open(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ConnectionDetail ref={detailRef} />
|
||||
</Paper>
|
||||
</BasePage>
|
||||
);
|
||||
|
|
|
@ -41,11 +41,10 @@ const LogPage = () => {
|
|||
title={t("Logs")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() => setEnableLog((e) => !e)}
|
||||
>
|
||||
{enableLog ? (
|
||||
|
@ -91,8 +90,9 @@ const LogPage = () => {
|
|||
sx={{ width: 120, mr: 1, '[role="button"]': { py: 0.65 } }}
|
||||
>
|
||||
<MenuItem value="all">ALL</MenuItem>
|
||||
<MenuItem value="info">INFO</MenuItem>
|
||||
<MenuItem value="inf">INFO</MenuItem>
|
||||
<MenuItem value="warn">WARN</MenuItem>
|
||||
<MenuItem value="err">ERROR</MenuItem>
|
||||
</Select>
|
||||
|
||||
<TextField
|
||||
|
|
|
@ -1,34 +1,59 @@
|
|||
import useSWR, { mutate } from "swr";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material";
|
||||
import { CachedRounded } from "@mui/icons-material";
|
||||
import {
|
||||
ClearRounded,
|
||||
ContentCopyRounded,
|
||||
LocalFireDepartmentRounded,
|
||||
RefreshRounded,
|
||||
TextSnippetOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
getProfiles,
|
||||
patchProfile,
|
||||
patchProfilesConfig,
|
||||
importProfile,
|
||||
enhanceProfiles,
|
||||
getRuntimeLogs,
|
||||
deleteProfile,
|
||||
updateProfile,
|
||||
} from "@/services/cmds";
|
||||
import { closeAllConnections, getProxies, updateProxy } from "@/services/api";
|
||||
import { atomCurrentProfile } from "@/services/states";
|
||||
import { BasePage, Notice } from "@/components/base";
|
||||
import { ProfileNew } from "@/components/profile/profile-new";
|
||||
import { atomLoadingCache } from "@/services/states";
|
||||
import { closeAllConnections } from "@/services/api";
|
||||
import { BasePage, DialogRef, Notice } from "@/components/base";
|
||||
import {
|
||||
ProfileViewer,
|
||||
ProfileViewerRef,
|
||||
} from "@/components/profile/profile-viewer";
|
||||
import { ProfileItem } from "@/components/profile/profile-item";
|
||||
import { EnhancedMode } from "@/components/profile/enhanced";
|
||||
import { ProfileMore } from "@/components/profile/profile-more";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
|
||||
import { throttle } from "lodash-es";
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [url, setUrl] = useState("");
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [activating, setActivating] = useState("");
|
||||
|
||||
const setCurrentProfile = useSetRecoilState(atomCurrentProfile);
|
||||
const {
|
||||
profiles = {},
|
||||
activateSelected,
|
||||
patchProfiles,
|
||||
mutateProfiles,
|
||||
} = useProfiles();
|
||||
|
||||
const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
|
||||
const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
|
||||
"getRuntimeLogs",
|
||||
getRuntimeLogs
|
||||
);
|
||||
|
||||
const chain = profiles.chain || [];
|
||||
const viewerRef = useRef<ProfileViewerRef>(null);
|
||||
const configRef = useRef<DialogRef>(null);
|
||||
|
||||
// distinguish type
|
||||
const { regularItems, enhanceItems } = useMemo(() => {
|
||||
|
@ -38,66 +63,17 @@ const ProfilePage = () => {
|
|||
const type1 = ["local", "remote"];
|
||||
const type2 = ["merge", "script"];
|
||||
|
||||
const regularItems = items.filter((i) => type1.includes(i.type!));
|
||||
const restItems = items.filter((i) => type2.includes(i.type!));
|
||||
|
||||
const regularItems = items.filter((i) => i && type1.includes(i.type!));
|
||||
const restItems = items.filter((i) => i && type2.includes(i.type!));
|
||||
const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
|
||||
|
||||
const enhanceItems = chain
|
||||
.map((i) => restMap[i]!)
|
||||
.filter(Boolean)
|
||||
.concat(restItems.filter((i) => !chain.includes(i.uid)));
|
||||
|
||||
return { regularItems, enhanceItems };
|
||||
}, [profiles]);
|
||||
|
||||
// sync selected proxy
|
||||
useEffect(() => {
|
||||
if (profiles.current == null) return;
|
||||
|
||||
const current = profiles.current;
|
||||
const profile = regularItems.find((p) => p.uid === current);
|
||||
|
||||
setCurrentProfile(current);
|
||||
|
||||
if (!profile) return;
|
||||
|
||||
setTimeout(async () => {
|
||||
const proxiesData = await getProxies();
|
||||
mutate("getProxies", proxiesData);
|
||||
|
||||
// init selected array
|
||||
const { selected = [] } = profile;
|
||||
const selectedMap = Object.fromEntries(
|
||||
selected.map((each) => [each.name!, each.now!])
|
||||
);
|
||||
|
||||
let hasChange = false;
|
||||
|
||||
const { global, groups } = proxiesData;
|
||||
[global, ...groups].forEach((group) => {
|
||||
const { name, now } = group;
|
||||
|
||||
if (!now || selectedMap[name] === now) return;
|
||||
if (selectedMap[name] == null) {
|
||||
selectedMap[name] = now!;
|
||||
} else {
|
||||
hasChange = true;
|
||||
updateProxy(name, selectedMap[name]);
|
||||
}
|
||||
});
|
||||
|
||||
// update profile selected list
|
||||
profile.selected = Object.entries(selectedMap).map(([name, now]) => ({
|
||||
name,
|
||||
now,
|
||||
}));
|
||||
|
||||
patchProfile(current!, { selected: profile.selected });
|
||||
// update proxies cache
|
||||
if (hasChange) mutate("getProxies", getProxies());
|
||||
}, 100);
|
||||
}, [profiles, regularItems]);
|
||||
|
||||
const onImport = async () => {
|
||||
if (!url) return;
|
||||
setUrl("");
|
||||
|
@ -111,16 +87,15 @@ const ProfilePage = () => {
|
|||
mutate("getProfiles", newProfiles);
|
||||
|
||||
const remoteItem = newProfiles.items?.find((e) => e.type === "remote");
|
||||
|
||||
if (!newProfiles.current && remoteItem) {
|
||||
const current = remoteItem.uid;
|
||||
patchProfilesConfig({ current });
|
||||
mutate("getProfiles", { ...newProfiles, current }, true);
|
||||
mutate("getRuntimeLogs");
|
||||
patchProfiles({ current });
|
||||
mutateLogs();
|
||||
setTimeout(() => activateSelected(), 2000);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
Notice.error("Failed to import profile.");
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
} finally {
|
||||
setDisabled(false);
|
||||
}
|
||||
|
@ -128,41 +103,135 @@ const ProfilePage = () => {
|
|||
|
||||
const onSelect = useLockFn(async (current: string, force: boolean) => {
|
||||
if (!force && current === profiles.current) return;
|
||||
|
||||
// 避免大多数情况下loading态闪烁
|
||||
const reset = setTimeout(() => setActivating(current), 100);
|
||||
try {
|
||||
await patchProfilesConfig({ current });
|
||||
setCurrentProfile(current);
|
||||
mutate("getProfiles", { ...profiles, current: current }, true);
|
||||
mutate("getRuntimeLogs");
|
||||
await patchProfiles({ current });
|
||||
mutateLogs();
|
||||
closeAllConnections();
|
||||
setTimeout(() => activateSelected(), 2000);
|
||||
Notice.success("Refresh clash config", 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString(), 4000);
|
||||
} finally {
|
||||
clearTimeout(reset);
|
||||
setActivating("");
|
||||
}
|
||||
});
|
||||
|
||||
const onEnhance = useLockFn(async () => {
|
||||
try {
|
||||
await enhanceProfiles();
|
||||
mutate("getRuntimeLogs");
|
||||
// Notice.success("Refresh clash config", 1000);
|
||||
mutateLogs();
|
||||
Notice.success("Refresh clash config", 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString(), 3000);
|
||||
}
|
||||
});
|
||||
|
||||
const onEnable = useLockFn(async (uid: string) => {
|
||||
if (chain.includes(uid)) return;
|
||||
const newChain = [...chain, uid];
|
||||
await patchProfiles({ chain: newChain });
|
||||
mutateLogs();
|
||||
});
|
||||
|
||||
const onDisable = useLockFn(async (uid: string) => {
|
||||
if (!chain.includes(uid)) return;
|
||||
const newChain = chain.filter((i) => i !== uid);
|
||||
await patchProfiles({ chain: newChain });
|
||||
mutateLogs();
|
||||
});
|
||||
|
||||
const onDelete = useLockFn(async (uid: string) => {
|
||||
try {
|
||||
await onDisable(uid);
|
||||
await deleteProfile(uid);
|
||||
mutateProfiles();
|
||||
mutateLogs();
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const onMoveTop = useLockFn(async (uid: string) => {
|
||||
if (!chain.includes(uid)) return;
|
||||
const newChain = [uid].concat(chain.filter((i) => i !== uid));
|
||||
await patchProfiles({ chain: newChain });
|
||||
mutateLogs();
|
||||
});
|
||||
|
||||
const onMoveEnd = useLockFn(async (uid: string) => {
|
||||
if (!chain.includes(uid)) return;
|
||||
const newChain = chain.filter((i) => i !== uid).concat([uid]);
|
||||
await patchProfiles({ chain: newChain });
|
||||
mutateLogs();
|
||||
});
|
||||
|
||||
// 更新所有配置
|
||||
const setLoadingCache = useSetRecoilState(atomLoadingCache);
|
||||
const onUpdateAll = useLockFn(async () => {
|
||||
const throttleMutate = throttle(mutateProfiles, 2000, {
|
||||
trailing: true,
|
||||
});
|
||||
const updateOne = async (uid: string) => {
|
||||
try {
|
||||
await updateProfile(uid);
|
||||
throttleMutate();
|
||||
} finally {
|
||||
setLoadingCache((cache) => ({ ...cache, [uid]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setLoadingCache((cache) => {
|
||||
// 获取没有正在更新的配置
|
||||
const items = regularItems.filter(
|
||||
(e) => e.type === "remote" && !cache[e.uid]
|
||||
);
|
||||
const change = Object.fromEntries(items.map((e) => [e.uid, true]));
|
||||
|
||||
Promise.allSettled(items.map((e) => updateOne(e.uid))).then(resolve);
|
||||
return { ...cache, ...change };
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const onCopyLink = async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) setUrl(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Profiles")}
|
||||
header={
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Refresh profiles")}
|
||||
title={t("Update All Profiles")}
|
||||
onClick={onUpdateAll}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("View Runtime Config")}
|
||||
onClick={() => configRef.current?.open()}
|
||||
>
|
||||
<TextSnippetOutlined />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
title={t("Reactivate Profiles")}
|
||||
onClick={onEnhance}
|
||||
>
|
||||
<CachedRounded />
|
||||
<LocalFireDepartmentRounded />
|
||||
</IconButton>
|
||||
</Box>
|
||||
}
|
||||
|
@ -179,6 +248,28 @@ const ProfilePage = () => {
|
|||
onChange={(e) => setUrl(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
placeholder={t("Profile URL")}
|
||||
InputProps={{
|
||||
sx: { pr: 1 },
|
||||
endAdornment: !url ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ p: 0.5 }}
|
||||
title={t("Paste")}
|
||||
onClick={onCopyLink}
|
||||
>
|
||||
<ContentCopyRounded fontSize="inherit" />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ p: 0.5 }}
|
||||
title={t("Clear")}
|
||||
onClick={() => setUrl("")}
|
||||
>
|
||||
<ClearRounded fontSize="inherit" />
|
||||
</IconButton>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={!url || disabled}
|
||||
|
@ -191,7 +282,7 @@ const ProfilePage = () => {
|
|||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
onClick={() => viewerRef.current?.create()}
|
||||
>
|
||||
{t("New")}
|
||||
</Button>
|
||||
|
@ -203,8 +294,10 @@ const ProfilePage = () => {
|
|||
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
|
||||
<ProfileItem
|
||||
selected={profiles.current === item.uid}
|
||||
activating={activating === item.uid}
|
||||
itemData={item}
|
||||
onSelect={(f) => onSelect(item.uid, f)}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
|
@ -212,10 +305,28 @@ const ProfilePage = () => {
|
|||
</Box>
|
||||
|
||||
{enhanceItems.length > 0 && (
|
||||
<EnhancedMode items={enhanceItems} chain={profiles.chain || []} />
|
||||
<Grid container spacing={{ xs: 2, lg: 3 }}>
|
||||
{enhanceItems.map((item) => (
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
|
||||
<ProfileMore
|
||||
selected={!!chain.includes(item.uid)}
|
||||
itemData={item}
|
||||
enableNum={chain.length || 0}
|
||||
logInfo={chainLogs[item.uid]}
|
||||
onEnable={() => onEnable(item.uid)}
|
||||
onDisable={() => onDisable(item.uid)}
|
||||
onDelete={() => onDelete(item.uid)}
|
||||
onMoveTop={() => onMoveTop(item.uid)}
|
||||
onMoveEnd={() => onMoveEnd(item.uid)}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<ProfileNew open={dialogOpen} onClose={() => setDialogOpen(false)} />
|
||||
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
|
||||
<ConfigViewer ref={configRef} />
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,12 +2,17 @@ import useSWR from "swr";
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, ButtonGroup, Paper } from "@mui/material";
|
||||
import { getClashConfig, updateConfigs } from "@/services/api";
|
||||
import { Box, Button, ButtonGroup, Paper } from "@mui/material";
|
||||
import {
|
||||
closeAllConnections,
|
||||
getClashConfig,
|
||||
updateConfigs,
|
||||
} from "@/services/api";
|
||||
import { patchClashConfig } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BasePage } from "@/components/base";
|
||||
import { ProxyGroups } from "@/components/proxy/proxy-groups";
|
||||
import { ProviderButton } from "@/components/proxy/provider-button";
|
||||
|
||||
const ProxyPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -26,9 +31,13 @@ const ProxyPage = () => {
|
|||
return ["rule", "global", "direct", "script"];
|
||||
}, [verge?.clash_core]);
|
||||
|
||||
const curMode = clashConfig?.mode.toLowerCase();
|
||||
const curMode = clashConfig?.mode?.toLowerCase();
|
||||
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
// 断开连接
|
||||
if (mode !== curMode && verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
}
|
||||
await updateConfigs({ mode });
|
||||
await patchClashConfig({ mode });
|
||||
mutateClash();
|
||||
|
@ -45,18 +54,22 @@ const ProxyPage = () => {
|
|||
contentStyle={{ height: "100%" }}
|
||||
title={t("Proxy Groups")}
|
||||
header={
|
||||
<ButtonGroup size="small">
|
||||
{modeList.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={mode === curMode ? "contained" : "outlined"}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{t(mode)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ProviderButton />
|
||||
|
||||
<ButtonGroup size="small">
|
||||
{modeList.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={mode === curMode ? "contained" : "outlined"}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{t(mode)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Paper } from "@mui/material";
|
||||
import { IconButton, Paper } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BasePage, Notice } from "@/components/base";
|
||||
import { GitHub } from "@mui/icons-material";
|
||||
import { openWebUrl } from "@/services/cmds";
|
||||
import SettingVerge from "@/components/setting/setting-verge";
|
||||
import SettingClash from "@/components/setting/setting-clash";
|
||||
import SettingSystem from "@/components/setting/setting-system";
|
||||
|
@ -12,8 +15,24 @@ const SettingPage = () => {
|
|||
Notice.error(err?.message || err.toString());
|
||||
};
|
||||
|
||||
const toGithubRepo = useLockFn(() => {
|
||||
return openWebUrl("https://github.com/zzzgydi/clash-verge");
|
||||
});
|
||||
|
||||
return (
|
||||
<BasePage title={t("Settings")}>
|
||||
<BasePage
|
||||
title={t("Settings")}
|
||||
header={
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="@zzzgydi/clash-verge"
|
||||
onClick={toGithubRepo}
|
||||
>
|
||||
<GitHub fontSize="inherit" />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Paper sx={{ borderRadius: 1, boxShadow: 2, mb: 3 }}>
|
||||
<SettingClash onError={onError} />
|
||||
</Paper>
|
||||
|
|
|
@ -2,14 +2,15 @@ import axios, { AxiosInstance } from "axios";
|
|||
import { getClashInfo } from "./cmds";
|
||||
|
||||
let axiosIns: AxiosInstance = null!;
|
||||
let server = "";
|
||||
let secret = "";
|
||||
|
||||
/// initialize some information
|
||||
/// enable force update axiosIns
|
||||
export async function getAxios(force: boolean = false) {
|
||||
export const getAxios = async (force: boolean = false) => {
|
||||
if (axiosIns && !force) return axiosIns;
|
||||
|
||||
let server = "";
|
||||
let secret = "";
|
||||
|
||||
try {
|
||||
const info = await getClashInfo();
|
||||
|
||||
|
@ -26,76 +27,70 @@ export async function getAxios(force: boolean = false) {
|
|||
axiosIns = axios.create({
|
||||
baseURL: `http://${server}`,
|
||||
headers: secret ? { Authorization: `Bearer ${secret}` } : {},
|
||||
timeout: 15000,
|
||||
});
|
||||
axiosIns.interceptors.response.use((r) => r.data);
|
||||
return axiosIns;
|
||||
}
|
||||
|
||||
/// get information
|
||||
export async function getInformation() {
|
||||
if (server) return { server, secret };
|
||||
const info = await getClashInfo();
|
||||
return info!;
|
||||
}
|
||||
};
|
||||
|
||||
/// Get Version
|
||||
export async function getVersion() {
|
||||
export const getVersion = async () => {
|
||||
const instance = await getAxios();
|
||||
return instance.get("/version") as Promise<{
|
||||
premium: boolean;
|
||||
meta?: boolean;
|
||||
version: string;
|
||||
}>;
|
||||
}
|
||||
};
|
||||
|
||||
/// Get current base configs
|
||||
export async function getClashConfig() {
|
||||
export const getClashConfig = async () => {
|
||||
const instance = await getAxios();
|
||||
return instance.get("/configs") as Promise<IConfigData>;
|
||||
}
|
||||
};
|
||||
|
||||
/// Update current configs
|
||||
export async function updateConfigs(config: Partial<IConfigData>) {
|
||||
export const updateConfigs = async (config: Partial<IConfigData>) => {
|
||||
const instance = await getAxios();
|
||||
return instance.patch("/configs", config);
|
||||
}
|
||||
};
|
||||
|
||||
/// Get current rules
|
||||
export async function getRules() {
|
||||
export const getRules = async () => {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/rules");
|
||||
return response?.rules as IRuleItem[];
|
||||
}
|
||||
};
|
||||
|
||||
/// Get Proxy delay
|
||||
export async function getProxyDelay(
|
||||
name: string,
|
||||
url?: string
|
||||
): Promise<{ delay: number }> {
|
||||
export const getProxyDelay = async (name: string, url?: string) => {
|
||||
const params = {
|
||||
timeout: 3000,
|
||||
timeout: 10000,
|
||||
url: url || "http://www.gstatic.com/generate_204",
|
||||
};
|
||||
|
||||
const instance = await getAxios();
|
||||
return instance.get(`/proxies/${encodeURIComponent(name)}/delay`, { params });
|
||||
}
|
||||
const result = await instance.get(
|
||||
`/proxies/${encodeURIComponent(name)}/delay`,
|
||||
{ params }
|
||||
);
|
||||
return result as any as { delay: number };
|
||||
};
|
||||
|
||||
/// Update the Proxy Choose
|
||||
export async function updateProxy(group: string, proxy: string) {
|
||||
export const updateProxy = async (group: string, proxy: string) => {
|
||||
const instance = await getAxios();
|
||||
return instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy });
|
||||
}
|
||||
};
|
||||
|
||||
// get proxy
|
||||
async function getProxiesInner() {
|
||||
export const getProxiesInner = async () => {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/proxies");
|
||||
return (response?.proxies || {}) as Record<string, IProxyItem>;
|
||||
}
|
||||
};
|
||||
|
||||
/// Get the Proxy information
|
||||
export async function getProxies() {
|
||||
export const getProxies = async () => {
|
||||
const [proxyRecord, providerRecord] = await Promise.all([
|
||||
getProxiesInner(),
|
||||
getProviders(),
|
||||
|
@ -149,10 +144,10 @@ export async function getProxies() {
|
|||
};
|
||||
|
||||
return { global: _global, direct, groups, records: proxyRecord, proxies };
|
||||
}
|
||||
};
|
||||
|
||||
// get proxy providers
|
||||
export async function getProviders() {
|
||||
export const getProviders = async () => {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/providers/proxies");
|
||||
|
||||
|
@ -164,30 +159,35 @@ export async function getProviders() {
|
|||
return type === "http" || type === "file";
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// proxy providers health check
|
||||
export async function providerHealthCheck(name: string) {
|
||||
export const providerHealthCheck = async (name: string) => {
|
||||
const instance = await getAxios();
|
||||
return instance.get(
|
||||
`/providers/proxies/${encodeURIComponent(name)}/healthcheck`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export async function getConnections() {
|
||||
export const providerUpdate = async (name: string) => {
|
||||
const instance = await getAxios();
|
||||
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`);
|
||||
};
|
||||
|
||||
export const getConnections = async () => {
|
||||
const instance = await getAxios();
|
||||
const result = await instance.get("/connections");
|
||||
return result as any as IConnections;
|
||||
}
|
||||
};
|
||||
|
||||
// Close specific connection
|
||||
export async function deleteConnection(id: string) {
|
||||
export const deleteConnection = async (id: string) => {
|
||||
const instance = await getAxios();
|
||||
await instance.delete<any, any>(`/connections/${encodeURIComponent(id)}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Close all connections
|
||||
export async function closeAllConnections() {
|
||||
export const closeAllConnections = async () => {
|
||||
const instance = await getAxios();
|
||||
await instance.delete<any, any>(`/connections`);
|
||||
}
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue