Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d44d331a78 | ||
|
|
595554f18a | ||
|
|
84d3c3f7eb | ||
|
|
890f55c9dc | ||
|
|
575a14e1f3 | ||
|
|
91074bebd6 | ||
|
|
3459a16b48 | ||
|
|
88c9b6849f | ||
|
|
d87bb25baf | ||
|
|
34cb796505 | ||
|
|
1eb34e0662 | ||
|
|
01d631033f | ||
|
|
84b2c07340 | ||
|
|
a86eeb636d | ||
|
|
417a5a8214 | ||
|
|
bcf40dde8c | ||
|
|
cad87484c7 | ||
|
|
b8ad641ed6 | ||
|
|
ae8197be8b | ||
|
|
eafb0274a7 | ||
|
|
71a23a4e02 | ||
|
|
26fd90dfa3 | ||
|
|
e393ebede2 | ||
|
|
2981bb3f19 | ||
|
|
6e9f05abb1 | ||
|
|
89bdc8ec75 | ||
|
|
ae25ade318 | ||
|
|
dd5e46a8a7 | ||
|
|
f7218aaa9e | ||
|
|
ee79bcfc44 | ||
|
|
2c2c174874 | ||
|
|
f57c49ce3a | ||
|
|
06121acfac | ||
|
|
2078ce7446 | ||
|
|
49f41abfdb | ||
|
|
70fcfe6d6c | ||
|
|
b060b4b9bf | ||
|
|
ee2135bfb3 | ||
|
|
7daa322441 | ||
|
|
74252cb66b | ||
|
|
88d3bba300 | ||
|
|
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 |
@@ -0,0 +1,33 @@
|
||||
---
|
||||
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 Nyanpasu Version: [e.g. 1.3.4]
|
||||
- Clash Core: [e.g. Clash or Clash Meta]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -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@v3
|
||||
|
||||
- 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,32 +32,37 @@ jobs:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
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
|
||||
|
||||
- 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 install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
@@ -75,13 +76,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
|
||||
|
||||
@@ -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@v3
|
||||
|
||||
- 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@v3
|
||||
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@v3
|
||||
|
||||
- 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@v3
|
||||
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
|
||||
|
||||
@@ -2,9 +2,9 @@ name: Compatible CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
# push:
|
||||
# tags:
|
||||
# - v**
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
name: Dev Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: startsWith(github.repository, 'keiko233')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Delete current release assets
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
tag: dev
|
||||
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: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: dev
|
||||
releaseName: "Clash Nyanpasu Dev"
|
||||
releaseBody: "More new features are now supported."
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
tauriScript: pnpm
|
||||
args: -f default-meta
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
pnpm build -f default-meta
|
||||
pnpm portable
|
||||
env:
|
||||
TAG_NAME: dev
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
@@ -0,0 +1,107 @@
|
||||
name: Meta CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Delete current release assets
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: meta
|
||||
fail-if-no-assets: false
|
||||
fail-if-no-release: false
|
||||
assets: |
|
||||
*.zip
|
||||
*.gz
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.dmg
|
||||
*.msi
|
||||
*.sig
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Yarn install and check
|
||||
run: |
|
||||
yarn install --network-timeout 1000000
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: meta
|
||||
releaseName: "Clash Verge Meta"
|
||||
releaseBody: ""
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
args: -f default-meta
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
yarn build -f default-meta
|
||||
yarn run portable
|
||||
env:
|
||||
TAG_NAME: meta
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
@@ -0,0 +1,107 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: startsWith(github.repository, 'keiko233')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install Dependencies (Ubuntu Only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: v__VERSION__
|
||||
releaseName: "Clash Nyanpasu v__VERSION__"
|
||||
releaseBody: "More new features are now supported."
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
tauriScript: pnpm
|
||||
args: -f default-meta
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
pnpm build -f default-meta
|
||||
pnpm portable
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
|
||||
release-update:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
startsWith(github.repository, 'keiko233')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Pnpm install
|
||||
run: pnpm i
|
||||
|
||||
- name: Release updater file
|
||||
run: pnpm updater
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -35,14 +35,10 @@ jobs:
|
||||
echo ${{ github.event.inputs.os }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- 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@v3
|
||||
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
|
||||
|
||||
@@ -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@v3
|
||||
|
||||
- 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@v3
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn pretty-quick --staged
|
||||
pnpm pretty-quick --staged
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center">
|
||||
<img src="./src/assets/image/logo.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
Clash Verge
|
||||
Clash Nyanpasu
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
@@ -12,71 +12,61 @@ A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="h
|
||||
## Features
|
||||
|
||||
- Full `clash` config supported, Partial `clash premium` config supported.
|
||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/zzzgydi/clash-verge/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97)
|
||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/keiko233/clash-nyanpasu/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97)
|
||||
- Simple UI and supports custom theme color.
|
||||
- Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core.
|
||||
- System proxy setting and guard.
|
||||
|
||||
## Install
|
||||
|
||||
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
||||
Download from [release](https://github.com/keiko233/clash-nyanpasu/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
||||
|
||||
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 Nyanpasu" is damaged and can't be opened
|
||||
|
||||
open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/Clash\ Nyanpasu.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
|
||||
pnpm i
|
||||
```
|
||||
|
||||
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
|
||||
yarn run check
|
||||
# force update to latest version
|
||||
# pnpm run check --force
|
||||
|
||||
pnpm run check
|
||||
```
|
||||
|
||||
Then run
|
||||
|
||||
```shell
|
||||
yarn dev
|
||||
pnpm dev
|
||||
|
||||
# run it in another way if app instance exists
|
||||
pnpm dev:diff
|
||||
```
|
||||
|
||||
Or you can build it
|
||||
|
||||
```shell
|
||||
yarn build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Todos
|
||||
|
||||
> This keng is a little big...
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/demo1.png" alt="demo1" width="32%" />
|
||||
<img src="./docs/demo2.png" alt="demo2" width="32%" />
|
||||
<img src="./docs/demo3.png" alt="demo3" width="32%" />
|
||||
<img src="./docs/demo4.png" alt="demo4" width="32%" />
|
||||
<img src="./docs/demo5.png" alt="demo5" width="32%" />
|
||||
<img src="./docs/demo6.png" alt="demo6" width="32%" />
|
||||
</div>
|
||||
|
||||
### Custom Theme
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/color1.png" alt="demo1" width="16%" />
|
||||
<img src="./docs/color2.png" alt="demo2" width="16%" />
|
||||
<img src="./docs/color3.png" alt="demo3" width="16%" />
|
||||
<img src="./docs/color4.png" alt="demo4" width="16%" />
|
||||
<img src="./docs/color5.png" alt="demo5" width="16%" />
|
||||
<img src="./docs/color6.png" alt="demo6" width="16%" />
|
||||
</div>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This is a learning project for Rust practice.
|
||||
@@ -87,8 +77,9 @@ Issue and PR welcome!
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
Clash Verge was based on or inspired by these projects and so on:
|
||||
Clash Nyanpasu was based on or inspired by these projects and so on:
|
||||
|
||||
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): A Clash GUI based on tauri. Supports Windows, macOS and Linux.
|
||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
|
||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.
|
||||
- [MetaCubeX/Clash.Meta](https://github.com/MetaCubeX/Clash.Meta): A rule-based tunnel in Go.
|
||||
|
||||
@@ -1,3 +1,209 @@
|
||||
## v1.4.0
|
||||
|
||||
### Features
|
||||
|
||||
- Default use Meta Core.
|
||||
- Support copy PowerShell, CMD and sh env command.
|
||||
- Add Upload Traffic, Download Traffic and Active Connections to ConnectionsPage.
|
||||
- SettingPage use Grid layout.
|
||||
- Import LoadingButton & use when Download Profile.
|
||||
- New default theme color.
|
||||
- Add Nyanpasu Element. (Logo designer [@ReallySnow](https://github.com/ReallySnow))
|
||||
- Default enable unified-delay & tcp-concurrent.
|
||||
- Use Meta Country.mmdb.
|
||||
- Disable IPv6 by default.
|
||||
- Add Material You element.
|
||||
- Add Router switch transition.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix touchpad scrolling causes blank area to appear.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.2.0",
|
||||
"name": "clash-nyanpasu",
|
||||
"version": "1.4.0",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"dev": "tauri dev -f default-meta",
|
||||
"dev:diff": "tauri dev -f verge-dev",
|
||||
"build": "tauri build",
|
||||
"build": "tauri build -f default-meta",
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsc && vite build",
|
||||
@@ -18,22 +18,27 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@mui/x-data-grid": "^5.17.11",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^5.14.14",
|
||||
"@mui/lab": "5.0.0-alpha.149",
|
||||
"@mui/material": "^5.14.14",
|
||||
"@mui/x-data-grid": "^6.16.3",
|
||||
"@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-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^3.1.3",
|
||||
"recoil": "^0.7.6",
|
||||
"snarkdown": "^2.0.0",
|
||||
@@ -41,13 +46,15 @@
|
||||
},
|
||||
"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",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-transition-group": "^4.4.9",
|
||||
"@vitejs/plugin-react": "^4.1.0",
|
||||
"adm-zip": "^0.5.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^10.0.0",
|
||||
@@ -58,9 +65,9 @@
|
||||
"pretty-quick": "^3.1.3",
|
||||
"sass": "^1.54.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.9",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^2.2.1"
|
||||
"vite-plugin-svgr": "^4.1.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
|
||||
@@ -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,13 +32,21 @@ async function resolve() {
|
||||
|
||||
const { version } = require("../package.json");
|
||||
|
||||
const tag = META ? "meta" : ALPHA ? "alpha" : `v${version}`;
|
||||
const buildCmd = META ? `pnpm build -f default-meta` : `pnpm build`;
|
||||
|
||||
console.log(`[INFO]: Upload to tag "${tag}"`);
|
||||
console.log(`[INFO]: Building app. "${buildCmd}"`);
|
||||
|
||||
execSync(buildCmd);
|
||||
|
||||
const cwd = process.cwd();
|
||||
const bundlePath = path.join(cwd, "src-tauri/target/release/bundle");
|
||||
const join = (p) => path.join(bundlePath, p);
|
||||
|
||||
const appPathList = [
|
||||
join("macos/Clash Verge.aarch64.app.tar.gz"),
|
||||
join("macos/Clash Verge.aarch64.app.tar.gz.sig"),
|
||||
join("macos/Clash Nyanpasu.aarch64.app.tar.gz"),
|
||||
join("macos/Clash Nyanpasu.aarch64.app.tar.gz.sig"),
|
||||
];
|
||||
|
||||
for (const appPath of appPathList) {
|
||||
@@ -40,21 +55,21 @@ async function resolve() {
|
||||
}
|
||||
}
|
||||
|
||||
fs.copyFileSync(join("macos/Clash Verge.app.tar.gz"), appPathList[0]);
|
||||
fs.copyFileSync(join("macos/Clash Verge.app.tar.gz.sig"), appPathList[1]);
|
||||
fs.copyFileSync(join("macos/Clash Nyanpasu.app.tar.gz"), appPathList[0]);
|
||||
fs.copyFileSync(join("macos/Clash Nyanpasu.app.tar.gz.sig"), appPathList[1]);
|
||||
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: `v${version}`,
|
||||
tag,
|
||||
});
|
||||
|
||||
if (!release.id) throw new Error("failed to find the release");
|
||||
|
||||
await uploadAssets(release.id, [
|
||||
join(`dmg/Clash Verge_${version}_aarch64.dmg`),
|
||||
join(`dmg/Clash Nyanpasu_${version}_aarch64.dmg`),
|
||||
...appPathList,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -15,28 +15,36 @@ 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 = "2023.08.17";
|
||||
|
||||
const CLASH_BACKUP_URL_PREFIX =
|
||||
"https://github.com/zhongfly/Clash-premium-backup/releases/download/";
|
||||
const CLASH_BACKUP_LATEST_DATE = "2023-09-05-gdcc8d87";
|
||||
|
||||
//https://github.com/zhongfly/Clash-premium-backup/releases/download/2023-09-05-gdcc8d87/clash-windows-amd64-2023-09-05-gdcc8d87.zip
|
||||
//https://github.com/zhongfly/Clash-premium-backup/releases/download/2023-09-05-gdcc8d87/clash-windows-amd64-n2023-09-05-gdcc8d87.zip
|
||||
|
||||
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 +53,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 +77,42 @@ function clash() {
|
||||
};
|
||||
}
|
||||
|
||||
function clashBackup() {
|
||||
const name = CLASH_MAP[`${platform}-${arch}`];
|
||||
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${CLASH_BACKUP_URL_PREFIX}${CLASH_BACKUP_LATEST_DATE}/${name}-n${CLASH_BACKUP_LATEST_DATE}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "clash",
|
||||
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
};
|
||||
}
|
||||
|
||||
function clashS3() {
|
||||
const name = CLASH_MAP[`${platform}-${arch}`];
|
||||
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${CLASH_STORAGE_PREFIX}${CLASH_LATEST_DATE}/${name}-${CLASH_LATEST_DATE}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "clash",
|
||||
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
};
|
||||
}
|
||||
|
||||
function clashMeta() {
|
||||
const name = META_MAP[`${platform}-${arch}`];
|
||||
const isWin = platform === "win32";
|
||||
@@ -103,36 +147,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 +311,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/MetaCubeX/meta-rules-dat/releases/download/latest/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(clashBackup()), 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 +346,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();
|
||||
|
||||
@@ -17,7 +17,7 @@ async function resolvePortable() {
|
||||
|
||||
const zip = new AdmZip();
|
||||
|
||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "Clash Nyanpasu.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "clash.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
|
||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||
@@ -26,7 +26,7 @@ async function resolvePortable() {
|
||||
const packageJson = require("../package.json");
|
||||
const { version } = packageJson;
|
||||
|
||||
const zipFile = `Clash.Verge_${version}_x64_portable.zip`;
|
||||
const zipFile = `Clash.Nyanpasu_${version}_x64_portable.zip`;
|
||||
zip.writeZip(zipFile);
|
||||
|
||||
console.log("[INFO]: create portable zip successfully");
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi"]
|
||||
license = "GPL-3.0"
|
||||
repository = "https://github.com/zzzgydi/clash-verge.git"
|
||||
repository = "https://github.com/keiko233/clash-nyanpasu.git"
|
||||
default-run = "clash-verge"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.2 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())?;
|
||||
|
||||
@@ -30,6 +30,11 @@ impl IClashTemp {
|
||||
map.insert("mode".into(), "rule".into());
|
||||
map.insert("external-controller".into(), "127.0.0.1:9090".into());
|
||||
map.insert("secret".into(), "".into());
|
||||
#[cfg(feature = "default-meta")]
|
||||
map.insert("unified-delay".into(), true.into());
|
||||
#[cfg(feature = "default-meta")]
|
||||
map.insert("tcp-concurrent".into(), true.into());
|
||||
map.insert("ipv6".into(), false.into());
|
||||
|
||||
Self(map)
|
||||
}
|
||||
@@ -53,7 +58,7 @@ impl IClashTemp {
|
||||
help::save_yaml(
|
||||
&dirs::clash_path()?,
|
||||
&self.0,
|
||||
Some("# Generated by Clash Verge"),
|
||||
Some("# Generated by Clash Nyanpasu"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ impl Config {
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
Some("# Clash Nyanpasu Runtime"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ impl Config {
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("failed to get runtime config"))?;
|
||||
|
||||
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
|
||||
help::save_yaml(&path, &config, Some("# Generated by Clash Nyanpasu"))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,11 @@ impl IProfiles {
|
||||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
valid: Some(vec!["dns".into()]),
|
||||
valid: Some(vec![
|
||||
"dns".into(),
|
||||
"unified-delay".into(),
|
||||
"tcp-concurrent".into(),
|
||||
]),
|
||||
items: Some(vec![]),
|
||||
..Self::default()
|
||||
}
|
||||
@@ -65,7 +69,7 @@ impl IProfiles {
|
||||
help::save_yaml(
|
||||
&dirs::profiles_path()?,
|
||||
self,
|
||||
Some("# Profiles Config for Clash Verge"),
|
||||
Some("# Profiles Config for Clash Nyanpasu"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -138,9 +142,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 +159,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 +193,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 +211,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,18 @@ 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>,
|
||||
|
||||
/// 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 +117,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,13 +136,14 @@ impl IVerge {
|
||||
proxy_guard_duration: Some(30),
|
||||
auto_close_connection: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
enable_clash_fields: Some(true),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Save IVerge App Config
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Nyanpasu Config"))
|
||||
}
|
||||
|
||||
/// patch verge config
|
||||
@@ -131,10 +157,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 +181,10 @@ impl IVerge {
|
||||
patch!(auto_close_connection);
|
||||
patch!(default_latency_test);
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(enable_clash_fields);
|
||||
|
||||
patch!(window_size_position);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
@@ -167,4 +199,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +55,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()),
|
||||
|
||||
@@ -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 Nyanpasu.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;
|
||||
|
||||
@@ -23,11 +23,11 @@ pub struct Sysopt {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;<local>";
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;<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,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>";
|
||||
static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
impl Sysopt {
|
||||
pub fn global() -> &'static Sysopt {
|
||||
@@ -158,7 +158,7 @@ impl Sysopt {
|
||||
#[cfg(target_os = "windows")]
|
||||
let app_path = format!("\"{app_path}\"");
|
||||
|
||||
// use the /Applications/Clash Verge.app path
|
||||
// use the /Applications/Clash Nyanpasu.app path
|
||||
#[cfg(target_os = "macos")]
|
||||
let app_path = (|| -> Option<String> {
|
||||
let path = std::path::PathBuf::from(&app_path);
|
||||
@@ -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,89 @@ 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_sh",
|
||||
t!("Copy Env (sh)", "复制环境变量(sh)"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"copy_env_cmd",
|
||||
t!("Copy Env (CMD)", "复制环境变量(CMD)"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"copy_env_ps",
|
||||
t!("Copy Env (PS)", "复制环境变量(PS)"),
|
||||
))
|
||||
.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 +129,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 +157,19 @@ impl Tray {
|
||||
"open_window" => resolve::create_window(app_handle),
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"copy_env_sh" => feat::copy_clash_env("sh"),
|
||||
#[cfg(target_os = "windows")]
|
||||
"copy_env_cmd" => feat::copy_clash_env("cmd"),
|
||||
#[cfg(target_os = "windows")]
|
||||
"copy_env_ps" => feat::copy_clash_env("ps"),
|
||||
"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);
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct JsonResponse {
|
||||
pub data: Option<ResponseBody>,
|
||||
}
|
||||
|
||||
/// Install the Clash Verge Service
|
||||
/// Install the Clash Nyanpasu Service
|
||||
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
||||
pub async fn install_service() -> Result<()> {
|
||||
let binary_path = dirs::service_path()?;
|
||||
@@ -60,7 +60,7 @@ pub async fn install_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uninstall the Clash Verge Service
|
||||
/// Uninstall the Clash Nyanpasu Service
|
||||
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
let binary_path = dirs::service_path()?;
|
||||
@@ -99,10 +99,10 @@ pub async fn check_service() -> Result<JsonResponse> {
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to connect to the Clash Verge Service")?
|
||||
.context("failed to connect to the Clash Nyanpasu Service")?
|
||||
.json::<JsonResponse>()
|
||||
.await
|
||||
.context("failed to parse the Clash Verge Service response")?;
|
||||
.context("failed to parse the Clash Nyanpasu Service response")?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
@@ -148,7 +148,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
.await?
|
||||
.json::<JsonResponse>()
|
||||
.await
|
||||
.context("failed to connect to the Clash Verge Service")?;
|
||||
.context("failed to connect to the Clash Nyanpasu Service")?;
|
||||
|
||||
if res.code != 0 {
|
||||
bail!(res.msg);
|
||||
@@ -168,7 +168,7 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
.await?
|
||||
.json::<JsonResponse>()
|
||||
.await
|
||||
.context("failed to connect to the Clash Verge Service")?;
|
||||
.context("failed to connect to the Clash Nyanpasu Service")?;
|
||||
|
||||
if res.code != 0 {
|
||||
bail!(res.msg);
|
||||
|
||||
@@ -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,23 @@ async fn update_core_config() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// copy env variable
|
||||
pub fn copy_clash_env(option: &str) {
|
||||
let port = { Config::clash().data().get_client_info().port };
|
||||
let http_proxy = format!("http://127.0.0.1:{}", port);
|
||||
let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
|
||||
|
||||
let sh = format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
|
||||
let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}");
|
||||
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
|
||||
|
||||
let mut cliboard = Clipboard::new();
|
||||
|
||||
match option {
|
||||
"sh" => cliboard.write_text(sh),
|
||||
"cmd" => cliboard.write_text(cmd),
|
||||
"ps" => cliboard.write_text(ps),
|
||||
_ => log::error!(target: "app", "copy_clash_env: Invalid option! {option}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -107,6 +109,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 +119,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]
|
||||
|
||||
@@ -5,7 +5,7 @@ use chrono::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 tauri::PackageInfo;
|
||||
@@ -17,42 +17,60 @@ 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
|
||||
/// Initialize all the config files
|
||||
/// before tauri setup
|
||||
pub fn init_config() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
@@ -75,21 +93,21 @@ pub fn init_config() -> Result<()> {
|
||||
|
||||
crate::log_err!(dirs::clash_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Verge"))?;
|
||||
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Nyanpasuasu"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
crate::log_err!(dirs::verge_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
|
||||
help::save_yaml(&path, &IVerge::template(), Some("# Clash Nyanpasu"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
crate::log_err!(dirs::profiles_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
|
||||
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Nyanpasu"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
@@ -97,7 +115,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 +128,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()
|
||||
.title("Clash Nyanpasu")
|
||||
.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,13 +1,13 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "1.2.0"
|
||||
"productName": "Clash Nyanpasu",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
"devPath": "http://localhost:3000/",
|
||||
"beforeDevCommand": "yarn run web:dev",
|
||||
"beforeBuildCommand": "yarn run web:build"
|
||||
"beforeDevCommand": "pnpm run web:dev",
|
||||
"beforeBuildCommand": "pnpm run web:build"
|
||||
},
|
||||
"tauri": {
|
||||
"systemTray": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "top.gydi.clashverge",
|
||||
"identifier": "moe.elaina.clash.nyanpasu",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
@@ -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,18 +46,18 @@
|
||||
"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/keiko233/clash-nyanpasu/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/keiko233/clash-nyanpasu/releases/download/updater/update.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK"
|
||||
},
|
||||
"allowlist": {
|
||||
"shell": {
|
||||
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 72 KiB |
@@ -1,18 +1,36 @@
|
||||
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400"
|
||||
height="400" viewBox="0, 0, 400,400">
|
||||
<!--
|
||||
Copyright (C) 2022 The Project Nyanpasu
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
thanks: @ReallySnow
|
||||
-->
|
||||
|
||||
<svg width="420px" height="490px" viewBox="0 0 420 490" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="Gradient2" x1="0" x2="-0.1" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#5b5c9d" />
|
||||
<stop offset="80%" stop-color="rgb(63, 64, 109)" />
|
||||
<stop offset="100%" stop-color="rgb(63, 64, 109)" />
|
||||
</linearGradient>
|
||||
<style><![CDATA[
|
||||
#element {
|
||||
fill: #000000;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g id="svgg">
|
||||
<path id="path0"
|
||||
d="M118.124 39.858 C 116.645 40.182,114.562 42.930,113.283 46.247 C 111.908 49.812,110.887 54.542,107.205 74.400 C 106.736 76.930,105.926 81.160,105.404 83.800 C 104.527 88.237,103.654 92.889,102.177 101.000 C 101.856 102.760,101.413 105.100,101.192 106.200 C 100.754 108.375,100.784 108.199,98.436 122.200 C 96.675 132.702,95.947 137.215,94.984 143.600 C 94.636 145.910,94.098 149.420,93.789 151.400 C 92.649 158.697,91.961 164.413,92.180 164.768 C 92.304 164.968,92.223 165.192,92.000 165.266 C 91.777 165.340,96.852 165.347,103.277 165.282 C 116.520 165.149,115.721 165.379,116.358 161.519 C 122.839 122.279,179.334 121.580,186.617 160.650 C 186.981 162.602,187.463 164.425,187.689 164.700 C 188.328 165.481,229.717 165.423,230.367 164.640 C 230.622 164.332,231.013 162.937,231.235 161.540 C 237.531 121.949,295.239 121.807,301.620 161.367 C 302.274 165.421,301.855 165.252,310.962 165.117 C 315.273 165.052,318.861 165.090,318.935 165.200 C 319.009 165.310,319.005 165.152,318.927 164.848 C 318.849 164.545,318.519 162.610,318.193 160.548 C 317.867 158.487,317.330 155.135,317.000 153.100 C 316.162 147.934,315.312 142.663,314.823 139.600 C 314.595 138.170,314.226 136.010,314.004 134.800 C 313.781 133.590,312.423 125.400,310.985 116.600 C 304.278 75.545,304.008 74.156,298.145 50.400 C 295.961 41.553,294.621 39.584,290.800 39.611 C 287.927 39.631,283.053 43.395,279.420 48.400 C 277.509 51.032,261.566 79.109,256.960 87.953 C 253.991 93.654,253.647 93.789,245.600 92.402 C 218.757 87.774,194.454 87.780,170.478 92.420 C 162.868 93.893,163.987 94.596,157.579 84.306 C 133.879 46.247,126.566 38.009,118.124 39.858 M147.277 134.807 C 123.365 138.118,111.083 165.918,124.948 185.349 C 140.543 207.202,173.704 202.274,182.466 176.800 C 190.000 154.900,170.471 131.596,147.277 134.807 M262.051 134.810 C 235.780 138.440,224.730 170.720,243.356 189.422 C 264.398 210.548,299.874 195.275,298.689 165.600 C 297.927 146.523,280.892 132.207,262.051 134.810 M157.600 153.840 C 163.092 156.341,166.343 161.914,165.902 168.074 C 164.875 182.425,145.082 186.221,138.715 173.287 C 132.759 161.189,145.324 148.250,157.600 153.840 M273.193 153.597 C 279.788 156.926,283.287 165.159,280.844 171.600 C 275.229 186.406,253.594 183.910,252.135 168.287 C 251.125 157.467,263.609 148.758,273.193 153.597 M91.200 168.809 C 91.200 169.522,90.939 171.836,90.621 173.952 C 89.933 178.523,87.567 196.170,85.788 210.000 C 85.462 212.530,85.012 215.860,84.786 217.400 C 84.561 218.940,84.191 221.820,83.964 223.800 C 83.737 225.780,83.288 229.560,82.965 232.200 C 81.886 241.026,80.172 255.664,79.404 262.600 C 78.867 267.450,78.532 270.381,76.987 283.800 C 76.251 290.187,75.405 297.881,74.568 305.800 C 74.220 309.100,73.789 313.150,73.613 314.800 C 73.436 316.450,73.063 320.230,72.784 323.200 C 72.504 326.170,72.169 328.723,72.038 328.874 C 71.701 329.262,59.638 327.033,54.028 325.546 C 34.668 320.412,26.096 301.951,35.625 285.911 C 38.026 281.869,41.515 278.587,49.795 272.581 C 58.081 266.570,59.262 265.247,59.510 261.702 C 59.969 255.136,50.677 252.070,40.551 255.447 C -6.127 271.014,-3.894 337.227,43.806 351.951 C 50.541 354.030,58.050 355.239,67.760 355.807 C 72.410 356.079,75.202 356.542,82.181 358.199 C 88.822 359.777,100.215 360.425,156.000 362.398 C 170.725 362.918,255.192 362.921,267.760 362.401 C 273.062 362.182,281.900 361.820,287.400 361.597 C 302.851 360.972,326.558 359.339,333.200 358.444 C 343.055 357.116,343.889 354.966,341.839 336.200 C 341.502 333.120,340.948 327.900,340.607 324.600 C 340.266 321.300,339.709 315.990,339.370 312.800 C 339.030 309.610,338.499 304.570,338.189 301.600 C 337.022 290.393,335.650 278.160,334.622 269.800 C 333.105 257.460,332.638 254.027,330.408 238.800 C 329.716 234.070,328.983 229.030,328.781 227.600 C 328.579 226.170,328.231 223.920,328.008 222.600 C 327.785 221.280,326.695 214.260,325.587 207.000 C 324.478 199.740,323.124 191.010,322.578 187.600 C 322.032 184.190,321.325 179.780,321.007 177.800 C 320.689 175.820,320.203 172.711,319.925 170.892 C 319.508 168.155,319.315 167.635,318.810 167.885 C 318.475 168.051,314.780 168.279,310.600 168.393 C 301.870 168.631,302.159 168.535,301.810 171.300 C 299.468 189.825,283.036 203.371,264.230 202.279 C 247.552 201.310,233.532 188.465,231.203 172.020 C 230.651 168.125,232.337 168.400,209.000 168.400 C 185.703 168.400,187.432 168.114,186.814 172.064 C 180.639 211.474,123.556 212.329,116.559 173.117 C 115.684 168.218,116.787 168.629,103.881 168.400 C 97.676 168.290,92.285 168.046,91.900 167.857 C 91.307 167.567,91.200 167.712,91.200 168.809 M211.015 197.632 C 214.247 200.942,215.394 201.357,220.447 201.050 C 225.236 200.759,225.846 201.071,224.310 203.023 C 221.899 206.089,216.333 205.843,210.779 202.425 C 208.312 200.907,207.846 200.906,205.600 202.405 C 200.180 206.022,193.561 206.136,191.600 202.647 C 190.824 201.267,191.555 200.665,193.600 201.001 C 199.643 201.993,201.561 201.450,204.900 197.805 C 207.534 194.929,208.352 194.906,211.015 197.632 "
|
||||
stroke="none" fill="url(#Gradient2)" fill-rule="evenodd"></path>
|
||||
<path id="path1"
|
||||
d="M148.000 131.622 C 132.020 133.140,119.114 145.293,116.377 161.400 C 115.675 165.535,116.697 165.239,103.579 165.112 C 97.252 165.050,92.159 165.135,92.261 165.300 C 92.364 165.465,92.279 165.600,92.074 165.600 C 91.391 165.600,91.684 167.617,92.424 168.013 C 92.839 168.235,97.730 168.400,103.897 168.400 C 116.551 168.400,115.666 168.070,116.562 173.117 C 123.522 212.326,180.639 211.470,186.814 172.064 C 187.432 168.114,185.703 168.400,209.000 168.400 C 232.337 168.400,230.651 168.125,231.203 172.020 C 233.532 188.465,247.552 201.310,264.230 202.279 C 283.039 203.371,299.468 189.826,301.811 171.294 C 302.169 168.458,302.345 168.400,310.600 168.400 C 318.988 168.400,319.609 168.159,318.852 165.200 C 318.824 165.090,315.273 165.052,310.961 165.117 C 301.855 165.252,302.274 165.421,301.620 161.367 C 295.239 121.807,237.531 121.949,231.235 161.540 C 231.013 162.937,230.622 164.332,230.367 164.640 C 229.717 165.423,188.328 165.481,187.689 164.700 C 187.463 164.425,186.981 162.602,186.617 160.650 C 183.194 142.288,166.648 129.850,148.000 131.622 M159.886 135.622 C 183.880 141.759,192.035 172.033,174.435 189.636 C 156.404 207.671,125.892 198.962,119.944 174.084 C 114.368 150.761,136.343 129.601,159.886 135.622 M274.658 135.615 C 299.082 141.861,307.061 172.817,288.734 190.222 C 267.975 209.936,234.188 195.420,234.219 166.800 C 234.241 145.502,253.935 130.315,274.658 135.615 M148.542 152.793 C 139.499 154.706,134.598 164.925,138.715 173.287 C 145.082 186.221,164.875 182.425,165.902 168.074 C 166.591 158.446,157.916 150.811,148.542 152.793 M262.091 153.188 C 253.922 156.127,249.883 165.408,253.408 173.139 C 257.835 182.850,271.593 184.491,278.201 176.097 C 287.389 164.426,275.974 148.192,262.091 153.188 M204.900 197.805 C 201.561 201.450,199.643 201.993,193.600 201.001 C 191.555 200.665,190.824 201.267,191.600 202.647 C 193.561 206.136,200.180 206.022,205.600 202.405 C 207.846 200.906,208.312 200.907,210.779 202.425 C 216.333 205.843,221.899 206.089,224.310 203.023 C 225.846 201.071,225.236 200.759,220.447 201.050 C 215.394 201.357,214.247 200.942,211.015 197.632 C 208.352 194.906,207.534 194.929,204.900 197.805 "
|
||||
stroke="none" fill="#ffffff" fill-rule="evenodd"></path>
|
||||
|
||||
<g id="nyanpasu_logo" fill-rule="nonzero">
|
||||
<path
|
||||
d="M349.980734,470.820835 C362.906597,470.820835 375.380055,471.040616 387.808272,470.736801 C394.678368,470.562269 398.898662,465.526692 400.333433,457.394784 C402.20122,446.832354 400.417451,436.780592 395.809381,427.284747 C390.832924,417.03906 385.255414,407.090725 380.233716,396.903216 C360.528238,356.825451 356.463054,315.532425 371.896535,273.134028 C376.646789,260.076434 384.111475,248.479739 394.781775,239.255389 C400.869857,233.993566 405.684741,233.870747 409.911498,238.718864 C414.138255,243.56698 413.285148,248.415097 407.500824,253.663991 C390.535629,269.081003 384.285974,289.34613 382.230762,311.27901 C379.516331,340.367711 385.669042,367.633519 399.499715,393.373787 C407.100123,407.523823 416.070671,421.169656 418.88851,437.433471 C420.614112,447.317165 420.484854,457.129754 417.402035,466.787202 C412.613003,481.764651 401.244707,490.012913 385.513931,490 C273.567341,489.896558 161.622905,489.814679 49.6806226,489.754347 C42.5713979,489.754347 35.4621733,489.993521 28.605003,487.73753 C11.4136053,482.081394 -0.0322464095,467.259085 0,448.855634 C0.103475152,390.930336 0.374918274,333.011502 3.10227536,275.124988 C4.89250738,237.057576 7.30964376,199.054805 11.2067914,161.148996 C16.3771366,110.612228 23.6608604,60.405131 34.5315112,10.7604159 C35.0024707,8.67513302 35.7547542,6.66358964 36.7676855,4.78107191 C38.5191399,1.50374502 41.1689418,-0.513071536 45.1694964,0.113951558 C49.170051,0.740974653 49.3768648,4.11526388 49.9326769,6.9530282 C53.9913979,27.6706469 60.3056819,47.7612426 66.8590945,67.7742684 C68.9853989,74.238424 70.1875042,82.4543658 74.7891114,86.326395 C79.26146,90.0949977 87.6051045,87.3541957 94.2425352,87.3735882 C154.302557,87.5028713 214.369043,86.8176708 274.422602,88.3755323 C279.256875,88.4983513 281.615845,87.0827012 283.509484,82.6353621 C293.203881,59.8750702 301.967616,36.8044988 308.495177,12.925908 C309.309506,9.94593225 309.942874,6.778496 313.710763,6.46175237 C317.129653,6.17732953 319.346439,8.49796139 320.858765,11.1547293 C321.981523,13.2263513 322.777248,15.4591645 323.217735,17.7740247 C332.832735,64.3247661 340.022746,111.343788 344.758685,158.640904 C354.220417,251.724745 355.745669,345.112401 356.152834,438.538842 C356.152834,444.123872 356.217463,449.695975 355.370819,455.274541 C354.569415,460.646254 352.048872,465.38448 349.980734,470.820835 Z"
|
||||
id="element" fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M314.373914,51.8580359 C319.16418,79.1645649 322.474053,106.509891 325.189183,133.926344 C335.474355,237.585109 337,341.580114 337,445.633314 C337,462.193111 328.052999,470.974135 311.283837,470.980602 C222.356854,471.006466 133.429871,471.006466 44.5028879,470.980602 C27.8242301,470.980602 18.9741981,462.050856 19,445.491059 C19.0776317,397.460537 19.1422776,349.423548 20.816608,301.425356 C22.7559868,246.3726 25.665055,191.384506 31.4637977,136.538667 C34.2500385,110.221458 37.8831415,84.0335719 41.044329,57.7745583 C41.348165,55.2398296 42.0398768,52.7503639 42.8026991,49 C47.5541772,66.3034032 53.702008,81.848015 58.6926762,97.7676631 C60.6902363,104.123883 64.0001095,106.632747 70.7620769,106.632747 C141.816607,106.58964 212.868982,106.72974 283.919202,107.053047 C290.11875,107.053047 293.415694,104.679972 295.555475,99.1643504 C301.63866,83.3804912 308.652746,67.9328716 314.373914,51.8580359 Z"
|
||||
id="bg" fill="#ffffff" fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M119.999315,204.118735 C120.083729,213.985193 112.350174,222.057169 102.850433,222 C94.0584657,221.961379 86.9547636,214.451375 87,205.249067 C87.0456702,196.148935 94.5909186,188.268541 103.493273,188.006712 C112.395627,187.744884 119.921396,195.178255 119.999315,204.118735 Z"
|
||||
id="element" fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M236,204.861743 C236,195.671258 243.035135,188.142475 251.816026,188.001869 C261.124552,187.861264 268.993481,195.671258 269,205.104607 C269.006509,214.537955 262.179823,221.772746 253.255623,221.996436 C244.16206,222.194562 236.026056,214.103357 236,204.861743 Z"
|
||||
id="element" fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M190.493614,267.920884 C185.555364,268.142338 181.468093,262.430145 177.097162,262.137045 C172.945422,261.850458 168.716321,267.914371 162.875519,267.999044 C153.295574,268.096744 144.921179,260.691085 144.089542,251.064378 C143.851011,248.296212 143.844564,245.541072 147.312943,245.059085 C150.362279,244.622692 151.0263,246.902358 151.638746,249.481638 C153.327808,256.646305 157.286143,260.411011 162.701456,259.935538 C168.890385,259.394931 172.371657,255.649765 173.087252,249.468612 C173.383805,247.045652 174.234782,245.104678 176.819949,245.026518 C179.869286,244.935332 180.481732,247.208485 180.855647,249.813818 C181.751752,255.988458 185.426428,259.870405 191.570229,259.889945 C197.71403,259.909485 201.433834,255.981945 202.342833,249.872438 C202.774769,246.954465 203.516151,244.433805 206.907168,245.111192 C210.130569,245.762525 210.195037,248.634905 209.872697,251.383532 C208.750953,260.769245 200.621538,268.044638 190.493614,267.920884 Z"
|
||||
id="element" fill-rule="evenodd"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,21 @@
|
||||
.page-enter {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.page-enter-active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
|
||||
.page-exit {
|
||||
opacity: 1;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.page-exit-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
@@ -16,6 +16,9 @@ body {
|
||||
--text-primary: #637381;
|
||||
--selection-color: #f5f5f5;
|
||||
--scroller-color: #90939980;
|
||||
--background-color: #ffffff;
|
||||
--background-color-alpha: rgba(24, 103, 192, 0.1);
|
||||
--border-radius: 12px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@@ -33,8 +36,13 @@ body {
|
||||
background-color: var(--scroller-color);
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@import "./layout.scss";
|
||||
@import "./page.scss";
|
||||
@import "./anime.scss";
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
height: 100%;
|
||||
max-width: 225px;
|
||||
min-width: 125px;
|
||||
padding: 16px 0 8px;
|
||||
padding: 36px 0 28px;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
@@ -18,6 +19,7 @@
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
overflow: hidden;
|
||||
background-color: var(--background-color-alpha);
|
||||
|
||||
$maxLogo: 100px;
|
||||
|
||||
@@ -37,6 +39,11 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
fill: var(--primary-main);
|
||||
|
||||
#bg {
|
||||
fill: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.the-newbtn {
|
||||
@@ -66,6 +73,7 @@
|
||||
position: relative;
|
||||
flex: 1 1 75%;
|
||||
height: 100%;
|
||||
background-color: var(--background-color-alpha);
|
||||
|
||||
.the-bar {
|
||||
position: absolute;
|
||||
@@ -82,8 +90,8 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 2px;
|
||||
bottom: 10px;
|
||||
right: 28px;
|
||||
bottom: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,3 +112,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.macos {
|
||||
&.layout {
|
||||
.layout__left {
|
||||
padding-top: 24px;
|
||||
}
|
||||
.layout__right .the-content {
|
||||
top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
> header {
|
||||
flex: 0 0 58px;
|
||||
width: 90%;
|
||||
width: 100%;
|
||||
// max-width: 850px;
|
||||
margin: 0 auto;
|
||||
padding-right: 4px;
|
||||
@@ -16,20 +16,40 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
> section {
|
||||
position: relative;
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
.base-container {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 8px 0;
|
||||
box-sizing: border-box;
|
||||
scrollbar-gutter: stable;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
.base-content {
|
||||
width: 90%;
|
||||
// max-width: 850px;
|
||||
margin: 0 auto;
|
||||
> section {
|
||||
position: relative;
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 28px 0;
|
||||
box-sizing: border-box;
|
||||
scrollbar-gutter: stable;
|
||||
background-color: var(--background-color);
|
||||
|
||||
.base-content {
|
||||
width: 90%;
|
||||
// max-width: 850px;
|
||||
margin: 0 auto;
|
||||
animation: baseContentIn 0.3s normal 1 forwards;
|
||||
|
||||
@keyframes baseContentIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(50%) scale(0.9);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, ReactNode, useImperativeHandle, useState } from "react";
|
||||
import { ReactNode } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type SxProps,
|
||||
type Theme,
|
||||
} from "@mui/material";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
|
||||
interface Props {
|
||||
title: ReactNode;
|
||||
@@ -18,6 +19,8 @@ interface Props {
|
||||
disableCancel?: boolean;
|
||||
disableFooter?: boolean;
|
||||
contentSx?: SxProps<Theme>;
|
||||
children?: ReactNode;
|
||||
loading?: boolean;
|
||||
onOk?: () => void;
|
||||
onCancel?: () => void;
|
||||
onClose?: () => void;
|
||||
@@ -39,6 +42,7 @@ export const BaseDialog: React.FC<Props> = (props) => {
|
||||
disableCancel,
|
||||
disableOk,
|
||||
disableFooter,
|
||||
loading,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -55,9 +59,13 @@ export const BaseDialog: React.FC<Props> = (props) => {
|
||||
</Button>
|
||||
)}
|
||||
{!disableOk && (
|
||||
<Button variant="contained" onClick={props.onOk}>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
variant="contained"
|
||||
onClick={props.onOk}
|
||||
>
|
||||
{okBtn}
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
)}
|
||||
</DialogActions>
|
||||
)}
|
||||
|
||||
@@ -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) => {
|
||||
@@ -22,11 +23,13 @@ export const BasePage: React.FC<Props> = (props) => {
|
||||
{header}
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<div className="base-content" style={contentStyle} data-windrag>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
<div className="base-container">
|
||||
<section>
|
||||
<div className="base-content" style={contentStyle} data-windrag>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</BaseErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -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" }}>
|
||||
@@ -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);
|
||||
@@ -15,7 +15,7 @@ const LayoutItem = (props: LinkProps) => {
|
||||
selected={!!match}
|
||||
sx={[
|
||||
{
|
||||
borderRadius: 2,
|
||||
borderRadius: 8,
|
||||
textAlign: "center",
|
||||
"& .MuiListItemText-primary": { color: "text.secondary" },
|
||||
},
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { createTheme, Theme } from "@mui/material";
|
||||
import { alpha, createTheme, Theme } from "@mui/material";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { atomThemeMode } from "@/services/states";
|
||||
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
|
||||
@@ -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);
|
||||
@@ -84,13 +84,19 @@ export default function useCustomTheme() {
|
||||
}
|
||||
|
||||
// css
|
||||
const backgroundColor = mode === "light" ? "#ffffff" : "#121212";
|
||||
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
|
||||
const scrollColor = mode === "light" ? "#90939980" : "#54545480";
|
||||
|
||||
const rootEle = document.documentElement;
|
||||
rootEle.style.setProperty("--background-color", backgroundColor);
|
||||
rootEle.style.setProperty("--selection-color", selectColor);
|
||||
rootEle.style.setProperty("--scroller-color", scrollColor);
|
||||
rootEle.style.setProperty("--primary-main", theme.palette.primary.main);
|
||||
rootEle.style.setProperty(
|
||||
"--background-color-alpha",
|
||||
alpha(theme.palette.primary.main, 0.1)
|
||||
);
|
||||
|
||||
// inject css
|
||||
let style = document.querySelector("style#verge-theme");
|
||||
@@ -121,4 +127,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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,284 @@
|
||||
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");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 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) => {
|
||||
setLoading(true);
|
||||
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);
|
||||
setLoading(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
fileDataRef.current = null;
|
||||
props.onChange();
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
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}
|
||||
loading={loading}
|
||||
>
|
||||
<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",
|
||||
}));
|
||||