Compare commits

..

177 commits
v1.2.2 ... main

Author SHA1 Message Date
GyDi
9df1115380 chore: fix check 2023-11-03 16:00:34 +08:00
GyDi
f22e360cbb chore: fix check script 2023-11-03 15:52:36 +08:00
MZhao
67769af6f4
feat: new windows tray icons (#899) 2023-11-03 15:01:46 +08:00
Aromia
b1a9a1d6d9
chore: update download links in README.md (#896) 2023-11-03 11:21:20 +08:00
Goooler
cf0606ecb7
chore: bump github actions (#868)
https://github.com/actions/checkout/releases/tag/v4.0.0
https://github.com/actions/setup-node/releases/tag/v4.0.0
2023-11-02 20:14:05 +08:00
neovali
7287edcd6f
chore: add fedora linux install description (#874) 2023-11-02 20:13:24 +08:00
GyDi
e0d26203dd feat: support auto clean log files 2023-11-02 20:12:46 +08:00
GyDi
7e3a85e9da
fix: latency url empty 2023-11-01 23:22:30 +08:00
GyDi
5a0fed9c93 feat: increase the concurrency of latency test 2023-11-01 20:52:38 +08:00
GyDi
1f1e743912
fix: change default port 2023-10-31 14:56:19 +08:00
GyDi
b4301ed0d5
fix: csp 2023-10-31 12:00:57 +08:00
GyDi
b5391560fc
v1.3.8 2023-10-31 01:29:20 +08:00
GyDi
718989cbcf
chore: update log 2023-10-31 01:28:57 +08:00
GyDi
d0aee76962
fix: add default valid key 2023-10-30 17:10:51 +08:00
GyDi
fb08af96bd
fix: page null exception, close #821 2023-10-30 00:53:24 +08:00
GyDi
510a0c5e70
feat: adjust the delay display interval and color, close #836 2023-10-29 23:01:05 +08:00
GyDi
5fe2be031f
chore: fix rust lint 2023-10-11 14:55:20 +08:00
GyDi
2ba3aaba47
chore: fix alpha ci 2023-10-11 14:31:44 +08:00
GyDi
ad8903991c
fix: try fix csp 2023-10-11 14:21:56 +08:00
GyDi
3e5624c570
chore: update readme 2023-10-11 00:04:56 +08:00
Majokeiko
f5ee6f3537
feat: ClashFieldViewer BaseDialog maxHeight usage percentage (#813)
*The overall interface will be more intuitive when the content is longer.
2023-10-10 14:29:27 +08:00
GyDi
afc77e7adc
chore: update readme 2023-10-10 10:25:57 +08:00
GyDi
024f42fce6
chore: update readme 2023-10-10 10:25:01 +08:00
GyDi
8a5f12b97c
chore: update clash meta 2023-10-08 21:45:12 +08:00
GyDi
954b21cf39
chore: update readme 2023-09-11 10:24:44 +08:00
GyDi
74d095774d
chore: fix updater 2023-09-11 10:16:54 +08:00
GyDi
17a2722e6d
chore: fix updater 2023-09-11 10:12:40 +08:00
GyDi
c843bddbfe
chore: fix clash map, close #736 2023-09-10 19:06:02 +08:00
GyDi
3f22a49755
v1.3.7 2023-09-10 19:02:10 +08:00
GyDi
7af2ffcebf
chore: update log 2023-09-10 19:01:59 +08:00
GyDi
de90c959e0
chore: update auto launch 2023-09-10 18:45:17 +08:00
GyDi
9987dc1eb4
fix: i18n 2023-09-10 15:03:29 +08:00
GyDi
3efd575dd2
feat: add Open Dashboard to the hotkey, close #723 2023-09-10 14:46:03 +08:00
GyDi
f4c7b17a87
feat: add check for updates button, close #766 2023-09-10 14:30:31 +08:00
GyDi
16d80718cb
fix: fix page undefined exception, close #770 2023-09-10 13:45:18 +08:00
GyDi
ad228d53b7
feat: add paste and clear icon 2023-09-09 16:52:00 +08:00
Majokeiko
15ee1e531b
feat: Subscription URL TextField use multiline (#761)
*Subscription link that are too long can make reading difficult, so use multiline TextField.
2023-09-07 16:14:42 +08:00
GyDi
1c8fb3392a
chore: update mmdb 2023-08-28 15:06:13 +08:00
GyDi
8647866a32
fix: set min window size, close #734 2023-08-28 15:00:27 +08:00
GyDi
23351c4f1c
chore: change ubuntu ci version 2023-08-28 14:44:09 +08:00
GyDi
1367c304cf
chore: update clash 2023-08-28 14:22:50 +08:00
GyDi
26d6bcb074
chore: update release link 2023-08-14 11:11:15 +08:00
GyDi
b0d651ece1
chore: update clash meta 2023-08-14 11:09:19 +08:00
GyDi
b6d50ba6a4
fix: rm debug code 2023-08-12 19:16:20 +08:00
GyDi
b3ab6a9166
v1.3.6 2023-08-12 16:12:03 +08:00
GyDi
f39a5ac9c2
chore: update log 2023-08-12 16:11:50 +08:00
GyDi
38a9a9240d
fix: use sudo when pkexec not found 2023-08-12 15:58:37 +08:00
GyDi
241b22a465
chore: update ci 2023-08-12 15:41:26 +08:00
GyDi
741abc0366
feat: show loading when change profile 2023-08-05 22:07:30 +08:00
GyDi
7854775de5
fix: remove div 2023-08-05 21:43:58 +08:00
GyDi
e62eaa6b4b
fix: list key 2023-08-05 21:43:05 +08:00
GyDi
b4cce23ef4
feat: support proxy provider update 2023-08-05 21:38:44 +08:00
GyDi
2bcaf90fc8
feat: add repo link 2023-08-05 19:54:59 +08:00
GyDi
96ffbe2f84
feat: support clash meta memory usage display 2023-08-05 19:40:23 +08:00
GyDi
6f5acee1c3
fix: websocket disconnect when window focus 2023-08-05 17:21:15 +08:00
GyDi
54e491d8bf
feat: supports show connection detail 2023-08-05 16:52:14 +08:00
GyDi
ab6374e278
chore: update geo data to meta, close #707 2023-08-05 13:42:37 +08:00
GyDi
2fda4c9f67
chore: fix faq 2023-08-05 13:41:30 +08:00
GyDi
5138a45b0f
chore: add faq and download link 2023-08-05 13:38:19 +08:00
GyDi
b224d4fa8a
chore: add promotion 2023-08-05 12:57:48 +08:00
whitemirror33
a552e44483
feat: update connection table with wider process column and click to show full detail (#696) 2023-08-04 14:36:28 +08:00
GyDi
0cf3bba118
feat: more trace logs 2023-08-04 14:15:15 +08:00
Andrei Shevchuk
2c48ea3508
feat: Add Russian Language (#697)
* Add Russian Language

* Add Russian support

* Minor update

* Update Russian translation
2023-08-03 11:07:58 +08:00
GyDi
b9b6212b75
fix: try fix undefined error 2023-07-28 09:26:06 +08:00
GyDi
b978aaec21
chore: alpha ci 2023-07-26 17:07:20 +08:00
GyDi
af704681d9
feat: center window when out of monitor 2023-07-24 20:55:26 +08:00
GyDi
1443ddfe6c
chore: fix test ci 2023-07-24 09:59:13 +08:00
GyDi
54457a3e1b
chore: fix test ci 2023-07-24 09:47:05 +08:00
GyDi
bf180e6a2c
chore: test ci 2023-07-24 09:28:54 +08:00
GyDi
864a5820c9
chore: test ci 2023-07-24 09:13:19 +08:00
GyDi
4d3ca49c3f
v1.3.5 2023-07-23 13:31:51 +08:00
GyDi
c49c3cf7f0
chore: update log 2023-07-23 13:31:34 +08:00
GyDi
5d5ab57469
fix: blurry tray icon in Windows 2023-07-23 13:25:54 +08:00
GyDi
31978d8de0
chore: update clash core 2023-07-23 13:11:35 +08:00
GyDi
e8eb68bf24
chore: fix check script 2023-07-23 13:11:17 +08:00
GyDi
9ea08f4fed
chore: update issue templates 2023-07-22 23:04:13 +08:00
GyDi
fe078a5c5b
v1.3.4 2023-07-22 20:38:08 +08:00
GyDi
61933954f3
chore: update log 2023-07-22 20:37:22 +08:00
GyDi
4c243638cb
fix: enable context menu in editable element 2023-07-22 17:21:04 +08:00
GyDi
02ba04b5d8
feat: support copy environment variable 2023-07-22 15:35:32 +08:00
GyDi
4f158a4829
fix: save window size and pos in Windows 2023-07-22 13:13:16 +08:00
GyDi
177a22df59
feat: save window size and position 2023-07-22 10:58:16 +08:00
GyDi
6b0ca2966e
feat: app log level add silent 2023-07-22 09:25:54 +08:00
GyDi
aadfaf7150
feat: overwrite resource file according to file modified 2023-07-22 09:18:54 +08:00
GyDi
b307b9a66b
feat: support app log level settings 2023-07-22 08:53:37 +08:00
Kimiblock Moe
6c1ab6002d
feat: Use polkit to elevate permission instaed of sudo (#678) 2023-07-21 23:05:33 +08:00
GyDi
9638eefc91
chore: update check script 2023-07-11 13:25:55 +08:00
GyDi
9e9c4ad587
fix: optimize traffic graph high CPU usage when hidden 2023-07-10 23:44:09 +08:00
GyDi
ce231431b9
fix: remove fallback group select status, close #659 2023-07-10 23:16:19 +08:00
GyDi
06e1e14e02
chore: update clash 2023-07-10 13:36:17 +08:00
GyDi
416e7884f5
feat: add unified-delay field 2023-06-30 13:58:51 +08:00
GyDi
d579222007
v1.3.3 2023-06-30 09:38:33 +08:00
GyDi
30243c84cd
chore: update log 2023-06-30 09:35:49 +08:00
GyDi
3557a77645
fix: error boundary with key 2023-06-30 09:17:59 +08:00
GyDi
97be28638b
chore: update clash meta 2023-06-30 09:02:26 +08:00
GyDi
aba0826c38
fix: connections is null 2023-06-30 09:02:17 +08:00
GyDi
f032228d0e
fix: font family not works in some interfaces, close #639 2023-06-29 14:21:14 +08:00
GyDi
6cf174c5ed
fix: encodeURIComponent secret 2023-06-29 14:15:57 +08:00
GyDi
c2109d245f
chore: update clash & clash meta 2023-06-08 13:52:40 +08:00
GyDi
6a9745171e
feat: add error boundary to the app root 2023-06-08 13:50:45 +08:00
GyDi
f9a68e8b23
fix: encode controller secret, close #601 2023-06-08 13:48:58 +08:00
GyDi
6e391df5ee
fix: linux not change icon 2023-05-28 18:14:11 +08:00
GyDi
f5edca94d3
fix: try fix blank error 2023-05-28 17:35:00 +08:00
GyDi
60046abec3
fix: close all connections when change mode 2023-05-28 17:07:39 +08:00
GyDi
cafc2060b8 fix: macos not change icon 2023-05-28 16:46:17 +08:00
GyDi
b1f45752cf chore: update deps 2023-05-28 16:45:43 +08:00
GyDi
ed17551170 chore: update clash 2023-05-28 16:44:07 +08:00
w568w
ef5adab638
feat: show tray icon variants in different status (#537) 2023-05-28 10:55:39 +08:00
GyDi
fb653ff99d
fix: error message null 2023-05-19 10:53:11 +08:00
GyDi
78fc47a9c4
chore: update gh proxy 2023-05-19 09:44:31 +08:00
GyDi
2a124cea61
v1.3.2 2023-05-19 00:57:22 +08:00
GyDi
4c7cc563dc
chore: update log 2023-05-19 00:57:10 +08:00
GyDi
6114af4f93
fix: profile data undefined error, close #566 2023-05-18 20:02:46 +08:00
GyDi
8c31629655
chore: update clash core 2023-05-05 20:45:52 +08:00
GyDi
03c8a8edb2
chore: update deps 2023-05-05 20:27:46 +08:00
yettera765
3eeaee154f
fix: import url error (#543)
use rustls instead of depending user's system tls
2023-05-05 12:08:08 +08:00
GyDi
8cf8fa7c80
v1.3.1 2023-04-11 10:04:55 +08:00
GyDi
6b4f6fc71e
chore: update log 2023-04-11 10:04:28 +08:00
Tatius Titus
30c2680b6f
chore: Upgrade to React 18 (#495)
* chore: Upgrade to React 18

* runfix: Add children type to FC components

* chore: Remove @types/react
2023-04-07 12:59:44 +08:00
Mr-Spade
fb7b1800cc
fix: linux DEFAULT_BYPASS (#503) 2023-04-07 12:47:13 +08:00
GyDi
ff573bf377
chore: update deps 2023-04-02 11:20:18 +08:00
GyDi
0a33bb861e
fix: open file with vscode 2023-04-02 11:19:48 +08:00
GyDi
728756289b
chore: update deps 2023-04-02 10:09:14 +08:00
GyDi
56ccd3a0ac
chore: update clash meta 2023-04-02 09:43:32 +08:00
Tatius Titus
66f3f0ba07
fix: Do not render div as a descendant of p (#494) 2023-04-02 09:37:09 +08:00
Srinivas Gowda
af5e0d589e
chore: update clash core version (#476) 2023-03-27 10:59:27 +08:00
GyDi
533dc99e7d
fix: use replace instead 2023-03-17 08:37:45 +08:00
GyDi
fc5ca965ba
fix: escape path space 2023-03-17 08:36:19 +08:00
John Smith
9c4a46bcdb
fix: escape the space in path (#451) 2023-03-17 08:20:35 +08:00
GyDi
52658886e7
fix: add target os linux 2023-03-16 23:57:34 +08:00
GyDi
8174ab7616
chore: fix ci 2023-03-16 23:54:14 +08:00
GyDi
2b6acedae1
fix: appimage path unwrap panic 2023-03-16 23:45:48 +08:00
GyDi
d00fe9c5f4
v1.3.0 2023-03-16 21:33:51 +08:00
GyDi
88aa270728
chore: update log 2023-03-16 21:33:14 +08:00
GyDi
4ae409c7f4
feat: auto restart core after grand permission 2023-03-16 21:32:39 +08:00
GyDi
9a29c9abdd
feat: add restart core button 2023-03-16 20:56:37 +08:00
GyDi
66d93ea037
fix: remove esc key listener in macOS 2023-03-16 20:42:45 +08:00
GyDi
6c0066dbfb
feat: support update all profiles 2023-03-16 20:32:41 +08:00
GyDi
4fde644733
chore: rm file 2023-03-16 20:31:48 +08:00
GyDi
e7841c60df
fix: adjust style 2023-03-16 19:32:59 +08:00
hybo
94f647b24a
chore: evaluate error context lazily (#447) 2023-03-16 17:06:52 +08:00
GyDi
630249d22a
fix: adjust swr option 2023-03-16 17:03:12 +08:00
GyDi
db99b4cb54
fix: infinite retry when websocket error 2023-03-16 17:03:00 +08:00
GyDi
c77db23586
fix: type error 2023-03-16 13:51:46 +08:00
GyDi
daf66bcec4
chore: update deps 2023-03-16 13:42:27 +08:00
GyDi
8caf36349f
chore: fix ci 2023-03-16 13:18:05 +08:00
GyDi
6934de58e5
chore: fix ci 2023-03-16 13:16:30 +08:00
GyDi
54a5007c01
feat: support to grant permission to clash core 2023-03-16 11:16:54 +08:00
GyDi
e25a455698
fix: do not parse log except the clash core 2023-03-16 10:34:28 +08:00
GyDi
ab429dfeb6
feat: support clash fields filter in ui 2023-03-15 08:43:24 +08:00
GyDi
c5289dc0e8
fix: field sort for filter 2023-03-15 08:43:03 +08:00
boatrainlsz
d191877002
chore: update README (#435) 2023-03-07 22:55:38 +08:00
GyDi
4d979160c2
chore: update deps 2023-03-07 00:10:49 +08:00
GyDi
d00e8f6e19
chore: update clash 2023-03-06 23:35:30 +08:00
GyDi
91b77e5237
fix: add meta fields 2023-02-18 00:46:03 +08:00
GyDi
5b8c246d53
chore: update clash 2023-02-18 00:09:40 +08:00
GyDi
b374b9b91c
feat: open dir on the tray 2023-02-17 00:15:21 +08:00
GyDi
403717117e
fix: runtime config user select 2023-02-17 00:00:23 +08:00
GyDi
027295d995
feat: support to disable clash fields filter 2023-02-16 23:52:55 +08:00
GyDi
c9b7eccbc1
fix: app_handle as_ref 2023-02-15 17:25:35 +08:00
GyDi
2b6d9348cd
fix: use crate 2023-02-11 23:31:55 +08:00
GyDi
692f8c8454
fix: appimage auto launch, close #403 2023-02-11 23:19:08 +08:00
inRm3D
6783355c4d
chore: add openssl depend (#395) 2023-02-02 10:54:15 +08:00
GyDi
fb9cca1e99
v1.2.3 2023-02-01 22:11:34 +08:00
GyDi
eb770ede1a
chore: update log 2023-02-01 22:11:06 +08:00
GyDi
2643e853af
chore: aarch script 2023-02-01 21:41:15 +08:00
GyDi
b79456e91b
chore: update clash 2023-01-30 20:50:08 +08:00
GyDi
ac66c086f8
chore: meta ci 2023-01-30 20:40:57 +08:00
GyDi
ebccf401dd
chore: fix ci 2023-01-22 13:31:34 +08:00
GyDi
66494845b7
chore: fix ci 2023-01-19 18:02:08 +08:00
GyDi
e6c36ad602
chore: alpha ci 2023-01-18 23:39:27 +08:00
inRm3D
26379182db
chore: try fix missing libssl3 (#378)
* Update ci.yml

* use package from source
2023-01-18 23:36:18 +08:00
GyDi
bba03d14d4
fix: compatible with UTF8 BOM, close #283 2023-01-17 19:51:02 +08:00
GyDi
23b728a762
feat: adjust macOS window style 2023-01-16 22:57:53 +08:00
GyDi
819c5207d2
fix: use selected proxy after profile changed 2023-01-15 21:33:03 +08:00
GyDi
311358544e
fix: error log 2023-01-15 19:11:51 +08:00
GyDi
4480ecc96d
chore: ci 2023-01-14 22:22:18 +08:00
96 changed files with 4132 additions and 2011 deletions

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Information**
- OS: [e.g. macOS]
- Clash Verge Version: [e.g. 1.3.4]
- Clash Core: [e.g. Clash or Clash Meta]
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -16,19 +16,15 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [windows-latest, ubuntu-latest, macos-latest] os: [windows-latest, ubuntu-20.04, macos-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
if: startsWith(github.repository, 'zzzgydi') if: startsWith(github.repository, 'zzzgydi')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Install Rust - name: install Rust stable
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
profile: minimal
override: true
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
@ -36,35 +32,13 @@ jobs:
workspaces: src-tauri workspaces: src-tauri
- name: Install Node - name: Install Node
uses: actions/setup-node@v1 uses: actions/setup-node@v4
with: with:
node-version: 16 node-version: "16"
cache: "yarn"
- name: Install Dependencies (ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Get yarn cache dir path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install and check
run: |
yarn install --network-timeout 1000000
yarn run check
- name: Delete current release assets - name: Delete current release assets
if: startsWith(matrix.os, 'ubuntu-')
uses: mknejp/delete-release-assets@v1 uses: mknejp/delete-release-assets@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@ -79,6 +53,18 @@ jobs:
*.dmg *.dmg
*.msi *.msi
*.sig *.sig
*.exe
- name: Install Dependencies (ubuntu only)
if: startsWith(matrix.os, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
- name: Yarn install and check
run: |
yarn install --network-timeout 1000000 --frozen-lockfile
yarn run check --force
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@ -91,14 +77,13 @@ jobs:
releaseName: "Clash Verge Alpha" releaseName: "Clash Verge Alpha"
releaseBody: "Alpha Version (include debug)" releaseBody: "Alpha Version (include debug)"
releaseDraft: false releaseDraft: false
includeDebug: ${{ github.event.inputs.debug }}
prerelease: true prerelease: true
args: -f default-meta includeDebug: ${{ github.event.inputs.debug }}
- name: Portable Bundle - name: Portable Bundle
if: matrix.os == 'windows-latest' if: startsWith(matrix.os, 'windows-')
run: | run: |
yarn build -f default-meta yarn build
yarn run portable yarn run portable
env: env:
TAG_NAME: alpha TAG_NAME: alpha

View file

@ -19,14 +19,10 @@ jobs:
if: startsWith(github.repository, 'zzzgydi') if: startsWith(github.repository, 'zzzgydi')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Install Rust - name: install Rust stable
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
profile: minimal
override: true
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
@ -34,32 +30,20 @@ jobs:
workspaces: src-tauri workspaces: src-tauri
- name: Install Node - name: Install Node
uses: actions/setup-node@v1 uses: actions/setup-node@v4
with: with:
node-version: 16 node-version: "16"
cache: "yarn"
- name: Install Dependencies (ubuntu only) - name: Install Dependencies (ubuntu only)
if: startsWith(matrix.os, 'ubuntu-') if: startsWith(matrix.os, 'ubuntu-')
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf 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 - name: Yarn install and check
run: | run: |
yarn install --network-timeout 1000000 yarn install --network-timeout 1000000 --frozen-lockfile
yarn run check yarn run check
- name: Tauri build - name: Tauri build
@ -78,7 +62,7 @@ jobs:
prerelease: true prerelease: true
- name: Portable Bundle - name: Portable Bundle
if: matrix.os == 'windows-latest' if: startsWith(matrix.os, 'windows-')
# rebuild with env settings # rebuild with env settings
run: | run: |
yarn build yarn build
@ -97,23 +81,16 @@ jobs:
startsWith(github.ref, 'refs/tags/v') startsWith(github.ref, 'refs/tags/v')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Get yarn cache dir path - name: Install Node
id: yarn-cache-dir-path uses: actions/setup-node@v4
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} node-version: "16"
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} cache: "yarn"
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install - name: Yarn install
run: yarn install run: yarn install --network-timeout 1000000 --frozen-lockfile
- name: Release updater file - name: Release updater file
run: yarn run updater run: yarn run updater

107
.github/workflows/meta.yml vendored Normal file
View file

@ -0,0 +1,107 @@
name: Meta CI
on:
workflow_dispatch:
push:
tags:
- v**
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
release:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
if: startsWith(github.repository, 'zzzgydi')
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Delete current release assets
if: matrix.os == 'ubuntu-latest'
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: meta
fail-if-no-assets: false
fail-if-no-release: false
assets: |
*.zip
*.gz
*.AppImage
*.deb
*.dmg
*.msi
*.sig
- name: Install Dependencies (ubuntu only)
if: startsWith(matrix.os, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
- name: Get yarn cache dir path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install and check
run: |
yarn install --network-timeout 1000000
yarn run check
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: meta
releaseName: "Clash Verge Meta"
releaseBody: ""
releaseDraft: false
prerelease: true
args: -f default-meta
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: |
yarn build -f default-meta
yarn run portable
env:
TAG_NAME: meta
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_WIN_PORTABLE: 1

View file

@ -35,14 +35,10 @@ jobs:
echo ${{ github.event.inputs.os }} echo ${{ github.event.inputs.os }}
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Install Rust - name: install Rust stable
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
profile: minimal
override: true
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
@ -50,32 +46,20 @@ jobs:
workspaces: src-tauri workspaces: src-tauri
- name: Install Node - name: Install Node
uses: actions/setup-node@v1 uses: actions/setup-node@v4
with: with:
node-version: 16 node-version: "16"
cache: "yarn"
- name: Install Dependencies (ubuntu only) - name: Install Dependencies (ubuntu only)
if: startsWith(github.event.inputs.os, 'ubuntu-') if: startsWith(github.event.inputs.os, 'ubuntu-')
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Get yarn cache dir path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install and check - name: Yarn install and check
run: | run: |
yarn install --network-timeout 1000000 yarn install --network-timeout 1000000 --frozen-lockfile
yarn run check yarn run check
- name: Tauri build - name: Tauri build
@ -84,3 +68,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: alpha
releaseName: "Clash Verge Alpha"
releaseBody: "Alpha Version (include debug)"
releaseDraft: false
includeUpdaterJson: false

View file

@ -4,27 +4,20 @@ on: workflow_dispatch
jobs: jobs:
release-update: release-update:
runs-on: macos-latest runs-on: ubuntu-latest
if: startsWith(github.repository, 'zzzgydi') if: startsWith(github.repository, 'zzzgydi')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Get yarn cache dir path - name: Install Node
id: yarn-cache-dir-path uses: actions/setup-node@v4
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} node-version: "16"
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} cache: "yarn"
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install - name: Yarn install
run: yarn install run: yarn install --network-timeout 1000000 --frozen-lockfile
- name: Release updater file - name: Release updater file
run: yarn run updater run: yarn run updater

View file

@ -17,17 +17,69 @@ A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="h
- Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core. - Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core.
- System proxy setting and guard. - System proxy setting and guard.
## Promotion
[狗狗加速 —— 技术流机场 Doggygo VPN](https://dg1.top)
- High-performance overseas VPN, free trial, discounted packages, unlock streaming media, the world's first to support Hysteria protocol.
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
- 使用 Clash Verge 专属邀请链接注册送 15 天,每天 1G 流量免费试用https://panel.dg1.top/#/register?code=sFCDayZf
<details>
<summary>Promotion Detail</summary>
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
- 优惠套餐每月仅需 15.8 元160G 流量,年付 8 折
- 海外团队,无跑路风险,高达 50% 返佣
- 集群负载均衡设计,高速专线(兼容老客户端)极低延迟无视晚高峰4K 秒开
- 全球首家 Hysteria 协议机场,将在今年 10 月上线更快的 `tuic` 协议(Clash Verge 客户端最佳搭配)
- 解锁流媒体及 ChatGPT
- 官网https://dg1.top
</details>
<br />
[EEVPN —— 海外运营机场 ※ 支持 ChatGPT](https://www.eejsq.net/#/register?code=yRr6qBO3)
- 年付低至 9.99 元,价格低,速度不减
<details>
<summary>Promotion Detail</summary>
- 中国大陆 BGP 网络接入
- IEPL 专线网络
- 最高 2500Mbps 速率可用
- 不限制在线客户端
- 解锁流媒体及 ChatGPT
- 海外运营 数据安全
</details>
## Install ## 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/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
- [Windows x64](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64_en-US.msi)
- [macOS intel](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64.dmg)
- [macOS arm](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_aarch64.dmg)
- [Linux AppImage](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.AppImage)
- [Linux deb](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.deb)
- [Fedora Linux](https://github.com/zzzgydi/clash-verge/issues/352)
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+ 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. Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
### FAQ
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/Clash\ Verge.app`
## Development ## 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 ```shell
yarn install yarn install
@ -36,6 +88,9 @@ yarn install
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin). 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 ```shell
# force update to latest version
# yarn run check --force
yarn run check yarn run check
``` ```
@ -43,6 +98,9 @@ Then run
```shell ```shell
yarn dev yarn dev
# run it in another way if app instance exists
yarn dev:diff
``` ```
Or you can build it Or you can build it

View file

@ -1,3 +1,168 @@
## v1.3.8
### Features
- update clash meta core
- add default valid keys
- adjust the delay display interval and color
### Bug Fixes
- fix connections page undefined exception
---
## v1.3.7
### Features
- update clash and clash meta core
- profiles page add paste button
- subscriptions url textfield use multi lines
- set min window size
- add check for updates buttons
- add open dashboard to the hotkey list
### Bug Fixes
- fix profiles page undefined exception
---
## v1.3.6
### Features
- add russian translation
- support to show connection detail
- support clash meta memory usage display
- support proxy provider update ui
- update geo data file from meta repo
- adjust setting page
### Bug Fixes
- center the window when it is out of screen
- use `sudo` when `pkexec` not found (Linux)
- reconnect websocket when window focus
### Notes
- The current version of the Linux installation package is built by Ubuntu 20.04 (Github Action).
---
## v1.3.5
### Features
- update clash core
### Bug Fixes
- fix blurry system tray icon (Windows)
- fix v1.3.4 wintun.dll not found (Windows)
- fix v1.3.4 clash core not found (macOS, Linux)
---
## v1.3.4
### Features
- update clash and clash meta core
- optimize traffic graph high CPU usage when window hidden
- use polkit to elevate permission (Linux)
- support app log level setting
- support copy environment variable
- overwrite resource file according to file modified
- save window size and position
### Bug Fixes
- remove fallback group select status
- enable context menu on editable element (Windows)
---
## v1.3.3
### Features
- update clash and clash meta core
- show tray icon variants in different system proxy status (Windows)
- close all connections when mode changed
### Bug Fixes
- encode controller secret into uri
- error boundary for each page
---
## v1.3.2
### Features
- update clash and clash meta core
### Bug Fixes
- fix import url issue
- fix profile undefined issue
---
## v1.3.1
### Features
- update clash and clash meta core
### Bug Fixes
- fix open url issue
- fix appimage path panic
- fix grant root permission in macOS
- fix linux system proxy default bypass
---
## v1.3.0
### Features
- update clash and clash meta
- support opening dir on tray
- support updating all profiles with one click
- support granting root permission to clash core(Linux, macOS)
- support enable/disable clash fields filter, feel free to experience the latest features of Clash Meta
### Bug Fixes
- deb add openssl depend(Linux)
- fix the AppImage auto launch path(Linux)
- fix get the default network service(macOS)
- remove the esc key listener in macOS, cmd+w instead(macOS)
- fix infinite retry when websocket error
---
## v1.2.3
### Features
- update clash
- adjust macOS window style
- profile supports UTF8 with BOM
### Bug Fixes
- fix selected proxy
- fix error log
---
## v1.2.2 ## v1.2.2
### Features ### Features

View file

@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "1.2.2", "version": "1.3.8",
"license": "GPL-3.0", "license": "GPL-3.0",
"scripts": { "scripts": {
"dev": "tauri dev", "dev": "tauri dev",
@ -24,14 +24,15 @@
"@mui/icons-material": "^5.10.9", "@mui/icons-material": "^5.10.9",
"@mui/material": "^5.10.13", "@mui/material": "^5.10.13",
"@mui/x-data-grid": "^5.17.11", "@mui/x-data-grid": "^5.17.11",
"@tauri-apps/api": "^1.2.0", "@tauri-apps/api": "^1.3.0",
"ahooks": "^3.7.2", "ahooks": "^3.7.2",
"axios": "^1.1.3", "axios": "^1.1.3",
"dayjs": "1.11.5", "dayjs": "1.11.5",
"i18next": "^22.0.4", "i18next": "^22.0.4",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.34.1", "monaco-editor": "^0.34.1",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-hook-form": "^7.39.5", "react-hook-form": "^7.39.5",
"react-i18next": "^12.0.0", "react-i18next": "^12.0.0",
@ -43,12 +44,12 @@
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^5.0.3", "@actions/github": "^5.0.3",
"@tauri-apps/cli": "^1.2.2", "@tauri-apps/cli": "^1.3.1",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@types/lodash": "^4.14.180", "@types/lodash": "^4.14.180",
"@types/react": "^17.0.0", "@types/lodash-es": "^4.17.7",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^2.0.1", "@vitejs/plugin-react": "^2.0.1",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -60,7 +61,7 @@
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"sass": "^1.54.0", "sass": "^1.54.0",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"vite": "^3.0.9", "vite": "^3.2.5",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^2.2.1" "vite-plugin-svgr": "^2.2.1"
}, },

View file

@ -1,12 +1,19 @@
/** /**
* Build and upload assets for macOS(aarch) * Build and upload assets
* for macOS(aarch)
*/ */
import fs from "fs-extra"; import fs from "fs-extra";
import path from "path"; import path from "path";
import { exit } from "process"; import { exit } from "process";
import { execSync } from "child_process";
import { createRequire } from "module"; import { createRequire } from "module";
import { getOctokit, context } from "@actions/github"; 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); const require = createRequire(import.meta.url);
async function resolve() { async function resolve() {
@ -25,6 +32,14 @@ async function resolve() {
const { version } = require("../package.json"); const { version } = require("../package.json");
const tag = META ? "meta" : ALPHA ? "alpha" : `v${version}`;
const buildCmd = META ? `yarn build -f default-meta` : `yarn build`;
console.log(`[INFO]: Upload to tag "${tag}"`);
console.log(`[INFO]: Building app. "${buildCmd}"`);
execSync(buildCmd);
const cwd = process.cwd(); const cwd = process.cwd();
const bundlePath = path.join(cwd, "src-tauri/target/release/bundle"); const bundlePath = path.join(cwd, "src-tauri/target/release/bundle");
const join = (p) => path.join(bundlePath, p); const join = (p) => path.join(bundlePath, p);
@ -48,7 +63,7 @@ async function resolve() {
const { data: release } = await github.rest.repos.getReleaseByTag({ const { data: release } = await github.rest.repos.getReleaseByTag({
...options, ...options,
tag: `v${version}`, tag,
}); });
if (!release.id) throw new Error("failed to find the release"); if (!release.id) throw new Error("failed to find the release");

View file

@ -15,28 +15,29 @@ const SIDECAR_HOST = execSync("rustc -vV")
.match(/(?<=host: ).+(?=\s*)/g)[0]; .match(/(?<=host: ).+(?=\s*)/g)[0];
/* ======= clash ======= */ /* ======= clash ======= */
const CLASH_STORAGE_PREFIX = "https://release.dreamacro.workers.dev/";
const CLASH_URL_PREFIX = const CLASH_URL_PREFIX =
"https://github.com/Dreamacro/clash/releases/download/premium/"; "https://github.com/Dreamacro/clash/releases/download/premium/";
const CLASH_LATEST_DATE = "2022.11.25"; const CLASH_LATEST_DATE = "latest";
const CLASH_MAP = { const CLASH_MAP = {
"win32-x64": "clash-windows-amd64", "win32-x64": "clash-windows-amd64",
"darwin-x64": "clash-darwin-amd64", "darwin-x64": "clash-darwin-amd64",
"darwin-arm64": "clash-darwin-arm64", "darwin-arm64": "clash-darwin-arm64",
"linux-x64": "clash-linux-amd64", "linux-x64": "clash-linux-amd64",
"linux-arm64": "clash-linux-armv8", "linux-arm64": "clash-linux-arm64",
}; };
/* ======= clash meta ======= */ /* ======= clash meta ======= */
const META_URL_PREFIX = `https://github.com/MetaCubeX/Clash.Meta/releases/download/`; const META_URL_PREFIX = `https://github.com/MetaCubeX/Clash.Meta/releases/download/`;
const META_VERSION = "v1.14.1"; const META_VERSION = "v1.16.0";
const META_MAP = { const META_MAP = {
"win32-x64": "Clash.Meta-windows-amd64-compatible", "win32-x64": "clash.meta-windows-amd64-compatible",
"darwin-x64": "Clash.Meta-darwin-amd64", "darwin-x64": "clash.meta-darwin-amd64",
"darwin-arm64": "Clash.Meta-darwin-arm64", "darwin-arm64": "clash.meta-darwin-arm64",
"linux-x64": "Clash.Meta-linux-amd64-compatible", "linux-x64": "clash.meta-linux-amd64-compatible",
"linux-arm64": "Clash.Meta-linux-arm64", "linux-arm64": "clash.meta-linux-arm64",
}; };
/** /**
@ -45,10 +46,10 @@ const META_MAP = {
const { platform, arch } = process; const { platform, arch } = process;
if (!CLASH_MAP[`${platform}-${arch}`]) { 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}`]) { 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() { function clash() {
@ -69,6 +70,24 @@ function clash() {
}; };
} }
function clashS3() {
const name = CLASH_MAP[`${platform}-${arch}`];
const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${CLASH_STORAGE_PREFIX}${CLASH_LATEST_DATE}/${name}-${CLASH_LATEST_DATE}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}.${urlExt}`;
return {
name: "clash",
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
};
}
function clashMeta() { function clashMeta() {
const name = META_MAP[`${platform}-${arch}`]; const name = META_MAP[`${platform}-${arch}`];
const isWin = platform === "win32"; const isWin = platform === "win32";
@ -103,36 +122,61 @@ async function resolveSidecar(binInfo) {
const tempExe = path.join(tempDir, exeFile); const tempExe = path.join(tempDir, exeFile);
await fs.mkdirp(tempDir); 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")) { if (zipFile.endsWith(".zip")) {
const zip = new AdmZip(tempZip); const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => { zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: ${name} entry name`, entry.entryName); 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;
}); });
zip.extractAllTo(tempDir, true);
await fs.rename(tempExe, sidecarPath);
console.log(`[INFO]: "${name}" unzip finished`);
} else {
// gz
const readStream = fs.createReadStream(tempZip);
const writeStream = fs.createWriteStream(sidecarPath);
await new Promise((resolve, reject) => {
const onError = (error) => {
console.error(`[ERROR]: "${name}" gz failed:`, error.message);
reject(error);
};
readStream
.pipe(zlib.createGunzip().on("error", onError))
.pipe(writeStream)
.on("finish", () => {
console.log(`[INFO]: "${name}" gunzip finished`);
execSync(`chmod 755 ${sidecarPath}`);
console.log(`[INFO]: "${name}" chmod binary finished`);
resolve();
})
.on("error", onError);
});
}
} catch (err) {
// 需要删除文件
await fs.remove(sidecarPath);
throw err;
} finally {
// delete temp dir
await fs.remove(tempDir);
} }
}
// delete temp dir /**
await fs.remove(tempDir); * prepare clash core
* if the core version is not updated in time, use S3 storage as a backup.
*/
async function resolveClash() {
try {
return await resolveSidecar(clash());
} catch {
console.log(`[WARN]: clash core needs to be updated`);
return await resolveSidecar(clashS3());
}
} }
/** /**
@ -242,21 +286,21 @@ const resolveUninstall = () =>
const resolveMmdb = () => const resolveMmdb = () =>
resolveResource({ resolveResource({
file: "Country.mmdb", file: "Country.mmdb",
downloadURL: `https://github.com/Dreamacro/maxmind-geoip/releases/download/20221112/Country.mmdb`, downloadURL: `https://github.com/Dreamacro/maxmind-geoip/releases/download/20230812/Country.mmdb`,
}); });
const resolveGeosite = () => const resolveGeosite = () =>
resolveResource({ resolveResource({
file: "geosite.dat", 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 = () => const resolveGeoIP = () =>
resolveResource({ resolveResource({
file: "geoip.dat", 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 = [ const tasks = [
{ name: "clash", func: () => resolveSidecar(clash()), retry: 5 }, { name: "clash", func: () => resolveSidecar(clashS3()), retry: 5 },
{ name: "clash-meta", func: () => resolveSidecar(clashMeta()), retry: 5 }, { name: "clash-meta", func: () => resolveSidecar(clashMeta()), retry: 5 },
{ name: "wintun", func: resolveWintun, retry: 5, winOnly: true }, { name: "wintun", func: resolveWintun, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5, winOnly: true }, { name: "service", func: resolveService, retry: 5, winOnly: true },
@ -277,7 +321,8 @@ async function runTask() {
await task.func(); await task.func();
break; break;
} catch (err) { } 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(); return runTask();

View file

@ -43,6 +43,7 @@ async function resolveUpdater() {
darwin: { signature: "", url: "" }, // compatible with older formats darwin: { signature: "", url: "" }, // compatible with older formats
"darwin-aarch64": { signature: "", url: "" }, "darwin-aarch64": { signature: "", url: "" },
"darwin-intel": { signature: "", url: "" }, "darwin-intel": { signature: "", url: "" },
"darwin-x86_64": { signature: "", url: "" },
"linux-x86_64": { signature: "", url: "" }, "linux-x86_64": { signature: "", url: "" },
"windows-x86_64": { signature: "", url: "" }, "windows-x86_64": { signature: "", url: "" },
"windows-i686": { signature: "", url: "" }, // no supported "windows-i686": { signature: "", url: "" }, // no supported
@ -68,12 +69,14 @@ async function resolveUpdater() {
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) { if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
updateData.platforms.darwin.url = browser_download_url; updateData.platforms.darwin.url = browser_download_url;
updateData.platforms["darwin-intel"].url = browser_download_url; updateData.platforms["darwin-intel"].url = browser_download_url;
updateData.platforms["darwin-x86_64"].url = browser_download_url;
} }
// darwin signature (intel) // darwin signature (intel)
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) { if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms.darwin.signature = sig; updateData.platforms.darwin.signature = sig;
updateData.platforms["darwin-intel"].signature = sig; updateData.platforms["darwin-intel"].signature = sig;
updateData.platforms["darwin-x86_64"].signature = sig;
} }
// darwin url (aarch) // darwin url (aarch)
@ -117,10 +120,7 @@ async function resolveUpdater() {
Object.entries(updateDataNew.platforms).forEach(([key, value]) => { Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
if (value.url) { if (value.url) {
updateDataNew.platforms[key].url = value.url.replace( updateDataNew.platforms[key].url = "https://ghproxy.com/" + value.url;
"https://github.com/",
"https://hub.fastgit.xyz/"
);
} else { } else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`); console.log(`[Error]: updateDataNew.platforms.${key} is null`);
} }

2105
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,37 +16,45 @@ tauri-build = { version = "1", features = [] }
warp = "0.3" warp = "0.3"
which = "4.2.2" which = "4.2.2"
anyhow = "1.0" anyhow = "1.0"
dirs = "4.0.0" dirs = "5.0.0"
open = "3.2.0" open = "4.0.1"
log = "0.4.14" log = "0.4.14"
ctrlc = "3.2.3" ctrlc = "3.2.3"
dunce = "1.0.2" dunce = "1.0.2"
log4rs = "1.0.0" log4rs = "1.0.0"
nanoid = "0.4.0" nanoid = "0.4.0"
chrono = "0.4.19" chrono = "0.4.19"
sysinfo = "0.27" sysinfo = "0.29"
sysproxy = "0.2" sysproxy = "0.3"
rquickjs = "0.1.7" rquickjs = "0.1.7"
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.9" serde_yaml = "0.9"
auto-launch = "0.4" auto-launch = "0.5"
once_cell = "1.14.0" once_cell = "1.14.0"
port_scanner = "0.1.5" port_scanner = "0.1.5"
delay_timer = "0.11.1" delay_timer = "0.11.1"
parking_lot = "0.12.0" parking_lot = "0.12.0"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json","rustls-tls"] }
tauri = { version = "1.1.1", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] } tauri = { version = "1.2.4", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
tauri-runtime-wry = { version = "0.12" }
window-vibrancy = { version = "0.3.0" } window-vibrancy = { version = "0.3.0" }
window-shadows = { version = "0.2.0" } window-shadows = { version = "0.2.0" }
wry = { version = "0.24.3" }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "0.2.1" runas = "1.1.0"
deelevate = "0.2.0" deelevate = "0.2.0"
winreg = { version = "0.10", features = ["transactions"] } winreg = { version = "0.50", features = ["transactions"] }
windows-sys = { version = "0.36", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] } 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] [features]
default = ["custom-protocol"] default = ["custom-protocol"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -175,6 +175,15 @@ pub async fn restart_sidecar() -> CmdResult {
wrap_err!(CoreManager::global().run_core().await) 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 /// get the system proxy
#[tauri::command] #[tauri::command]
pub fn get_sys_proxy() -> CmdResult<Mapping> { pub fn get_sys_proxy() -> CmdResult<Mapping> {
@ -220,6 +229,17 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
wrap_err!(open::that(url)) wrap_err!(open::that(url))
} }
#[tauri::command]
pub async fn clash_api_get_proxy_delay(
name: String,
url: Option<String>,
) -> CmdResult<clash_api::DelayRes> {
match clash_api::get_proxy_delay(name, url).await {
Ok(res) => Ok(res),
Err(err) => Err(format!("{}", err.to_string())),
}
}
#[cfg(windows)] #[cfg(windows)]
pub mod service { pub mod service {
use super::*; use super::*;

View file

@ -24,11 +24,23 @@ impl IClashTemp {
pub fn template() -> Self { pub fn template() -> Self {
let mut map = Mapping::new(); let mut map = Mapping::new();
map.insert("mixed-port".into(), 7890.into()); map.insert(
"mixed-port".into(),
match cfg!(feature = "default-meta") {
false => 7890.into(),
true => 7898.into(),
},
);
map.insert("log-level".into(), "info".into()); map.insert("log-level".into(), "info".into());
map.insert("allow-lan".into(), false.into()); map.insert("allow-lan".into(), false.into());
map.insert("mode".into(), "rule".into()); map.insert("mode".into(), "rule".into());
map.insert("external-controller".into(), "127.0.0.1:9090".into()); map.insert(
"external-controller".into(),
match cfg!(feature = "default-meta") {
false => "127.0.0.1:9090".into(),
true => "127.0.0.1:9098".into(),
},
);
map.insert("secret".into(), "".into()); map.insert("secret".into(), "".into());
Self(map) Self(map)

View file

@ -26,7 +26,7 @@ macro_rules! draft_define {
} }
pub fn draft(&self) -> MappedMutexGuard<$id> { pub fn draft(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |mut inner| { MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() { if inner.1.is_none() {
inner.1 = Some(inner.0.clone()); inner.1 = Some(inner.0.clone());
} }

View file

@ -190,7 +190,7 @@ impl PrfItem {
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false)); let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone()); let user_agent = opt_ref.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 { if self_proxy {
@ -282,8 +282,11 @@ impl PrfItem {
let name = name.unwrap_or(filename.unwrap_or("Remote File".into())); let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
let data = resp.text_with_charset("utf-8").await?; 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 // 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")?; .context("the remote profile data is invalid yaml")?;
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") { if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
@ -301,7 +304,7 @@ impl PrfItem {
extra, extra,
option, option,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(data), file_data: Some(data.into()),
}) })
} }

View file

@ -38,7 +38,7 @@ impl IProfiles {
} }
// compatible with the old old old version // compatible with the old old old version
profiles.items.as_mut().map(|items| { profiles.items.as_mut().map(|items| {
for mut item in items.iter_mut() { for item in items.iter_mut() {
if item.uid.is_none() { if item.uid.is_none() {
item.uid = Some(help::get_uid("d")); item.uid = Some(help::get_uid("d"));
} }
@ -55,7 +55,7 @@ impl IProfiles {
pub fn template() -> Self { pub fn template() -> Self {
Self { Self {
valid: Some(vec!["dns".into()]), valid: Some(vec!["dns".into(), "sub-rules".into(), "unified-delay".into()]),
items: Some(vec![]), items: Some(vec![]),
..Self::default() ..Self::default()
} }
@ -138,9 +138,9 @@ impl IProfiles {
let path = dirs::app_profiles_dir()?.join(&file); let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path) fs::File::create(path)
.context(format!("failed to create file \"{}\"", file))? .with_context(|| format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes()) .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() { if self.items.is_none() {
@ -155,7 +155,7 @@ impl IProfiles {
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or(vec![]); let mut items = self.items.take().unwrap_or(vec![]);
for mut each in items.iter_mut() { for each in items.iter_mut() {
if each.uid == Some(uid.clone()) { if each.uid == Some(uid.clone()) {
patch!(each, item, itype); patch!(each, item, itype);
patch!(each, item, name); patch!(each, item, name);
@ -189,7 +189,7 @@ impl IProfiles {
if let Some(items) = self.items.as_mut() { if let Some(items) = self.items.as_mut() {
let some_uid = Some(uid.clone()); let some_uid = Some(uid.clone());
for mut each in items.iter_mut() { for each in items.iter_mut() {
if each.uid == some_uid { if each.uid == some_uid {
each.extra = item.extra; each.extra = item.extra;
each.updated = item.updated; each.updated = item.updated;
@ -207,9 +207,9 @@ impl IProfiles {
let path = dirs::app_profiles_dir()?.join(&file); let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path) fs::File::create(path)
.context(format!("failed to create file \"{}\"", file))? .with_context(|| format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes()) .write(file_data.as_bytes())
.context(format!("failed to write to file \"{}\"", file))?; .with_context(|| format!("failed to write to file \"{}\"", file))?;
} }
break; break;

View file

@ -1,5 +1,6 @@
use crate::utils::{dirs, help}; use crate::utils::{dirs, help};
use anyhow::Result; use anyhow::Result;
use log::LevelFilter;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// ### `verge.yaml` schema /// ### `verge.yaml` schema
@ -8,6 +9,10 @@ pub struct IVerge {
/// app listening port for app singleton /// app listening port for app singleton
pub app_singleton_port: Option<u16>, pub app_singleton_port: Option<u16>,
/// app log level
/// silent | error | warn | info | debug | trace
pub app_log_level: Option<String>,
// i18n // i18n
pub language: Option<String>, pub language: Option<String>,
@ -21,6 +26,9 @@ pub struct IVerge {
/// enable traffic graph default is true /// enable traffic graph default is true
pub traffic_graph: Option<bool>, pub traffic_graph: Option<bool>,
/// show memory info (only for Clash Meta)
pub enable_memory_usage: Option<bool>,
/// clash tun mode /// clash tun mode
pub enable_tun_mode: Option<bool>, pub enable_tun_mode: Option<bool>,
@ -66,11 +74,22 @@ pub struct IVerge {
/// 默认的延迟测试连接 /// 默认的延迟测试连接
pub default_latency_test: Option<String>, pub default_latency_test: Option<String>,
/// 支持关闭字段过滤避免meta的新字段都被过滤掉默认为真
pub enable_clash_fields: Option<bool>,
/// 是否使用内部的脚本支持,默认为真 /// 是否使用内部的脚本支持,默认为真
pub enable_builtin_enhanced: Option<bool>, pub enable_builtin_enhanced: Option<bool>,
/// proxy 页面布局 列数 /// proxy 页面布局 列数
pub proxy_layout_column: Option<i32>, pub proxy_layout_column: Option<i32>,
/// 日志清理
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
pub auto_log_clean: Option<i32>,
/// window size and position
#[serde(skip_serializing_if = "Option::is_none")]
pub window_size_position: Option<Vec<f64>>,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
@ -113,6 +132,7 @@ impl IVerge {
theme_mode: Some("system".into()), theme_mode: Some("system".into()),
theme_blur: Some(false), theme_blur: Some(false),
traffic_graph: Some(true), traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_auto_launch: Some(false), enable_auto_launch: Some(false),
enable_silent_start: Some(false), enable_silent_start: Some(false),
enable_system_proxy: Some(false), enable_system_proxy: Some(false),
@ -120,6 +140,8 @@ impl IVerge {
proxy_guard_duration: Some(30), proxy_guard_duration: Some(30),
auto_close_connection: Some(true), auto_close_connection: Some(true),
enable_builtin_enhanced: Some(true), enable_builtin_enhanced: Some(true),
enable_clash_fields: Some(true),
auto_log_clean: Some(3),
..Self::default() ..Self::default()
} }
} }
@ -140,10 +162,12 @@ impl IVerge {
}; };
} }
patch!(app_log_level);
patch!(language); patch!(language);
patch!(theme_mode); patch!(theme_mode);
patch!(theme_blur); patch!(theme_blur);
patch!(traffic_graph); patch!(traffic_graph);
patch!(enable_memory_usage);
patch!(enable_tun_mode); patch!(enable_tun_mode);
patch!(enable_service_mode); patch!(enable_service_mode);
@ -163,6 +187,9 @@ impl IVerge {
patch!(default_latency_test); patch!(default_latency_test);
patch!(enable_builtin_enhanced); patch!(enable_builtin_enhanced);
patch!(proxy_layout_column); patch!(proxy_layout_column);
patch!(enable_clash_fields);
patch!(auto_log_clean);
patch!(window_size_position);
} }
/// 在初始化前尝试拿到单例端口的值 /// 在初始化前尝试拿到单例端口的值
@ -177,4 +204,21 @@ impl IVerge {
Err(_) => SERVER_PORT, // 这里就不log错误了 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
}
}
} }

View file

@ -1,6 +1,7 @@
use crate::config::Config; use crate::config::Config;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::collections::HashMap; use std::collections::HashMap;
@ -36,6 +37,32 @@ pub async fn patch_configs(config: &Mapping) -> Result<()> {
Ok(()) Ok(())
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct DelayRes {
delay: u64,
}
/// GET /proxies/{name}/delay
/// 获取代理延迟
pub async fn get_proxy_delay(name: String, test_url: Option<String>) -> Result<DelayRes> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/proxies/{name}/delay");
let default_url = "http://www.gstatic.com/generate_204";
let test_url = test_url
.map(|s| if s.is_empty() { default_url.into() } else { s })
.unwrap_or(default_url.into());
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client
.get(&url)
.headers(headers)
.query(&[("timeout", "10000"), ("url", &test_url)]);
let response = builder.send().await?;
Ok(response.json::<DelayRes>().await?)
}
/// 根据clash info获取clash服务地址和请求头 /// 根据clash info获取clash服务地址和请求头
fn clash_client_info() -> Result<(String, HeaderMap)> { fn clash_client_info() -> Result<(String, HeaderMap)> {
let client = { Config::clash().data().get_client_info() }; let client = { Config::clash().data().get_client_info() };

View file

@ -68,6 +68,10 @@ impl CoreManager {
if !output.status.success() { if !output.status.success() {
let error = clash_api::parse_check_output(output.stdout.clone()); let error = clash_api::parse_check_output(output.stdout.clone());
let error = match error.len() > 0 {
true => error,
false => output.stdout.clone(),
};
Logger::global().set_log(output.stdout); Logger::global().set_log(output.stdout);
bail!("{error}"); bail!("{error}");
} }
@ -136,6 +140,7 @@ impl CoreManager {
let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into()); let clash_core = clash_core.unwrap_or("clash".into());
let is_clash = clash_core == "clash";
let config_path = dirs::path_to_str(&config_path)?; let config_path = dirs::path_to_str(&config_path)?;
@ -167,13 +172,17 @@ impl CoreManager {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
match event { match event {
CommandEvent::Stdout(line) => { CommandEvent::Stdout(line) => {
let stdout = clash_api::parse_log(line.clone()); if is_clash {
log::info!(target: "app", "[clash]: {stdout}"); 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); Logger::global().set_log(line);
} }
CommandEvent::Stderr(err) => { CommandEvent::Stderr(err) => {
let stdout = clash_api::parse_log(err.clone()); // let stdout = clash_api::parse_log(err.clone());
log::error!(target: "app", "[clash]: {stdout}"); log::error!(target: "app", "[clash]: {err}");
Logger::global().set_log(err); Logger::global().set_log(err);
} }
CommandEvent::Error(err) => { CommandEvent::Error(err) => {
@ -215,7 +224,10 @@ impl CoreManager {
log::info!(target: "app", "recover clash core"); log::info!(target: "app", "recover clash core");
// 重新启动app // 重新启动app
if let Err(_) = self.run_core().await { 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(); let _ = self.recover_core();
} }
} }

View file

@ -4,7 +4,7 @@ use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use tauri::{AppHandle, GlobalShortcutManager}; use tauri::{AppHandle, GlobalShortcutManager};
use tauri_runtime_wry::wry::application::accelerator::Accelerator; use wry::application::accelerator::Accelerator;
pub struct Hotkey { pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置 current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
@ -76,6 +76,7 @@ impl Hotkey {
} }
let f = match func.trim() { let f = match func.trim() {
"open_dashboard" => || feat::open_dashboard(),
"clash_mode_rule" => || feat::change_clash_mode("rule".into()), "clash_mode_rule" => || feat::change_clash_mode("rule".into()),
"clash_mode_global" => || feat::change_clash_mode("global".into()), "clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()), "clash_mode_direct" => || feat::change_clash_mode("direct".into()),

View file

@ -0,0 +1,82 @@
use std::borrow::Cow;
/// 给clash内核的tun模式授权
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn grant_permission(core: String) -> anyhow::Result<()> {
use std::process::Command;
use tauri::utils::platform::current_exe;
let path = current_exe()?.with_file_name(core).canonicalize()?;
let path = path.display().to_string();
log::debug!("grant_permission path: {path}");
#[cfg(target_os = "macos")]
let output = {
// the path of clash /Applications/Clash Verge.app/Contents/MacOS/clash
// https://apple.stackexchange.com/questions/82967/problem-with-empty-spaces-when-executing-shell-commands-in-applescript
// let path = escape(&path);
let path = path.replace(' ', "\\\\ ");
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
Command::new("osascript")
.args(vec!["-e", &command])
.output()?
};
#[cfg(target_os = "linux")]
let output = {
let path = path.replace(' ', "\\ "); // 避免路径中有空格
let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}");
let sudo = match Command::new("which").arg("pkexec").output() {
Ok(output) => {
if output.stdout.is_empty() {
"sudo"
} else {
"pkexec"
}
}
Err(_) => "sudo",
};
Command::new(sudo).arg("sh").arg("-c").arg(shell).output()?
};
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
anyhow::bail!("{stderr}");
}
}
#[allow(unused)]
pub fn escape<'a>(text: &'a str) -> Cow<'a, str> {
let bytes = text.as_bytes();
let mut owned = None;
for pos in 0..bytes.len() {
let special = match bytes[pos] {
b' ' => Some(b' '),
_ => None,
};
if let Some(s) = special {
if owned.is_none() {
owned = Some(bytes[0..pos].to_owned());
}
owned.as_mut().unwrap().push(b'\\');
owned.as_mut().unwrap().push(b'\\');
owned.as_mut().unwrap().push(s);
} else if let Some(owned) = owned.as_mut() {
owned.push(bytes[pos]);
}
}
if let Some(owned) = owned {
unsafe { Cow::Owned(String::from_utf8_unchecked(owned)) }
} else {
unsafe { Cow::Borrowed(std::str::from_utf8_unchecked(bytes)) }
}
}

View file

@ -3,6 +3,7 @@ mod core;
pub mod handle; pub mod handle;
pub mod hotkey; pub mod hotkey;
pub mod logger; pub mod logger;
pub mod manager;
pub mod sysopt; pub mod sysopt;
pub mod timer; pub mod timer;
pub mod tray; pub mod tray;

View file

@ -25,7 +25,7 @@ pub struct Sysopt {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;<local>"; static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;<local>";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1"; static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,::1";
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>"; static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>";
@ -171,6 +171,24 @@ impl Sysopt {
})() })()
.unwrap_or(app_path); .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() let auto = AutoLaunchBuilder::new()
.set_app_name(app_name) .set_app_name(app_name)
.set_app_path(&app_path) .set_app_path(&app_path)

View file

@ -1,4 +1,4 @@
use crate::{config::Config, feat, utils::resolve}; use crate::{cmds, config::Config, feat, utils::resolve};
use anyhow::Result; use anyhow::Result;
use tauri::{ use tauri::{
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
@ -13,53 +13,81 @@ impl Tray {
let version = app_handle.package_info().version.to_string(); let version = app_handle.package_info().version.to_string();
if zh { macro_rules! t {
SystemTrayMenu::new() ($en: expr, $zh: expr) => {
.add_item(CustomMenuItem::new("open_window", "打开面板")) if zh {
.add_native_item(SystemTrayMenuItem::Separator) $zh
.add_item(CustomMenuItem::new("rule_mode", "规则模式")) } else {
.add_item(CustomMenuItem::new("global_mode", "全局模式")) $en
.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"))
} }
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"open_window",
t!("Dashboard", "打开面板"),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new(
"rule_mode",
t!("Rule Mode", "规则模式"),
))
.add_item(CustomMenuItem::new(
"global_mode",
t!("Global Mode", "全局模式"),
))
.add_item(CustomMenuItem::new(
"direct_mode",
t!("Direct Mode", "直连模式"),
))
.add_item(CustomMenuItem::new(
"script_mode",
t!("Script Mode", "脚本模式"),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new(
"system_proxy",
t!("System Proxy", "系统代理"),
))
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
.add_item(CustomMenuItem::new(
"copy_env",
t!("Copy Env", "复制环境变量"),
))
.add_submenu(SystemTraySubmenu::new(
t!("Open Dir", "打开目录"),
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"open_app_dir",
t!("App Dir", "应用目录"),
))
.add_item(CustomMenuItem::new(
"open_core_dir",
t!("Core Dir", "内核目录"),
))
.add_item(CustomMenuItem::new(
"open_logs_dir",
t!("Logs Dir", "日志目录"),
)),
))
.add_submenu(SystemTraySubmenu::new(
t!("More", "更多"),
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"restart_clash",
t!("Restart Clash", "重启 Clash"),
))
.add_item(CustomMenuItem::new(
"restart_app",
t!("Restart App", "重启应用"),
))
.add_item(
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q"))
} }
pub fn update_systray(app_handle: &AppHandle) -> Result<()> { pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
@ -93,6 +121,17 @@ impl Tray {
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.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("system_proxy").set_selected(*system_proxy);
let _ = tray.get_item("tun_mode").set_selected(*tun_mode); let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
@ -110,9 +149,15 @@ impl Tray {
"open_window" => resolve::create_window(app_handle), "open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(), "system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(), "tun_mode" => feat::toggle_tun_mode(),
"copy_env" => feat::copy_clash_env(),
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
"restart_clash" => feat::restart_clash_core(), "restart_clash" => feat::restart_clash_core(),
"restart_app" => api::process::restart(&app_handle.env()), "restart_app" => api::process::restart(&app_handle.env()),
"quit" => { "quit" => {
let _ = resolve::save_window_size_position(app_handle, true);
resolve::resolve_reset(); resolve::resolve_reset();
api::process::kill_children(); api::process::kill_children();
app_handle.exit(0); app_handle.exit(0);

View file

@ -1,4 +1,5 @@
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use std::collections::HashSet;
pub const HANDLE_FIELDS: [&str; 9] = [ pub const HANDLE_FIELDS: [&str; 9] = [
"mode", "mode",
@ -20,7 +21,7 @@ pub const DEFAULT_FIELDS: [&str; 5] = [
"rule-providers", "rule-providers",
]; ];
pub const OTHERS_FIELDS: [&str; 25] = [ pub const OTHERS_FIELDS: [&str; 30] = [
"dns", "dns",
"tun", "tun",
"ebpf", "ebpf",
@ -39,13 +40,18 @@ pub const OTHERS_FIELDS: [&str; 25] = [
"external-ui", "external-ui",
"bind-address", "bind-address",
"authentication", "authentication",
"tls", // meta "tls", // meta
"sniffer", // meta "sniffer", // meta
"listeners", // meta "geox-url", // meta
"sub-rules", // meta "listeners", // meta
"geodata-mode", // meta "sub-rules", // meta
"tcp-concurrent", // meta "geodata-mode", // meta
"enable-process", // 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> { pub fn use_clash_fields() -> Vec<String> {
@ -68,7 +74,11 @@ pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> {
.collect() .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(); let mut ret = Mapping::new();
for (key, value) in config.into_iter() { for (key, value) in config.into_iter() {
@ -94,7 +104,7 @@ pub fn use_lowercase(config: Mapping) -> Mapping {
ret ret
} }
pub fn use_sort(config: Mapping) -> Mapping { pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
let mut ret = Mapping::new(); let mut ret = Mapping::new();
HANDLE_FIELDS HANDLE_FIELDS
@ -107,6 +117,28 @@ pub fn use_sort(config: Mapping) -> Mapping {
ret.insert(key, value.clone()); 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 ret
} }

View file

@ -1,7 +1,6 @@
use super::{use_filter, use_lowercase}; use super::{use_filter, use_lowercase};
use serde_yaml::{self, Mapping, Sequence, Value}; use serde_yaml::{self, Mapping, Sequence, Value};
#[allow(unused)]
const MERGE_FIELDS: [&str; 6] = [ const MERGE_FIELDS: [&str; 6] = [
"prepend-rules", "prepend-rules",
"append-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_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"] ["rules", "proxies", "proxy-groups"]
.iter() .iter()

View file

@ -23,13 +23,14 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的配置 // config.yaml 的配置
let clash_config = { Config::clash().latest().0.clone() }; let clash_config = { Config::clash().latest().0.clone() };
let (clash_core, tun_mode, enable_builtin) = { let (clash_core, enable_tun, enable_builtin, enable_filter) = {
let verge = Config::verge(); let verge = Config::verge();
let verge = verge.latest(); let verge = verge.latest();
( (
verge.clash_core.clone(), verge.clash_core.clone(),
verge.enable_tun_mode.clone(), verge.enable_tun_mode.clone().unwrap_or(false),
verge.enable_builtin_enhanced.clone(), 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 mut exists_keys = use_keys(&config); // 保存出现过的keys
let valid = use_valid_fields(valid); let valid = use_valid_fields(valid);
config = use_filter(config, &valid); config = use_filter(config, &valid, enable_filter);
// 处理用户的profile // 处理用户的profile
chain.into_iter().for_each(|item| match item.data { chain.into_iter().for_each(|item| match item.data {
ChainType::Merge(merge) => { ChainType::Merge(merge) => {
exists_keys.extend(use_keys(&merge)); exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned()); config = use_merge(merge, config.to_owned());
config = use_filter(config.to_owned(), &valid); config = use_filter(config.to_owned(), &valid, enable_filter);
} }
ChainType::Script(script) => { ChainType::Script(script) => {
let mut logs = vec![]; let mut logs = vec![];
@ -73,7 +74,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
match use_script(script, config.to_owned()) { match use_script(script, config.to_owned()) {
Ok((res_config, res_logs)) => { Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config)); exists_keys.extend(use_keys(&res_config));
config = use_filter(res_config, &valid); config = use_filter(res_config, &valid, enable_filter);
logs.extend(res_logs); logs.extend(res_logs);
} }
Err(err) => logs.push(("exception".into(), err.to_string())), Err(err) => logs.push(("exception".into(), err.to_string())),
@ -91,7 +92,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
let clash_fields = use_clash_fields(); let clash_fields = use_clash_fields();
// 内建脚本最后跑 // 内建脚本最后跑
if enable_builtin.unwrap_or(true) { if enable_builtin {
ChainItem::builtin() ChainItem::builtin()
.into_iter() .into_iter()
.filter(|(s, _)| s.is_support(clash_core.as_ref())) .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 { match item.data {
ChainType::Script(script) => match use_script(script, config.to_owned()) { ChainType::Script(script) => match use_script(script, config.to_owned()) {
Ok((res_config, _)) => { Ok((res_config, _)) => {
config = use_filter(res_config, &clash_fields); config = use_filter(res_config, &clash_fields, enable_filter);
} }
Err(err) => { Err(err) => {
log::error!(target: "app", "builtin script error `{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_filter(config, &clash_fields, enable_filter);
config = use_tun(config, tun_mode.unwrap_or(false)); config = use_tun(config, enable_tun);
config = use_sort(config); config = use_sort(config, enable_filter);
let mut exists_set = HashSet::new(); let mut exists_set = HashSet::new();
exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s))); exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));

View file

@ -7,8 +7,19 @@
use crate::config::*; use crate::config::*;
use crate::core::*; use crate::core::*;
use crate::log_err; use crate::log_err;
use crate::utils::resolve;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use serde_yaml::{Mapping, Value}; 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 // 重启clash
pub fn restart_clash_core() { pub fn restart_clash_core() {
@ -319,3 +330,12 @@ async fn update_core_config() -> Result<()> {
} }
} }
} }
/// copy env variable
pub fn copy_clash_env() {
let port = { Config::clash().data().get_client_info().port };
let text = format!("export https_proxy=http://127.0.0.1:{port} http_proxy=http://127.0.0.1:{port} all_proxy=socks5://127.0.0.1:{port}");
let mut cliboard = Clipboard::new();
cliboard.write_text(text);
}

View file

@ -36,6 +36,7 @@ fn main() -> std::io::Result<()> {
cmds::open_core_dir, cmds::open_core_dir,
// cmds::kill_sidecar, // cmds::kill_sidecar,
cmds::restart_sidecar, cmds::restart_sidecar,
cmds::grant_permission,
// clash // clash
cmds::get_clash_info, cmds::get_clash_info,
cmds::get_clash_logs, cmds::get_clash_logs,
@ -65,6 +66,8 @@ fn main() -> std::io::Result<()> {
cmds::service::check_service, cmds::service::check_service,
cmds::service::install_service, cmds::service::install_service,
cmds::service::uninstall_service, cmds::service::uninstall_service,
// clash api
cmds::clash_api_get_proxy_delay
]); ]);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -108,6 +111,8 @@ fn main() -> std::io::Result<()> {
match event { match event {
tauri::WindowEvent::CloseRequested { api, .. } => { tauri::WindowEvent::CloseRequested { api, .. } => {
api.prevent_close(); api.prevent_close();
let _ = resolve::save_window_size_position(&app_handle, true);
app_handle.get_window("main").map(|win| { app_handle.get_window("main").map(|win| {
let _ = win.hide(); let _ = win.hide();
}); });
@ -116,6 +121,20 @@ fn main() -> std::io::Result<()> {
} }
} }
} }
#[cfg(not(target_os = "macos"))]
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { .. } => {
let _ = resolve::save_window_size_position(&app_handle, true);
}
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
let _ = resolve::save_window_size_position(&app_handle, false);
}
_ => {}
}
}
}
_ => {} _ => {}
}); });

View file

@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result};
use nanoid::nanoid; use nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use serde_yaml::{Mapping, Value}; 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 /// read data from yaml as struct T
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<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) 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!( serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
"failed to read the file with yaml format \"{}\"", format!(
path.display() "failed to read the file with yaml format \"{}\"",
)) path.display()
)
})
} }
/// read mapping from yaml fix #165 /// read mapping from yaml fix #165
pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> { pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
let mut val: Value = read_yaml(path)?; let mut val: Value = read_yaml(path)?;
val.apply_merge() val.apply_merge()
.context(format!("failed to apply merge \"{}\"", path.display()))?; .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
Ok(val Ok(val
.as_mapping() .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(); 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] = [ const ALPHABET: [char; 62] = [
@ -79,29 +82,18 @@ pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
/// open file /// open file
/// use vscode by default /// use vscode by default
pub fn open_file(path: PathBuf) -> Result<()> { 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 // use vscode first
if let Ok(code) = which::which("code") { if let Err(err) = open::with(&path, code) {
let mut command = Command::new(&code); log::error!(target: "app", "failed to open file with VScode `{err}`");
// default open
#[cfg(target_os = "windows")] open::that(path)?;
{
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(());
} }
open::that(path)?;
Ok(()) 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 /// wrap the anyhow error
/// transform the error to String /// transform the error to String
#[macro_export] #[macro_export]

View file

@ -1,13 +1,14 @@
use crate::config::*; use crate::config::*;
use crate::utils::{dirs, help}; use crate::utils::{dirs, help};
use anyhow::Result; use anyhow::Result;
use chrono::Local; use chrono::{DateTime, Local};
use log::LevelFilter; use log::LevelFilter;
use log4rs::append::console::ConsoleAppender; use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender; use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Config, Logger, Root}; use log4rs::config::{Appender, Logger, Root};
use log4rs::encode::pattern::PatternEncoder; use log4rs::encode::pattern::PatternEncoder;
use std::fs; use std::fs::{self, DirEntry};
use std::str::FromStr;
use tauri::PackageInfo; use tauri::PackageInfo;
/// initialize this instance's log file /// initialize this instance's log file
@ -17,42 +18,126 @@ fn init_log() -> Result<()> {
let _ = fs::create_dir_all(&log_dir); 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 local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
let log_file = format!("{}.log", local_time); let log_file = format!("{}.log", local_time);
let log_file = log_dir.join(log_file); let log_file = log_dir.join(log_file);
#[cfg(feature = "verge-dev")] let log_pattern = match log_level {
let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {M} {m}{n}"; LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
#[cfg(not(feature = "verge-dev"))] _ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
let time_format = "{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 stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
let tofile = FileAppender::builder().encoder(encode).build(log_file)?; let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
#[cfg(feature = "verge-dev")] let mut logger_builder = Logger::builder();
let level = LevelFilter::Debug; let mut root_builder = Root::builder();
#[cfg(not(feature = "verge-dev"))]
let level = LevelFilter::Info;
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("stdout", Box::new(stdout)))
.appender(Appender::builder().build("file", Box::new(tofile))) .appender(Appender::builder().build("file", Box::new(tofile)))
.logger( .logger(logger_builder.additive(false).build("app", log_level))
Logger::builder() .build_lossy(root_builder.build(log_level));
.appenders(["file", "stdout"])
.additive(false)
.build("app", level),
)
.build(Root::builder().appender("stdout").build(LevelFilter::Info))?;
log4rs::init_config(config)?; log4rs::init_config(config)?;
Ok(()) Ok(())
} }
/// Initialize all the files from resources /// 删除log文件
pub fn delete_log() -> Result<()> {
let log_dir = dirs::app_logs_dir()?;
if !log_dir.exists() {
return Ok(());
}
let auto_log_clean = {
let verge = Config::verge();
let verge = verge.data();
verge.auto_log_clean.clone().unwrap_or(0)
};
let day = match auto_log_clean {
1 => 7,
2 => 30,
3 => 90,
_ => return Ok(()),
};
log::debug!(target: "app", "try to delete log files, day: {day}");
// %Y-%m-%d to NaiveDateTime
let parse_time_str = |s: &str| {
let sa: Vec<&str> = s.split('-').collect();
if sa.len() != 4 {
return Err(anyhow::anyhow!("invalid time str"));
}
let year = i32::from_str(sa[0])?;
let month = u32::from_str(sa[1])?;
let day = u32::from_str(sa[2])?;
let time = chrono::NaiveDate::from_ymd_opt(year, month, day)
.ok_or(anyhow::anyhow!("invalid time str"))?
.and_hms_opt(0, 0, 0)
.ok_or(anyhow::anyhow!("invalid time str"))?;
Ok(time)
};
let process_file = |file: DirEntry| -> Result<()> {
let file_name = file.file_name();
let file_name = file_name.to_str().unwrap_or_default();
if file_name.ends_with(".log") {
let now = Local::now();
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
let file_time = DateTime::<Local>::from_local(created_time, now.offset().clone());
let duration = now.signed_duration_since(file_time);
if duration.num_days() > day {
let file_path = file.path();
let _ = fs::remove_file(file_path);
log::info!(target: "app", "delete log file: {file_name}");
}
}
Ok(())
};
for file in fs::read_dir(&log_dir)? {
if let Ok(file) = file {
let _ = process_file(file);
}
}
Ok(())
}
/// Initialize all the config files
/// before tauri setup
pub fn init_config() -> Result<()> { pub fn init_config() -> Result<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
unsafe { unsafe {
@ -60,6 +145,7 @@ pub fn init_config() -> Result<()> {
} }
let _ = init_log(); let _ = init_log();
let _ = delete_log();
crate::log_err!(dirs::app_home_dir().map(|app_dir| { crate::log_err!(dirs::app_home_dir().map(|app_dir| {
if !app_dir.exists() { if !app_dir.exists() {
@ -97,7 +183,8 @@ pub fn init_config() -> Result<()> {
Ok(()) Ok(())
} }
/// initialize app /// initialize app resources
/// after tauri setup
pub fn init_resources(package_info: &PackageInfo) -> Result<()> { pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
let app_dir = dirs::app_home_dir()?; let app_dir = dirs::app_home_dir()?;
let res_dir = dirs::app_resources_dir(package_info)?; let res_dir = dirs::app_resources_dir(package_info)?;
@ -109,13 +196,47 @@ pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
let _ = fs::create_dir_all(&res_dir); 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 // 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 src_path = res_dir.join(file);
let target_path = app_dir.join(file); let dest_path = app_dir.join(file);
if src_path.exists() {
let _ = fs::copy(src_path, target_path); 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(()) Ok(())

View file

@ -1,6 +1,6 @@
use crate::config::Config; use crate::{config::Config, core::*, utils::init, utils::server};
use crate::log_err; use crate::{log_err, trace_err};
use crate::{core::*, utils::init, utils::server}; use anyhow::Result;
use tauri::{App, AppHandle, Manager}; use tauri::{App, AppHandle, Manager};
/// handle something when start app /// 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_err!(init::init_resources(app.package_info()));
// 启动核心 // 启动核心
log::trace!("init config");
log_err!(Config::init_config()); log_err!(Config::init_config());
log::trace!("launch core");
log_err!(CoreManager::global().init()); log_err!(CoreManager::global().init());
// setup a simple http server for singleton // setup a simple http server for singleton
log::trace!("launch embed server");
server::embed_server(app.app_handle()); server::embed_server(app.app_handle());
log::trace!("init system tray");
log_err!(tray::Tray::update_systray(&app.app_handle())); log_err!(tray::Tray::update_systray(&app.app_handle()));
let silent_start = { Config::verge().data().enable_silent_start.clone() }; let silent_start = { Config::verge().data().enable_silent_start.clone() };
@ -43,22 +48,47 @@ pub fn resolve_reset() {
/// create main window /// create main window
pub fn create_window(app_handle: &AppHandle) { pub fn create_window(app_handle: &AppHandle) {
if let Some(window) = app_handle.get_window("main") { if let Some(window) = app_handle.get_window("main") {
let _ = window.unminimize(); trace_err!(window.unminimize(), "set win unminimize");
let _ = window.show(); trace_err!(window.show(), "set win visible");
let _ = window.set_focus(); trace_err!(window.set_focus(), "set win focus");
return; return;
} }
let builder = tauri::window::WindowBuilder::new( let mut builder = tauri::window::WindowBuilder::new(
app_handle, app_handle,
"main".to_string(), "main".to_string(),
tauri::WindowUrl::App("index.html".into()), tauri::WindowUrl::App("index.html".into()),
) )
.title("Clash Verge") .title("Clash Verge")
.center()
.fullscreen(false) .fullscreen(false)
.min_inner_size(600.0, 520.0); .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")] #[cfg(target_os = "windows")]
{ {
use std::time::Duration; use std::time::Duration;
@ -68,38 +98,82 @@ pub fn create_window(app_handle: &AppHandle) {
match builder match builder
.decorations(false) .decorations(false)
.transparent(true) .transparent(true)
.inner_size(800.0, 636.0)
.visible(false) .visible(false)
.build() .build()
{ {
Ok(_) => { Ok(win) => {
let app_handle = app_handle.clone(); 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") { if pos.x < -400
let _ = set_shadow(&window, true); || 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 { 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") { if let Some(window) = app_handle.get_window("main") {
let _ = window.show(); trace_err!(set_shadow(&window, true), "set win shadow");
let _ = window.unminimize(); trace_err!(window.show(), "set win visible");
let _ = window.set_focus(); 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")] #[cfg(target_os = "macos")]
crate::log_err!(builder.decorations(true).inner_size(800.0, 642.0).build());
#[cfg(target_os = "linux")]
crate::log_err!(builder crate::log_err!(builder
.decorations(true) .decorations(true)
.transparent(false) .hidden_title(true)
.inner_size(800.0, 642.0) .title_bar_style(tauri::TitleBarStyle::Overlay)
.build()); .build());
#[cfg(target_os = "linux")]
crate::log_err!(builder.decorations(true).transparent(false).build());
}
/// save window size and position
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
let win = app_handle
.get_window("main")
.ok_or(anyhow::anyhow!("failed to get window"))?;
let scale = win.scale_factor()?;
let size = win.inner_size()?;
let size = size.to_logical::<f64>(scale);
let pos = win.outer_position()?;
let pos = pos.to_logical::<f64>(scale);
let verge = Config::verge();
let mut verge = verge.latest();
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
if save_to_file {
verge.save_file()?;
}
Ok(())
} }

View file

@ -1,7 +1,7 @@
{ {
"package": { "package": {
"productName": "Clash Verge", "productName": "Clash Verge",
"version": "1.2.2" "version": "1.3.8"
}, },
"build": { "build": {
"distDir": "../dist", "distDir": "../dist",
@ -32,7 +32,7 @@
"shortDescription": "A Clash GUI based on tauri.", "shortDescription": "A Clash GUI based on tauri.",
"longDescription": "A Clash GUI based on tauri.", "longDescription": "A Clash GUI based on tauri.",
"deb": { "deb": {
"depends": [] "depends": ["openssl"]
}, },
"macOS": { "macOS": {
"frameworks": [], "frameworks": [],
@ -46,15 +46,15 @@
"digestAlgorithm": "sha256", "digestAlgorithm": "sha256",
"timestampUrl": "", "timestampUrl": "",
"wix": { "wix": {
"language": ["zh-CN", "en-US"] "language": ["zh-CN", "en-US", "ru-RU"]
} }
} }
}, },
"updater": { "updater": {
"active": true, "active": true,
"endpoints": [ "endpoints": [
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json", "https://ghproxy.com/https://github.com/zzzgydi/clash-verge/releases/download/updater/update-proxy.json",
"https://hub.fastgit.xyz/zzzgydi/clash-verge/releases/download/updater/update-proxy.json" "https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json"
], ],
"dialog": false, "dialog": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"

View file

@ -104,3 +104,14 @@
} }
} }
} }
.macos {
&.layout {
.layout__left {
padding-top: 24px;
}
.layout__right .the-content {
top: 20px;
}
}
}

View file

@ -1,4 +1,4 @@
import { forwardRef, ReactNode, useImperativeHandle, useState } from "react"; import { ReactNode } from "react";
import { import {
Button, Button,
Dialog, Dialog,
@ -18,6 +18,7 @@ interface Props {
disableCancel?: boolean; disableCancel?: boolean;
disableFooter?: boolean; disableFooter?: boolean;
contentSx?: SxProps<Theme>; contentSx?: SxProps<Theme>;
children?: ReactNode;
onOk?: () => void; onOk?: () => void;
onCancel?: () => void; onCancel?: () => void;
onClose?: () => void; onClose?: () => void;

View file

@ -1,15 +1,26 @@
import { ReactNode } from "react";
import { ErrorBoundary, FallbackProps } from "react-error-boundary"; import { ErrorBoundary, FallbackProps } from "react-error-boundary";
function ErrorFallback({ error }: FallbackProps) { function ErrorFallback({ error }: FallbackProps) {
return ( return (
<div role="alert"> <div role="alert" style={{ padding: 16 }}>
<p>Something went wrong:(</p> <h4>Something went wrong:(</h4>
<pre>{error.message}</pre> <pre>{error.message}</pre>
<details title="Error Stack">
<summary>Error Stack</summary>
<pre>{error.stack}</pre>
</details>
</div> </div>
); );
} }
export const BaseErrorBoundary: React.FC = (props) => { interface Props {
children?: ReactNode;
}
export const BaseErrorBoundary = (props: Props) => {
return ( return (
<ErrorBoundary FallbackComponent={ErrorFallback}> <ErrorBoundary FallbackComponent={ErrorFallback}>
{props.children} {props.children}

View file

@ -1,4 +1,4 @@
import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material"; import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material";
import { Close, CheckCircleRounded, ErrorRounded } from "@mui/icons-material"; import { Close, CheckCircleRounded, ErrorRounded } from "@mui/icons-material";
@ -77,13 +77,14 @@ export const Notice: NoticeInstance = (props) => {
const container = document.createElement("div"); const container = document.createElement("div");
parent.appendChild(container); parent.appendChild(container);
const root = createRoot(container);
const onUnmount = () => { const onUnmount = () => {
const result = ReactDOM.unmountComponentAtNode(container); root.unmount();
if (result && parent) setTimeout(() => parent.removeChild(container), 500); 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) => { (["info", "error", "success"] as const).forEach((type) => {

View file

@ -1,4 +1,4 @@
import React from "react"; import React, { ReactNode } from "react";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import { BaseErrorBoundary } from "./base-error-boundary"; import { BaseErrorBoundary } from "./base-error-boundary";
@ -6,6 +6,7 @@ interface Props {
title?: React.ReactNode; // the page title title?: React.ReactNode; // the page title
header?: React.ReactNode; // something behind title header?: React.ReactNode; // something behind title
contentStyle?: React.CSSProperties; contentStyle?: React.CSSProperties;
children?: ReactNode;
} }
export const BasePage: React.FC<Props> = (props) => { export const BasePage: React.FC<Props> = (props) => {

View file

@ -0,0 +1,104 @@
import dayjs from "dayjs";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { Box, Button, Snackbar } from "@mui/material";
import { deleteConnection } from "@/services/api";
import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic";
export interface ConnectionDetailRef {
open: (detail: IConnectionsItem) => void;
}
export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
(props, ref) => {
const [open, setOpen] = useState(false);
const [detail, setDetail] = useState<IConnectionsItem>(null!);
useImperativeHandle(ref, () => ({
open: (detail: IConnectionsItem) => {
if (open) return;
setOpen(true);
setDetail(detail);
},
}));
const onClose = () => setOpen(false);
return (
<Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
open={open}
onClose={onClose}
message={
detail ? (
<InnerConnectionDetail data={detail} onClose={onClose} />
) : null
}
/>
);
}
);
interface InnerProps {
data: IConnectionsItem;
onClose?: () => void;
}
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
const { metadata, rulePayload } = data;
const chains = [...data.chains].reverse().join(" / ");
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
const host = metadata.host
? `${metadata.host}:${metadata.destinationPort}`
: `${metadata.destinationIP}:${metadata.destinationPort}`;
const information = [
{ label: "Host", value: host },
{ label: "Download", value: parseTraffic(data.download).join(" ") },
{ label: "Upload", value: parseTraffic(data.upload).join(" ") },
{
label: "DL Speed",
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
},
{
label: "UL Speed",
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
},
{ label: "Chains", value: chains },
{ label: "Rule", value: rule },
{
label: "Process",
value: truncateStr(metadata.process || metadata.processPath),
},
{ label: "Time", value: dayjs(data.start).fromNow() },
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
{ label: "Destination IP", value: metadata.destinationIP },
{ label: "Type", value: `${metadata.type}(${metadata.network})` },
];
const onDelete = useLockFn(async () => deleteConnection(data.id));
return (
<Box sx={{ userSelect: "text" }}>
{information.map((each) => (
<div key={each.label}>
<b>{each.label}</b>: <span>{each.value}</span>
</div>
))}
<Box sx={{ textAlign: "right" }}>
<Button
variant="contained"
title="Close Connection"
onClick={() => {
onDelete();
onClose?.();
}}
>
Close
</Button>
</Box>
</Box>
);
};

View file

@ -24,10 +24,11 @@ const Tag = styled("span")(({ theme }) => ({
interface Props { interface Props {
value: IConnectionsItem; value: IConnectionsItem;
onShowDetail?: () => void;
} }
const ConnectionItem = (props: Props) => { export const ConnectionItem = (props: Props) => {
const { value } = props; const { value, onShowDetail } = props;
const { id, metadata, chains, start, curUpload, curDownload } = value; const { id, metadata, chains, start, curUpload, curDownload } = value;
@ -44,8 +45,9 @@ const ConnectionItem = (props: Props) => {
} }
> >
<ListItemText <ListItemText
sx={{ userSelect: "text" }} sx={{ userSelect: "text", cursor: "pointer" }}
primary={metadata.host || metadata.destinationIP} primary={metadata.host || metadata.destinationIP}
onClick={onShowDetail}
secondary={ secondary={
<Box sx={{ display: "flex", flexWrap: "wrap" }}> <Box sx={{ display: "flex", flexWrap: "wrap" }}>
<Tag sx={{ textTransform: "uppercase", color: "success" }}> <Tag sx={{ textTransform: "uppercase", color: "success" }}>
@ -56,7 +58,7 @@ const ConnectionItem = (props: Props) => {
{!!metadata.process && <Tag>{metadata.process}</Tag>} {!!metadata.process && <Tag>{metadata.process}</Tag>}
{chains.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>} {chains?.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>}
<Tag>{dayjs(start).fromNow()}</Tag> <Tag>{dayjs(start).fromNow()}</Tag>
@ -71,5 +73,3 @@ const ConnectionItem = (props: Props) => {
</ListItem> </ListItem>
); );
}; };
export default ConnectionItem;

View file

@ -1,32 +1,29 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { DataGrid, GridColDef } from "@mui/x-data-grid"; import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
interface Props { interface Props {
connections: IConnectionsItem[]; connections: IConnectionsItem[];
onShowDetail: (data: IConnectionsItem) => void;
} }
const ConnectionTable = (props: Props) => { export const ConnectionTable = (props: Props) => {
const { connections } = props; const { connections, onShowDetail } = props;
const [columnVisible, setColumnVisible] = useState<
Partial<Record<keyof IConnectionsItem, boolean>>
>({});
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ { field: "host", headerName: "Host", flex: 220, minWidth: 220 },
field: "host",
headerName: "Host",
flex: 200,
minWidth: 200,
resizable: false,
disableColumnMenu: true,
},
{ {
field: "download", field: "download",
headerName: "Download", headerName: "Download",
width: 88, width: 88,
align: "right", align: "right",
headerAlign: "right", headerAlign: "right",
disableColumnMenu: true,
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
}, },
{ {
field: "upload", field: "upload",
@ -34,18 +31,13 @@ const ConnectionTable = (props: Props) => {
width: 88, width: 88,
align: "right", align: "right",
headerAlign: "right", headerAlign: "right",
disableColumnMenu: true,
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
}, },
{ {
field: "dlSpeed", field: "dlSpeed",
headerName: "DL Speed", headerName: "DL Speed",
align: "right",
width: 88, width: 88,
align: "right",
headerAlign: "right", headerAlign: "right",
disableColumnMenu: true,
valueFormatter: (params: any) =>
parseTraffic(params.value).join(" ") + "/s",
}, },
{ {
field: "ulSpeed", field: "ulSpeed",
@ -53,55 +45,26 @@ const ConnectionTable = (props: Props) => {
width: 88, width: 88,
align: "right", align: "right",
headerAlign: "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", field: "time",
headerName: "Time", headerName: "Time",
width: 120, flex: 120,
minWidth: 100,
align: "right", align: "right",
headerAlign: "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", field: "destinationIP",
headerName: "Destination IP", headerName: "Destination IP",
width: 125, flex: 200,
disableColumnMenu: true, minWidth: 130,
},
{
field: "type",
headerName: "Type",
width: 160,
disableColumnMenu: true,
}, },
{ field: "type", headerName: "Type", flex: 160, minWidth: 100 },
]; ];
const connRows = useMemo(() => { const connRows = useMemo(() => {
@ -115,30 +78,33 @@ const ConnectionTable = (props: Props) => {
host: metadata.host host: metadata.host
? `${metadata.host}:${metadata.destinationPort}` ? `${metadata.host}:${metadata.destinationPort}`
: `${metadata.destinationIP}:${metadata.destinationPort}`, : `${metadata.destinationIP}:${metadata.destinationPort}`,
download: each.download, download: parseTraffic(each.download).join(" "),
upload: each.upload, upload: parseTraffic(each.upload).join(" "),
dlSpeed: each.curDownload, dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s",
ulSpeed: each.curUpload, ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s",
chains, chains,
rule, rule,
process: metadata.process || metadata.processPath, process: truncateStr(metadata.process || metadata.processPath),
time: each.start, time: dayjs(each.start).fromNow(),
source: `${metadata.sourceIP}:${metadata.sourcePort}`, source: `${metadata.sourceIP}:${metadata.sourcePort}`,
destinationIP: metadata.destinationIP, destinationIP: metadata.destinationIP,
type: `${metadata.type}(${metadata.network})`, type: `${metadata.type}(${metadata.network})`,
connectionData: each,
}; };
}); });
}, [connections]); }, [connections]);
return ( return (
<DataGrid <DataGrid
hideFooter
rows={connRows} rows={connRows}
columns={columns} columns={columns}
onRowClick={(e) => onShowDetail(e.row.connectionData)}
density="compact" density="compact"
sx={{ border: "none", "div:focus": { outline: "none !important" } }} sx={{ border: "none", "div:focus": { outline: "none !important" } }}
hideFooter columnVisibilityModel={columnVisible}
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
/> />
); );
}; };
export default ConnectionTable;

View file

@ -6,7 +6,7 @@ import {
HorizontalRuleRounded, HorizontalRuleRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
const LayoutControl = () => { export const LayoutControl = () => {
const minWidth = 40; const minWidth = 40;
return ( return (
@ -37,5 +37,3 @@ const LayoutControl = () => {
</> </>
); );
}; };
export default LayoutControl;

View file

@ -2,7 +2,7 @@ import { alpha, ListItem, ListItemButton, ListItemText } from "@mui/material";
import { useMatch, useResolvedPath, useNavigate } from "react-router-dom"; import { useMatch, useResolvedPath, useNavigate } from "react-router-dom";
import type { LinkProps } 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 { to, children } = props;
const resolved = useResolvedPath(to); const resolved = useResolvedPath(to);
@ -40,5 +40,3 @@ const LayoutItem = (props: LinkProps) => {
</ListItem> </ListItem>
); );
}; };
export default LayoutItem;

View file

@ -1,93 +1,83 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Box, Typography } from "@mui/material"; 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 { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { TrafficGraph, type TrafficRef } from "./traffic-graph"; import { TrafficGraph, type TrafficRef } from "./traffic-graph";
import { useLogSetup } from "./use-log-setup"; import { useLogSetup } from "./use-log-setup";
import { useVisibility } from "@/hooks/use-visibility";
import { useWebsocket } from "@/hooks/use-websocket";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
// setup the traffic // setup the traffic
const LayoutTraffic = () => { export const LayoutTraffic = () => {
const { clashInfo } = useClashInfo(); const { clashInfo } = useClashInfo();
const { verge } = useVerge();
// whether hide traffic graph // whether hide traffic graph
const { verge } = useVerge();
const trafficGraph = verge?.traffic_graph ?? true; const trafficGraph = verge?.traffic_graph ?? true;
const trafficRef = useRef<TrafficRef>(null); const trafficRef = useRef<TrafficRef>(null);
const [traffic, setTraffic] = useState({ up: 0, down: 0 }); const [traffic, setTraffic] = useState({ up: 0, down: 0 });
const [memory, setMemory] = useState({ inuse: 0 });
const wsRef = useRef<WebSocket | null>(null); const pageVisible = useVisibility();
const [refresh, setRefresh] = useState({});
// setup log ws during layout // setup log ws during layout
useLogSetup(); useLogSetup();
const { connect, disconnect } = useWebsocket((event) => {
const data = JSON.parse(event.data) as ITrafficItem;
trafficRef.current?.appendData(data);
setTraffic(data);
});
useEffect(() => { useEffect(() => {
if (!clashInfo) return; if (!clashInfo || !pageVisible) return;
const { server = "", secret = "" } = clashInfo; 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);
});
ws.addEventListener("error", () => {
setTimeout(() => {
if (document.visibilityState === "visible") {
setRefresh({});
}
}, 1000);
});
ws.addEventListener("close", () => {
setTimeout(() => {
if (document.visibilityState === "visible") {
setRefresh({});
}
}, 1000);
});
wsRef.current = ws;
return () => { return () => {
ws?.close(); disconnect();
wsRef.current = null;
}; };
}, [clashInfo, refresh]); }, [clashInfo, pageVisible]);
/* --------- 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(() => { useEffect(() => {
const handleVisibleChange = () => { if (!clashInfo || !pageVisible || !displayMemory) return;
if (document.visibilityState === "visible") { const { server = "", secret = "" } = clashInfo;
// reconnect websocket memoryWs.connect(
if ( `ws://${server}/memory?token=${encodeURIComponent(secret)}`
wsRef.current && );
wsRef.current.readyState !== WebSocket.CONNECTING return () => memoryWs.disconnect();
) { }, [clashInfo, pageVisible, displayMemory]);
setRefresh({});
}
}
};
document.addEventListener("visibilitychange", handleVisibleChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibleChange);
};
}, []);
const [up, upUnit] = parseTraffic(traffic.up); const [up, upUnit] = parseTraffic(traffic.up);
const [down, downUnit] = parseTraffic(traffic.down); const [down, downUnit] = parseTraffic(traffic.down);
const [inuse, inuseUnit] = parseTraffic(memory.inuse);
const iconStyle: any = {
sx: { mr: "8px", fontSize: 16 },
};
const valStyle: any = { const valStyle: any = {
component: "span", component: "span",
color: "primary", color: "primary",
textAlign: "center", textAlign: "center",
sx: { flex: "1 1 54px", userSelect: "none" }, sx: { flex: "1 1 56px", userSelect: "none" },
}; };
const unitStyle: any = { const unitStyle: any = {
component: "span", component: "span",
@ -103,31 +93,44 @@ const LayoutTraffic = () => {
position="relative" position="relative"
onClick={trafficRef.current?.toggleStyle} onClick={trafficRef.current?.toggleStyle}
> >
{trafficGraph && ( {trafficGraph && pageVisible && (
<div style={{ width: "100%", height: 60, marginBottom: 6 }}> <div style={{ width: "100%", height: 60, marginBottom: 6 }}>
<TrafficGraph ref={trafficRef} /> <TrafficGraph ref={trafficRef} />
</div> </div>
)} )}
<Box mb={1.5} display="flex" alignItems="center" whiteSpace="nowrap"> <Box display="flex" flexDirection="column" gap={0.75}>
<ArrowUpward <Box display="flex" alignItems="center" whiteSpace="nowrap">
sx={{ mr: 0.75, fontSize: 18 }} <ArrowUpward
color={+up > 0 ? "primary" : "disabled"} {...iconStyle}
/> color={+up > 0 ? "primary" : "disabled"}
<Typography {...valStyle}>{up}</Typography> />
<Typography {...unitStyle}>{upUnit}/s</Typography> <Typography {...valStyle}>{up}</Typography>
</Box> <Typography {...unitStyle}>{upUnit}/s</Typography>
</Box>
<Box display="flex" alignItems="center" whiteSpace="nowrap"> <Box display="flex" alignItems="center" whiteSpace="nowrap">
<ArrowDownward <ArrowDownward
sx={{ mr: 0.75, fontSize: 18 }} {...iconStyle}
color={+down > 0 ? "primary" : "disabled"} color={+down > 0 ? "primary" : "disabled"}
/> />
<Typography {...valStyle}>{down}</Typography> <Typography {...valStyle}>{down}</Typography>
<Typography {...unitStyle}>{downUnit}/s</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>
</Box> </Box>
); );
}; };
export default LayoutTraffic;

View file

@ -1,17 +1,19 @@
import useSWR from "swr"; import useSWR from "swr";
import { useState } from "react"; import { useRef } from "react";
import { Button } from "@mui/material"; import { Button } from "@mui/material";
import { checkUpdate } from "@tauri-apps/api/updater"; 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 { interface Props {
className?: string; className?: string;
} }
const UpdateButton = (props: Props) => { export const UpdateButton = (props: Props) => {
const { className } = props; const { className } = props;
const [dialogOpen, setDialogOpen] = useState(false); const viewerRef = useRef<DialogRef>(null);
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, { const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
errorRetryCount: 2, errorRetryCount: 2,
revalidateIfStale: false, revalidateIfStale: false,
@ -22,21 +24,17 @@ const UpdateButton = (props: Props) => {
return ( return (
<> <>
<UpdateViewer ref={viewerRef} />
<Button <Button
color="error" color="error"
variant="contained" variant="contained"
size="small" size="small"
className={className} className={className}
onClick={() => setDialogOpen(true)} onClick={() => viewerRef.current?.open()}
> >
New New
</Button> </Button>
{dialogOpen && (
<UpdateDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
)}
</> </>
); );
}; };
export default UpdateButton;

View file

@ -9,7 +9,7 @@ import { useVerge } from "@/hooks/use-verge";
/** /**
* custom theme * custom theme
*/ */
export default function useCustomTheme() { export const useCustomTheme = () => {
const { verge } = useVerge(); const { verge } = useVerge();
const { theme_mode, theme_setting } = verge ?? {}; const { theme_mode, theme_setting } = verge ?? {};
const [mode, setMode] = useRecoilState(atomThemeMode); const [mode, setMode] = useRecoilState(atomThemeMode);
@ -121,4 +121,4 @@ export default function useCustomTheme() {
}, [mode, theme_setting]); }, [mode, theme_setting]);
return { theme }; return { theme };
} };

View file

@ -1,9 +1,10 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { useRecoilValue, useSetRecoilState } from "recoil"; import { useRecoilValue, useSetRecoilState } from "recoil";
import { getClashLogs } from "@/services/cmds"; import { getClashLogs } from "@/services/cmds";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
import { atomEnableLog, atomLogData } from "@/services/states"; import { atomEnableLog, atomLogData } from "@/services/states";
import { useWebsocket } from "@/hooks/use-websocket";
const MAX_LOG_NUM = 1000; const MAX_LOG_NUM = 1000;
@ -14,7 +15,14 @@ export const useLogSetup = () => {
const enableLog = useRecoilValue(atomEnableLog); const enableLog = useRecoilValue(atomEnableLog);
const setLogData = useSetRecoilState(atomLogData); const setLogData = useSetRecoilState(atomLogData);
const [refresh, setRefresh] = useState({}); 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(() => { useEffect(() => {
if (!enableLog || !clashInfo) return; if (!enableLog || !clashInfo) return;
@ -22,21 +30,10 @@ export const useLogSetup = () => {
getClashLogs().then(setLogData); getClashLogs().then(setLogData);
const { server = "", secret = "" } = clashInfo; const { server = "", secret = "" } = clashInfo;
const ws = new WebSocket(`ws://${server}/logs?token=${secret}`); connect(`ws://${server}/logs?token=${encodeURIComponent(secret)}`);
ws.addEventListener("message", (event) => { return () => {
const data = JSON.parse(event.data) as ILogItem; disconnect();
const time = dayjs().format("MM-DD HH:mm:ss"); };
setLogData((l) => { }, [clashInfo, enableLog]);
if (l.length >= MAX_LOG_NUM) l.shift();
return [...l, { ...data, time }];
});
});
ws.addEventListener("error", () => {
setTimeout(() => setRefresh({}), 1000);
});
return () => ws?.close();
}, [clashInfo, enableLog, refresh]);
}; };

View file

@ -1,11 +1,12 @@
import { styled, Box } from "@mui/material"; import { styled, Box } from "@mui/material";
const Item = styled(Box)(({ theme: { palette } }) => ({ const Item = styled(Box)(({ theme: { palette, typography } }) => ({
padding: "8px 0", padding: "8px 0",
margin: "0 12px", margin: "0 12px",
lineHeight: 1.35, lineHeight: 1.35,
borderBottom: `1px solid ${palette.divider}`, borderBottom: `1px solid ${palette.divider}`,
fontSize: "0.875rem", fontSize: "0.875rem",
fontFamily: typography.fontFamily,
userSelect: "text", userSelect: "text",
"& .time": { "& .time": {
color: palette.text.secondary, color: palette.text.secondary,

View file

@ -27,6 +27,7 @@ export const ProfileBox = styled(Box)(
}[key]!; }[key]!;
return { return {
position: "relative",
width: "100%", width: "100%",
display: "block", display: "block",
cursor: "pointer", cursor: "pointer",

View file

@ -12,6 +12,7 @@ import {
keyframes, keyframes,
MenuItem, MenuItem,
Menu, Menu,
CircularProgress,
} from "@mui/material"; } from "@mui/material";
import { RefreshRounded } from "@mui/icons-material"; import { RefreshRounded } from "@mui/icons-material";
import { atomLoadingCache } from "@/services/states"; import { atomLoadingCache } from "@/services/states";
@ -28,13 +29,14 @@ const round = keyframes`
interface Props { interface Props {
selected: boolean; selected: boolean;
activating: boolean;
itemData: IProfileItem; itemData: IProfileItem;
onSelect: (force: boolean) => void; onSelect: (force: boolean) => void;
onEdit: () => void; onEdit: () => void;
} }
export const ProfileItem = (props: Props) => { export const ProfileItem = (props: Props) => {
const { selected, itemData, onSelect, onEdit } = props; const { selected, activating, itemData, onSelect, onEdit } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<any>(null);
@ -192,6 +194,25 @@ export const ProfileItem = (props: Props) => {
event.preventDefault(); 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"> <Box position="relative">
<Typography <Typography
width="calc(100% - 36px)" width="calc(100% - 36px)"

View file

@ -115,7 +115,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
fileDataRef.current = null; fileDataRef.current = null;
props.onChange(); props.onChange();
} catch (err: any) { } catch (err: any) {
Notice.error(err.message); Notice.error(err.message || err.toString());
} }
}) })
); );
@ -143,7 +143,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
<BaseDialog <BaseDialog
open={open} open={open}
title={openType === "new" ? t("Create Profile") : t("Edit Profile")} title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
contentSx={{ width: 375, pb: 0, maxHeight: 320 }} contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
okBtn={t("Save")} okBtn={t("Save")}
cancelBtn={t("Cancel")} cancelBtn={t("Cancel")}
onClose={handleClose} onClose={handleClose}
@ -188,7 +188,12 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
name="url" name="url"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<TextField {...text} {...field} label={t("Subscription URL")} /> <TextField
{...text}
{...field}
multiline
label={t("Subscription URL")}
/>
)} )}
/> />

View file

@ -0,0 +1,86 @@
import dayjs from "dayjs";
import useSWR, { mutate } from "swr";
import { useState } from "react";
import {
Button,
IconButton,
List,
ListItem,
ListItemText,
} from "@mui/material";
import { RefreshRounded } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { getProviders, providerUpdate } from "@/services/api";
import { BaseDialog } from "../base";
export const ProviderButton = () => {
const { t } = useTranslation();
const { data } = useSWR("getProviders", getProviders);
const [open, setOpen] = useState(false);
const hasProvider = Object.keys(data || {}).length > 0;
const handleUpdate = useLockFn(async (key: string) => {
await providerUpdate(key);
await mutate("getProxies");
await mutate("getProviders");
});
if (!hasProvider) return null;
return (
<>
<Button
size="small"
variant="outlined"
sx={{ textTransform: "capitalize" }}
onClick={() => setOpen(true)}
>
{t("Provider")}
</Button>
<BaseDialog
open={open}
title={t("Proxy Provider")}
contentSx={{ width: 400 }}
disableOk
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(data || {}).map(([key, item]) => {
const time = dayjs(item.updatedAt);
return (
<ListItem sx={{ p: 0 }} key={key}>
<ListItemText
primary={key}
secondary={
<>
<span style={{ marginRight: "4em" }}>
Type: {item.vehicleType}
</span>
<span title={time.format("YYYY-MM-DD HH:mm:ss")}>
Updated: {time.fromNow()}
</span>
</>
}
/>
<IconButton
size="small"
color="inherit"
title="Update Provider"
onClick={() => handleUpdate(key)}
>
<RefreshRounded />
</IconButton>
</ListItem>
);
})}
</List>
</BaseDialog>
</>
);
};

View file

@ -137,20 +137,14 @@ export const ProxyItemMini = (props: Props) => {
e.stopPropagation(); e.stopPropagation();
onDelay(); onDelay();
}} }}
color={ color={delayManager.formatDelayColor(delay)}
delay > 500
? "error.main"
: delay < 100
? "success.main"
: "text.secondary"
}
sx={({ palette }) => sx={({ palette }) =>
!proxy.provider !proxy.provider
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } } ? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
: {} : {}
} }
> >
{delay > 1e5 ? "Error" : delay > 3000 ? "Timeout" : `${delay}`} {delayManager.formatDelay(delay)}
</Widget> </Widget>
)} )}
@ -166,19 +160,21 @@ export const ProxyItemMini = (props: Props) => {
); );
}; };
const Widget = styled(Box)(() => ({ const Widget = styled(Box)(({ theme: { typography } }) => ({
padding: "3px 6px", padding: "3px 6px",
fontSize: 14, fontSize: 14,
fontFamily: typography.fontFamily,
borderRadius: "4px", borderRadius: "4px",
})); }));
const TypeBox = styled(Box)(({ theme }) => ({ const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({
display: "inline-block", display: "inline-block",
border: "1px solid #ccc", border: "1px solid #ccc",
borderColor: alpha(theme.palette.text.secondary, 0.36), borderColor: alpha(palette.text.secondary, 0.36),
color: alpha(theme.palette.text.secondary, 0.42), color: alpha(palette.text.secondary, 0.42),
borderRadius: 4, borderRadius: 4,
fontSize: 10, fontSize: 10,
fontFamily: typography.fontFamily,
marginRight: "4px", marginRight: "4px",
padding: "0 2px", padding: "0 2px",
lineHeight: 1.25, lineHeight: 1.25,

View file

@ -145,20 +145,14 @@ export const ProxyItem = (props: Props) => {
e.stopPropagation(); e.stopPropagation();
onDelay(); onDelay();
}} }}
color={ color={delayManager.formatDelayColor(delay)}
delay > 500
? "error.main"
: delay < 100
? "success.main"
: "text.secondary"
}
sx={({ palette }) => sx={({ palette }) =>
!proxy.provider !proxy.provider
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } } ? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
: {} : {}
} }
> >
{delay > 1e5 ? "Error" : delay > 3000 ? "Timeout" : `${delay}ms`} {delayManager.formatDelay(delay)}
</Widget> </Widget>
)} )}

View file

@ -40,7 +40,7 @@ export const ProxyRender = (props: RenderProps) => {
<ListItemText <ListItemText
primary={group.name} primary={group.name}
secondary={ secondary={
<Box <ListItemTextChild
sx={{ sx={{
overflow: "hidden", overflow: "hidden",
display: "flex", display: "flex",
@ -50,7 +50,7 @@ export const ProxyRender = (props: RenderProps) => {
> >
<StyledTypeBox>{group.type}</StyledTypeBox> <StyledTypeBox>{group.type}</StyledTypeBox>
<StyledSubtitle>{group.now}</StyledSubtitle> <StyledSubtitle>{group.now}</StyledSubtitle>
</Box> </ListItemTextChild>
} }
secondaryTypographyProps={{ secondaryTypographyProps={{
sx: { display: "flex", alignItems: "center" }, sx: { display: "flex", alignItems: "center" },
@ -142,7 +142,11 @@ const StyledSubtitle = styled("span")`
white-space: nowrap; white-space: nowrap;
`; `;
const StyledTypeBox = styled(Box)(({ theme }) => ({ const ListItemTextChild = styled("span")`
display: block;
`;
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
display: "inline-block", display: "inline-block",
border: "1px solid #ccc", border: "1px solid #ccc",
borderColor: alpha(theme.palette.primary.main, 0.5), borderColor: alpha(theme.palette.primary.main, 0.5),

View file

@ -1,7 +1,6 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useRecoilValue } from "recoil";
import { atomCurrentProfile } from "@/services/states";
import { ProxySortType } from "./use-filter-sort"; import { ProxySortType } from "./use-filter-sort";
import { useProfiles } from "@/hooks/use-profiles";
export interface HeadState { export interface HeadState {
open?: boolean; open?: boolean;
@ -25,7 +24,8 @@ export const DEFAULT_STATE: HeadState = {
}; };
export function useHeadStateNew() { export function useHeadStateNew() {
const current = useRecoilValue(atomCurrentProfile); const { profiles } = useProfiles();
const current = profiles?.current || "";
const [state, setState] = useState<Record<string, HeadState>>({}); const [state, setState] = useState<Record<string, HeadState>>({});

View file

@ -0,0 +1,132 @@
import { mutate } from "swr";
import { forwardRef, useImperativeHandle, useState } from "react";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { useLockFn } from "ahooks";
import { Lock } from "@mui/icons-material";
import {
Box,
Button,
IconButton,
List,
ListItemButton,
ListItemText,
} from "@mui/material";
import { changeClashCore, restartSidecar } from "@/services/cmds";
import { closeAllConnections } from "@/services/api";
import { grantPermission } from "@/services/cmds";
import getSystem from "@/utils/get-system";
const VALID_CORE = [
{ name: "Clash", core: "clash" },
{ name: "Clash Meta", core: "clash-meta" },
];
const OS = getSystem();
export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { verge, mutateVerge } = useVerge();
const [open, setOpen] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}));
const { clash_core = "clash" } = verge ?? {};
const onCoreChange = useLockFn(async (core: string) => {
if (core === clash_core) return;
try {
closeAllConnections();
await changeClashCore(core);
mutateVerge();
setTimeout(() => {
mutate("getClashConfig");
mutate("getVersion");
}, 100);
Notice.success(`Successfully switch to ${core}`, 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const onGrant = useLockFn(async (core: string) => {
try {
await grantPermission(core);
// 自动重启
if (core === clash_core) await restartSidecar();
Notice.success(`Successfully grant permission to ${core}`, 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const onRestart = useLockFn(async () => {
try {
await restartSidecar();
Notice.success(`Successfully restart core`, 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
return (
<BaseDialog
open={open}
title={
<Box display="flex" justifyContent="space-between">
{t("Clash Core")}
<Button variant="contained" size="small" onClick={onRestart}>
{t("Restart")}
</Button>
</Box>
}
contentSx={{
pb: 0,
width: 320,
height: 200,
overflowY: "auto",
userSelect: "text",
marginTop: "-8px",
}}
disableOk
cancelBtn={t("Back")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<List component="nav">
{VALID_CORE.map((each) => (
<ListItemButton
key={each.core}
selected={each.core === clash_core}
onClick={() => onCoreChange(each.core)}
>
<ListItemText primary={each.name} secondary={`/${each.core}`} />
{(OS === "macos" || OS === "linux") && (
<IconButton
color="inherit"
size="small"
edge="end"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onGrant(each.core);
}}
>
<Lock fontSize="inherit" />
</IconButton>
)}
</ListItemButton>
))}
</List>
</BaseDialog>
);
});

View file

@ -64,7 +64,7 @@ export const ConfigViewer = forwardRef<DialogRef>((props, ref) => {
{t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" /> {t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" />
</> </>
} }
contentSx={{ width: 520, pb: 1 }} contentSx={{ width: 520, pb: 1, userSelect: "text" }}
cancelBtn={t("Back")} cancelBtn={t("Back")}
disableOk disableOk
onClose={() => setOpen(false)} onClose={() => setOpen(false)}

View file

@ -1,84 +0,0 @@
import { mutate } from "swr";
import { useState } from "react";
import { useLockFn } from "ahooks";
import { IconButton, Menu, MenuItem } from "@mui/material";
import { Settings } from "@mui/icons-material";
import { changeClashCore } from "@/services/cmds";
import { closeAllConnections } from "@/services/api";
import { useVerge } from "@/hooks/use-verge";
import { Notice } from "@/components/base";
const VALID_CORE = [
{ name: "Clash", core: "clash" },
{ name: "Clash Meta", core: "clash-meta" },
];
export const CoreSwitch = () => {
const { verge, mutateVerge } = useVerge();
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const { clash_core = "clash" } = verge ?? {};
const onCoreChange = useLockFn(async (core: string) => {
if (core === clash_core) return;
try {
closeAllConnections();
await changeClashCore(core);
mutateVerge();
setTimeout(() => {
mutate("getClashConfig");
mutate("getVersion");
}, 100);
setAnchorEl(null);
Notice.success(`Successfully switch to ${core}`, 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
return (
<>
<IconButton
color="inherit"
size="small"
onClick={(event) => {
const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX });
setAnchorEl(event.currentTarget);
}}
>
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
<Menu
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorPosition={position}
anchorReference="anchorPosition"
transitionDuration={225}
onContextMenu={(e) => {
setAnchorEl(null);
e.preventDefault();
}}
>
{VALID_CORE.map((each) => (
<MenuItem
key={each.core}
sx={{ minWidth: 125 }}
selected={each.core === clash_core}
onClick={() => onCoreChange(each.core)}
>
{each.name}
</MenuItem>
))}
</Menu>
</>
);
};

View file

@ -14,6 +14,7 @@ const ItemWrapper = styled("div")`
`; `;
const HOTKEY_FUNC = [ const HOTKEY_FUNC = [
"open_dashboard",
"clash_mode_rule", "clash_mode_rule",
"clash_mode_global", "clash_mode_global",
"clash_mode_direct", "clash_mode_direct",

View file

@ -0,0 +1,80 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { List, Switch } from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { SettingItem } from "./setting-comp";
import { GuardState } from "./guard-state";
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
const [open, setOpen] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}));
const onSwitchFormat = (_e: any, value: boolean) => value;
const onError = (err: any) => {
Notice.error(err.message || err.toString());
};
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
};
return (
<BaseDialog
open={open}
title={t("Layout Setting")}
contentSx={{ width: 450 }}
disableOk
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<List>
<SettingItem label={t("Theme Blur")}>
<GuardState
value={verge?.theme_blur ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ theme_blur: e })}
onGuard={(e) => patchVerge({ theme_blur: e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem label={t("Traffic Graph")}>
<GuardState
value={verge?.traffic_graph ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ traffic_graph: e })}
onGuard={(e) => patchVerge({ traffic_graph: e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem label={t("Memory Usage")}>
<GuardState
value={verge?.enable_memory_usage ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_memory_usage: e })}
onGuard={(e) => patchVerge({ enable_memory_usage: e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
</List>
</BaseDialog>
);
});

View file

@ -19,20 +19,26 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [values, setValues] = useState({ const [values, setValues] = useState({
appLogLevel: "info",
autoCloseConnection: false, autoCloseConnection: false,
enableClashFields: true,
enableBuiltinEnhanced: true, enableBuiltinEnhanced: true,
proxyLayoutColumn: 6, proxyLayoutColumn: 6,
defaultLatencyTest: "", defaultLatencyTest: "",
autoLogClean: 0,
}); });
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true); setOpen(true);
setValues({ setValues({
appLogLevel: verge?.app_log_level ?? "info",
autoCloseConnection: verge?.auto_close_connection ?? false, autoCloseConnection: verge?.auto_close_connection ?? false,
enableClashFields: verge?.enable_clash_fields ?? true,
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true, enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
proxyLayoutColumn: verge?.proxy_layout_column || 6, proxyLayoutColumn: verge?.proxy_layout_column || 6,
defaultLatencyTest: verge?.default_latency_test || "", defaultLatencyTest: verge?.default_latency_test || "",
autoLogClean: verge?.auto_log_clean || 0,
}); });
}, },
close: () => setOpen(false), close: () => setOpen(false),
@ -41,10 +47,13 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const onSave = useLockFn(async () => { const onSave = useLockFn(async () => {
try { try {
await patchVerge({ await patchVerge({
app_log_level: values.appLogLevel,
auto_close_connection: values.autoCloseConnection, auto_close_connection: values.autoCloseConnection,
enable_clash_fields: values.enableClashFields,
enable_builtin_enhanced: values.enableBuiltinEnhanced, enable_builtin_enhanced: values.enableBuiltinEnhanced,
proxy_layout_column: values.proxyLayoutColumn, proxy_layout_column: values.proxyLayoutColumn,
default_latency_test: values.defaultLatencyTest, default_latency_test: values.defaultLatencyTest,
auto_log_clean: values.autoLogClean as any,
}); });
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
@ -65,7 +74,28 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
> >
<List> <List>
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Auto Close Connections" /> <ListItemText primary={t("App Log Level")} />
<Select
size="small"
sx={{ width: 100, "> div": { py: "7.5px" } }}
value={values.appLogLevel}
onChange={(e) => {
setValues((v) => ({
...v,
appLogLevel: e.target.value as string,
}));
}}
>
{["trace", "debug", "info", "warn", "error", "silent"].map((i) => (
<MenuItem value={i} key={i}>
{i[0].toUpperCase() + i.slice(1).toLowerCase()}
</MenuItem>
))}
</Select>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Close Connections")} />
<Switch <Switch
edge="end" edge="end"
checked={values.autoCloseConnection} checked={values.autoCloseConnection}
@ -76,7 +106,18 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem> </ListItem>
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Enable Builtin Enhanced" /> <ListItemText primary={t("Enable Clash Fields Filter")} />
<Switch
edge="end"
checked={values.enableClashFields}
onChange={(_, c) =>
setValues((v) => ({ ...v, enableClashFields: c }))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Enable Builtin Enhanced")} />
<Switch <Switch
edge="end" edge="end"
checked={values.enableBuiltinEnhanced} checked={values.enableBuiltinEnhanced}
@ -87,10 +128,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem> </ListItem>
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Proxy Layout Column" /> <ListItemText primary={t("Proxy Layout Column")} />
<Select <Select
size="small" size="small"
sx={{ width: 100, "> div": { py: "7.5px" } }} sx={{ width: 135, "> div": { py: "7.5px" } }}
value={values.proxyLayoutColumn} value={values.proxyLayoutColumn}
onChange={(e) => { onChange={(e) => {
setValues((v) => ({ setValues((v) => ({
@ -111,7 +152,33 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem> </ListItem>
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Default Latency Test" /> <ListItemText primary={t("Auto Log Clean")} />
<Select
size="small"
sx={{ width: 135, "> div": { py: "7.5px" } }}
value={values.autoLogClean}
onChange={(e) => {
setValues((v) => ({
...v,
autoLogClean: e.target.value as number,
}));
}}
>
{[
{ key: "Never Clean", value: 0 },
{ key: "Retain 7 Days", value: 1 },
{ key: "Retain 30 Days", value: 2 },
{ key: "Retain 90 Days", value: 3 },
].map((i) => (
<MenuItem key={i.value} value={i.value}>
{t(i.key)}
</MenuItem>
))}
</Select>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Test")} />
<TextField <TextField
size="small" size="small"
autoComplete="off" autoComplete="off"

View file

@ -1,4 +1,4 @@
import React from "react"; import React, { ReactNode } from "react";
import { import {
Box, Box,
List, List,
@ -8,12 +8,14 @@ import {
} from "@mui/material"; } from "@mui/material";
interface ItemProps { interface ItemProps {
label: React.ReactNode; label: ReactNode;
extra?: React.ReactNode; extra?: ReactNode;
children?: ReactNode;
secondary?: ReactNode;
} }
export const SettingItem: React.FC<ItemProps> = (props) => { export const SettingItem: React.FC<ItemProps> = (props) => {
const { label, extra, children } = props; const { label, extra, children, secondary } = props;
const primary = !extra ? ( const primary = !extra ? (
label label
@ -26,13 +28,16 @@ export const SettingItem: React.FC<ItemProps> = (props) => {
return ( return (
<ListItem sx={{ pt: "5px", pb: "5px" }}> <ListItem sx={{ pt: "5px", pb: "5px" }}>
<ListItemText primary={primary} /> <ListItemText primary={primary} secondary={secondary} />
{children} {children}
</ListItem> </ListItem>
); );
}; };
export const SettingList: React.FC<{ title: string }> = (props) => ( export const SettingList: React.FC<{
title: string;
children: ReactNode;
}> = (props) => (
<List> <List>
<ListSubheader sx={{ background: "transparent" }} disableSticky> <ListSubheader sx={{ background: "transparent" }} disableSticky>
{props.title} {props.title}

View file

@ -1,43 +1,45 @@
import useSWR from "swr"; import useSWR from "swr";
import snarkdown from "snarkdown"; import snarkdown from "snarkdown";
import { useMemo } from "react"; import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
import { useLockFn } from "ahooks";
import { Box, styled } from "@mui/material";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
styled,
} from "@mui/material";
import { relaunch } from "@tauri-apps/api/process"; import { relaunch } from "@tauri-apps/api/process";
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater"; import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { atomUpdateState } from "@/services/states"; import { atomUpdateState } from "@/services/states";
import { Notice } from "@/components/base";
interface Props {
open: boolean;
onClose: () => void;
}
const UpdateLog = styled(Box)(() => ({ const UpdateLog = styled(Box)(() => ({
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" }, "h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
})); }));
const UpdateDialog = (props: Props) => { export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const { open, onClose } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, { const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
errorRetryCount: 2, errorRetryCount: 2,
revalidateIfStale: false, revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour focusThrottleInterval: 36e5, // 1 hour
}); });
const [updateState, setUpdateState] = useRecoilState(atomUpdateState); useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}));
const onUpdate = async () => { // markdown parser
const parseContent = useMemo(() => {
if (!updateInfo?.manifest?.body) {
return "New Version is available";
}
return snarkdown(updateInfo?.manifest?.body);
}, [updateInfo]);
const onUpdate = useLockFn(async () => {
if (updateState) return; if (updateState) return;
setUpdateState(true); setUpdateState(true);
@ -49,39 +51,20 @@ const UpdateDialog = (props: Props) => {
} finally { } finally {
setUpdateState(false); setUpdateState(false);
} }
}; });
// markdown parser
const parseContent = useMemo(() => {
if (!updateInfo?.manifest?.body) {
return "New Version is available";
}
return snarkdown(updateInfo?.manifest?.body);
}, [updateInfo]);
return ( return (
<Dialog open={open} onClose={onClose}> <BaseDialog
<DialogTitle>New Version v{updateInfo?.manifest?.version}</DialogTitle> open={open}
title={`New Version v${updateInfo?.manifest?.version}`}
<DialogContent sx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}> contentSx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}
<UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} /> okBtn={t("Update")}
</DialogContent> cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
<DialogActions> onCancel={() => setOpen(false)}
<Button variant="outlined" onClick={onClose}> onOk={onUpdate}
{t("Cancel")} >
</Button> <UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
<Button </BaseDialog>
autoFocus
variant="contained"
disabled={updateState}
onClick={onUpdate}
>
{t("Update")}
</Button>
</DialogActions>
</Dialog>
); );
}; });
export default UpdateDialog;

View file

@ -60,7 +60,10 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
.trim(); .trim();
url = url.replaceAll("%port", port || "9090"); url = url.replaceAll("%port", port || "9090");
url = url.replaceAll("%secret", clashInfo.secret || ""); url = url.replaceAll(
"%secret",
encodeURIComponent(clashInfo.secret || "")
);
} }
await openWebUrl(url); await openWebUrl(url);

View file

@ -8,16 +8,16 @@ import {
Typography, Typography,
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import { ArrowForward } from "@mui/icons-material"; import { ArrowForward, Settings } from "@mui/icons-material";
import { DialogRef } from "@/components/base"; import { DialogRef } from "@/components/base";
import { useClash } from "@/hooks/use-clash"; import { useClash } from "@/hooks/use-clash";
import { GuardState } from "./mods/guard-state"; import { GuardState } from "./mods/guard-state";
import { CoreSwitch } from "./mods/core-switch";
import { WebUIViewer } from "./mods/web-ui-viewer"; import { WebUIViewer } from "./mods/web-ui-viewer";
import { ClashFieldViewer } from "./mods/clash-field-viewer"; import { ClashFieldViewer } from "./mods/clash-field-viewer";
import { ClashPortViewer } from "./mods/clash-port-viewer"; import { ClashPortViewer } from "./mods/clash-port-viewer";
import { ControllerViewer } from "./mods/controller-viewer"; import { ControllerViewer } from "./mods/controller-viewer";
import { SettingList, SettingItem } from "./mods/setting-comp"; import { SettingList, SettingItem } from "./mods/setting-comp";
import { ClashCoreViewer } from "./mods/clash-core-viewer";
interface Props { interface Props {
onError: (err: Error) => void; onError: (err: Error) => void;
@ -39,6 +39,7 @@ const SettingClash = ({ onError }: Props) => {
const fieldRef = useRef<DialogRef>(null); const fieldRef = useRef<DialogRef>(null);
const portRef = useRef<DialogRef>(null); const portRef = useRef<DialogRef>(null);
const ctrlRef = useRef<DialogRef>(null); const ctrlRef = useRef<DialogRef>(null);
const coreRef = useRef<DialogRef>(null);
const onSwitchFormat = (_e: any, value: boolean) => value; const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IConfigData>) => { const onChangeData = (patch: Partial<IConfigData>) => {
@ -51,6 +52,7 @@ const SettingClash = ({ onError }: Props) => {
<ClashFieldViewer ref={fieldRef} /> <ClashFieldViewer ref={fieldRef} />
<ClashPortViewer ref={portRef} /> <ClashPortViewer ref={portRef} />
<ControllerViewer ref={ctrlRef} /> <ControllerViewer ref={ctrlRef} />
<ClashCoreViewer ref={coreRef} />
<SettingItem label={t("Allow Lan")}> <SettingItem label={t("Allow Lan")}>
<GuardState <GuardState
@ -143,7 +145,21 @@ const SettingClash = ({ onError }: Props) => {
</IconButton> </IconButton>
</SettingItem> </SettingItem>
<SettingItem label={t("Clash Core")} extra={<CoreSwitch />}> <SettingItem
label={t("Clash Core")}
extra={
<IconButton
color="inherit"
size="small"
onClick={() => coreRef.current?.open()}
>
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
}
>
<Typography sx={{ py: "7px", pr: 1 }}>{version}</Typography> <Typography sx={{ py: "7px", pr: 1 }}>{version}</Typography>
</SettingItem> </SettingItem>
</SettingList> </SettingList>

View file

@ -1,17 +1,13 @@
import { useRef } from "react"; import { useRef } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { IconButton, MenuItem, Select, Typography } from "@mui/material";
IconButton,
MenuItem,
Select,
Switch,
Typography,
} from "@mui/material";
import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds"; import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
import { ArrowForward } from "@mui/icons-material"; import { ArrowForward } from "@mui/icons-material";
import { checkUpdate } from "@tauri-apps/api/updater";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { version } from "@root/package.json"; import { version } from "@root/package.json";
import { DialogRef } from "@/components/base"; import { DialogRef, Notice } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp"; import { SettingList, SettingItem } from "./mods/setting-comp";
import { ThemeModeSwitch } from "./mods/theme-mode-switch"; import { ThemeModeSwitch } from "./mods/theme-mode-switch";
import { ConfigViewer } from "./mods/config-viewer"; import { ConfigViewer } from "./mods/config-viewer";
@ -19,34 +15,54 @@ import { HotkeyViewer } from "./mods/hotkey-viewer";
import { MiscViewer } from "./mods/misc-viewer"; import { MiscViewer } from "./mods/misc-viewer";
import { ThemeViewer } from "./mods/theme-viewer"; import { ThemeViewer } from "./mods/theme-viewer";
import { GuardState } from "./mods/guard-state"; import { GuardState } from "./mods/guard-state";
import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer";
import getSystem from "@/utils/get-system";
interface Props { interface Props {
onError?: (err: Error) => void; onError?: (err: Error) => void;
} }
const OS = getSystem();
const SettingVerge = ({ onError }: Props) => { const SettingVerge = ({ onError }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge(); const { verge, patchVerge, mutateVerge } = useVerge();
const { theme_mode, language } = verge ?? {};
const { theme_mode, theme_blur, traffic_graph, language } = verge ?? {};
const configRef = useRef<DialogRef>(null); const configRef = useRef<DialogRef>(null);
const hotkeyRef = useRef<DialogRef>(null); const hotkeyRef = useRef<DialogRef>(null);
const miscRef = useRef<DialogRef>(null); const miscRef = useRef<DialogRef>(null);
const themeRef = useRef<DialogRef>(null); const themeRef = useRef<DialogRef>(null);
const layoutRef = useRef<DialogRef>(null);
const updateRef = useRef<DialogRef>(null);
const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IVergeConfig>) => { const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false); mutateVerge({ ...verge, ...patch }, false);
}; };
const onCheckUpdate = useLockFn(async () => {
try {
const info = await checkUpdate();
if (!info?.shouldUpdate) {
Notice.success("No Updates Available");
} else {
updateRef.current?.open();
}
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
return ( return (
<SettingList title={t("Verge Setting")}> <SettingList title={t("Verge Setting")}>
<ThemeViewer ref={themeRef} /> <ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} /> <ConfigViewer ref={configRef} />
<HotkeyViewer ref={hotkeyRef} /> <HotkeyViewer ref={hotkeyRef} />
<MiscViewer ref={miscRef} /> <MiscViewer ref={miscRef} />
<LayoutViewer ref={layoutRef} />
<UpdateViewer ref={updateRef} />
<SettingItem label={t("Language")}> <SettingItem label={t("Language")}>
<GuardState <GuardState
@ -59,6 +75,7 @@ const SettingVerge = ({ onError }: Props) => {
<Select size="small" sx={{ width: 100, "> div": { py: "7.5px" } }}> <Select size="small" sx={{ width: 100, "> div": { py: "7.5px" } }}>
<MenuItem value="zh"></MenuItem> <MenuItem value="zh"></MenuItem>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">English</MenuItem>
<MenuItem value="ru">Русский</MenuItem>
</Select> </Select>
</GuardState> </GuardState>
</SettingItem> </SettingItem>
@ -74,30 +91,26 @@ const SettingVerge = ({ onError }: Props) => {
</GuardState> </GuardState>
</SettingItem> </SettingItem>
<SettingItem label={t("Theme Blur")}> <SettingItem label={t("Theme Setting")}>
<GuardState <IconButton
value={theme_blur ?? false} color="inherit"
valueProps="checked" size="small"
onCatch={onError} sx={{ my: "2px" }}
onFormat={onSwitchFormat} onClick={() => themeRef.current?.open()}
onChange={(e) => onChangeData({ theme_blur: e })}
onGuard={(e) => patchVerge({ theme_blur: e })}
> >
<Switch edge="end" /> <ArrowForward />
</GuardState> </IconButton>
</SettingItem> </SettingItem>
<SettingItem label={t("Traffic Graph")}> <SettingItem label={t("Layout Setting")}>
<GuardState <IconButton
value={traffic_graph ?? true} color="inherit"
valueProps="checked" size="small"
onCatch={onError} sx={{ my: "2px" }}
onFormat={onSwitchFormat} onClick={() => layoutRef.current?.open()}
onChange={(e) => onChangeData({ traffic_graph: e })}
onGuard={(e) => patchVerge({ traffic_graph: e })}
> >
<Switch edge="end" /> <ArrowForward />
</GuardState> </IconButton>
</SettingItem> </SettingItem>
<SettingItem label={t("Miscellaneous")}> <SettingItem label={t("Miscellaneous")}>
@ -111,17 +124,6 @@ const SettingVerge = ({ onError }: Props) => {
</IconButton> </IconButton>
</SettingItem> </SettingItem>
<SettingItem label={t("Theme Setting")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => themeRef.current?.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Hotkey Setting")}> <SettingItem label={t("Hotkey Setting")}>
<IconButton <IconButton
color="inherit" color="inherit"
@ -177,6 +179,19 @@ const SettingVerge = ({ onError }: Props) => {
</IconButton> </IconButton>
</SettingItem> </SettingItem>
{!(OS === "windows" && WIN_PORTABLE) && (
<SettingItem label={t("Check for Updates")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={onCheckUpdate}
>
<ArrowForward />
</IconButton>
</SettingItem>
)}
<SettingItem label={t("Verge Version")}> <SettingItem label={t("Verge Version")}>
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography> <Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
</SettingItem> </SettingItem>

View file

@ -1,9 +1,10 @@
import useSWR from "swr"; import useSWR, { mutate } from "swr";
import { import {
getProfiles, getProfiles,
patchProfile, patchProfile,
patchProfilesConfig, patchProfilesConfig,
} from "@/services/cmds"; } from "@/services/cmds";
import { getProxies, updateProxy } from "@/services/api";
export const useProfiles = () => { export const useProfiles = () => {
const { data: profiles, mutate: mutateProfiles } = useSWR( const { data: profiles, mutate: mutateProfiles } = useSWR(
@ -23,9 +24,49 @@ export const useProfiles = () => {
} }
}; };
// 根据selected的节点选择
const activateSelected = async () => {
const proxiesData = await getProxies();
const profileData = await getProfiles();
if (!profileData || !proxiesData) return;
const current = profileData.items?.find(
(e) => e && e.uid === profileData.current
);
if (!current) return;
// init selected array
const { selected = [] } = current;
const selectedMap = Object.fromEntries(
selected.map((each) => [each.name!, each.now!])
);
let hasChange = false;
const newSelected: typeof selected = [];
const { global, groups } = proxiesData;
[global, ...groups].forEach(({ type, name, now }) => {
if (!now || type !== "Selector") return;
if (selectedMap[name] != null && selectedMap[name] !== now) {
hasChange = true;
updateProxy(name, selectedMap[name]);
}
newSelected.push({ name, now: selectedMap[name] });
});
if (hasChange) {
patchProfile(profileData.current!, { selected: newSelected });
mutate("getProxies", getProxies());
}
};
return { return {
profiles, profiles,
current: profiles?.items?.find((p) => p.uid === profiles.current), current: profiles?.items?.find((p) => p && p.uid === profiles.current),
activateSelected,
patchProfiles, patchProfiles,
patchCurrent, patchCurrent,
mutateProfiles, mutateProfiles,

View file

@ -0,0 +1,27 @@
import { useEffect, useState } from "react";
export const useVisibility = () => {
const [visible, setVisible] = useState(true);
useEffect(() => {
const handleVisibilityChange = () => {
setVisible(document.visibilityState === "visible");
};
const handleFocus = () => setVisible(true);
const handleClick = () => setVisible(true);
handleVisibilityChange();
document.addEventListener("focus", handleFocus);
document.addEventListener("pointerdown", handleClick);
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
document.removeEventListener("focus", handleFocus);
document.removeEventListener("pointerdown", handleClick);
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, []);
return visible;
};

View file

@ -0,0 +1,53 @@
import { useRef } from "react";
export type WsMsgFn = (event: MessageEvent<any>) => void;
export interface WsOptions {
errorCount?: number; // default is 5
retryInterval?: number; // default is 2500
onError?: () => void;
}
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
const wsRef = useRef<WebSocket | null>(null);
const timerRef = useRef<any>(null);
const disconnect = () => {
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
const connect = (url: string) => {
let errorCount = options?.errorCount ?? 5;
if (!url) return;
const connectHelper = () => {
disconnect();
const ws = new WebSocket(url);
wsRef.current = ws;
ws.addEventListener("message", onMessage);
ws.addEventListener("error", () => {
errorCount -= 1;
if (errorCount >= 0) {
timerRef.current = setTimeout(connectHelper, 2500);
} else {
disconnect();
options?.onError?.();
}
});
};
connectHelper();
};
return { connect, disconnect };
};

View file

@ -15,6 +15,7 @@
"global": "global", "global": "global",
"direct": "direct", "direct": "direct",
"script": "script", "script": "script",
"Profiles": "Profiles", "Profiles": "Profiles",
"Profile URL": "Profile URL", "Profile URL": "Profile URL",
"Import": "Import", "Import": "Import",
@ -34,6 +35,9 @@
"Refresh": "Refresh", "Refresh": "Refresh",
"To Top": "To Top", "To Top": "To Top",
"To End": "To End", "To End": "To End",
"Update All Profiles": "Update All Profiles",
"View Runtime Config": "View Runtime Config",
"Reactivate Profiles": "Reactivate Profiles",
"Location": "Location", "Location": "Location",
"Delay check": "Delay check", "Delay check": "Delay check",
@ -51,6 +55,8 @@
"Descriptions": "Descriptions", "Descriptions": "Descriptions",
"Subscription URL": "Subscription URL", "Subscription URL": "Subscription URL",
"Update Interval": "Update Interval", "Update Interval": "Update Interval",
"Use System Proxy": "Use System Proxy",
"Use Clash Proxy": "Use Clash Proxy",
"Settings": "Settings", "Settings": "Settings",
"Clash Setting": "Clash Setting", "Clash Setting": "Clash Setting",
@ -60,6 +66,7 @@
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "Log Level", "Log Level": "Log Level",
"Mixed Port": "Mixed Port", "Mixed Port": "Mixed Port",
"External": "External",
"Clash Core": "Clash Core", "Clash Core": "Clash Core",
"Tun Mode": "Tun Mode", "Tun Mode": "Tun Mode",
"Service Mode": "Service Mode", "Service Mode": "Service Mode",
@ -74,12 +81,16 @@
"Theme Mode": "Theme Mode", "Theme Mode": "Theme Mode",
"Theme Blur": "Theme Blur", "Theme Blur": "Theme Blur",
"Theme Setting": "Theme Setting", "Theme Setting": "Theme Setting",
"Layout Setting": "Layout Setting",
"Miscellaneous": "Miscellaneous",
"Hotkey Setting": "Hotkey Setting", "Hotkey Setting": "Hotkey Setting",
"Traffic Graph": "Traffic Graph", "Traffic Graph": "Traffic Graph",
"Memory Usage": "Memory Usage",
"Language": "Language", "Language": "Language",
"Open App Dir": "Open App Dir", "Open App Dir": "Open App Dir",
"Open Core Dir": "Open Core Dir", "Open Core Dir": "Open Core Dir",
"Open Logs Dir": "Open Logs Dir", "Open Logs Dir": "Open Logs Dir",
"Check for Updates": "Check for Updates",
"Verge Version": "Verge Version", "Verge Version": "Verge Version",
"theme.light": "Light", "theme.light": "Light",
"theme.dark": "Dark", "theme.dark": "Dark",
@ -87,11 +98,17 @@
"Clash Field": "Clash Field", "Clash Field": "Clash Field",
"Runtime Config": "Runtime Config", "Runtime Config": "Runtime Config",
"ReadOnly": "ReadOnly", "ReadOnly": "ReadOnly",
"Restart": "Restart",
"Back": "Back", "Back": "Back",
"Save": "Save", "Save": "Save",
"Cancel": "Cancel", "Cancel": "Cancel",
"Default": "Default",
"Download Speed": "Download Speed",
"Upload Speed": "Upload Speed",
"open_dashboard": "Open Dashboard",
"clash_mode_rule": "Rule Mode", "clash_mode_rule": "Rule Mode",
"clash_mode_global": "Global Mode", "clash_mode_global": "Global Mode",
"clash_mode_direct": "Direct Mode", "clash_mode_direct": "Direct Mode",
@ -101,5 +118,18 @@
"disable_system_proxy": "Disable System Proxy", "disable_system_proxy": "Disable System Proxy",
"toggle_tun_mode": "Toggle Tun Mode", "toggle_tun_mode": "Toggle Tun Mode",
"enable_tun_mode": "Enable Tun Mode", "enable_tun_mode": "Enable Tun Mode",
"disable_tun_mode": "Disable Tun Mode" "disable_tun_mode": "Disable Tun Mode",
"App Log Level": "App Log Level",
"Auto Close Connections": "Auto Close Connections",
"Enable Clash Fields Filter": "Enable Clash Fields Filter",
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
"Proxy Layout Column": "Proxy Layout Column",
"Default Latency Test": "Default Latency Test",
"Auto Log Clean": "Auto Log Clean",
"Never Clean": "Never Clean",
"Retain 7 Days": "Retain 7 Days",
"Retain 30 Days": "Retain 30 Days",
"Retain 90 Days": "Retain 90 Days"
} }

111
src/locales/ru.json Normal file
View file

@ -0,0 +1,111 @@
{
"Label-Proxies": "Прокси",
"Label-Profiles": "Профили",
"Label-Connections": "Соединения",
"Label-Logs": "Логи",
"Label-Rules": "Правила",
"Label-Settings": "Настройки",
"Connections": "Соединения",
"Logs": "Логи",
"Clear": "Очистить",
"Proxies": "Прокси",
"Proxy Groups": "Группы прокси",
"rule": "правила",
"global": "глобальный",
"direct": "прямой",
"script": "скриптовый",
"Profiles": "Профили",
"Profile URL": "URL профиля",
"Import": "Импорт",
"New": "Новый",
"Create Profile": "Создать профиль",
"Choose File": "Выбрать файл",
"Close All": "Закрыть всё",
"Select": "Выбрать",
"Edit Info": "Изменить информацию",
"Edit File": "Изменить файл",
"Open File": "Открыть файл",
"Update": "Обновить",
"Update(Proxy)": "Обновить (прокси)",
"Delete": "Удалить",
"Enable": "Включить",
"Disable": "Отключить",
"Refresh": "Обновить",
"To Top": "Наверх",
"To End": "Вниз",
"Update All Profiles": "Обновить все профили",
"View Runtime Config": "Просмотреть используемый конфиг",
"Reactivate Profiles": "Реактивировать профили",
"Location": "Местоположение",
"Delay check": "Проверка задержки",
"Sort by default": "Сортировать по умолчанию",
"Sort by delay": "Сортировать по задержке",
"Sort by name": "Сортировать по названию",
"Delay check URL": "URL проверки задержки",
"Proxy detail": "Подробности о прокси",
"Filter": "Фильтр",
"Filter conditions": "Условия фильтрации",
"Refresh profiles": "Обновить профили",
"Type": "Тип",
"Name": "Название",
"Descriptions": "Описания",
"Subscription URL": "URL подписки",
"Update Interval": "Интервал обновления",
"Settings": "Настройки",
"Clash Setting": "Настройки Clash",
"System Setting": "Настройки системы",
"Verge Setting": "Настройки Verge",
"Allow Lan": "Разрешить локальную сеть",
"IPv6": "IPv6",
"Log Level": "Уровень логов",
"Mixed Port": "Смешанный порт",
"Clash Core": "Ядро Clash",
"Tun Mode": "Режим туннеля",
"Service Mode": "Режим сервиса",
"Auto Launch": "Автозапуск",
"Silent Start": "Тихий запуск",
"System Proxy": "Системный прокси",
"System Proxy Setting": "Настройка системного прокси",
"Proxy Guard": "Защита прокси",
"Guard Duration": "Период защиты",
"Proxy Bypass": "Игнорирование прокси",
"Current System Proxy": "Текущий системный прокси",
"Theme Mode": "Режим темы",
"Theme Blur": "Размытие темы",
"Theme Setting": "Настройка темы",
"Hotkey Setting": "Настройка клавиатурных сокращений",
"Traffic Graph": "График трафика",
"Language": "Язык",
"Open App Dir": "Открыть папку приложения",
"Open Core Dir": "Открыть папку ядра",
"Open Logs Dir": "Открыть папку логов",
"Verge Version": "Версия Verge",
"theme.light": "Светлая",
"theme.dark": "Тёмная",
"theme.system": "Системная",
"Clash Field": "Используемые настройки Clash",
"Runtime Config": "Используемый конфиг",
"ReadOnly": "Только для чтения",
"Restart": "Перезапуск",
"Back": "Назад",
"Save": "Сохранить",
"Cancel": "Отмена",
"open_dashboard": "Open Dashboard",
"clash_mode_rule": "Режим правил",
"clash_mode_global": "Глобальный режим",
"clash_mode_direct": "Прямой режим",
"clash_mode_script": "Скриптовый режим",
"toggle_system_proxy": "Переключить режим системного прокси",
"enable_system_proxy": "Включить системный прокси",
"disable_system_proxy": "Отключить системный прокси",
"toggle_tun_mode": "Переключить режим туннеля",
"enable_tun_mode": "Включить режим туннеля",
"disable_tun_mode": "Отключить режим туннеля"
}

View file

@ -15,6 +15,7 @@
"global": "全局", "global": "全局",
"direct": "直连", "direct": "直连",
"script": "脚本", "script": "脚本",
"Profiles": "配置", "Profiles": "配置",
"Profile URL": "配置文件链接", "Profile URL": "配置文件链接",
"Import": "导入", "Import": "导入",
@ -34,6 +35,9 @@
"Refresh": "刷新", "Refresh": "刷新",
"To Top": "移到最前", "To Top": "移到最前",
"To End": "移到末尾", "To End": "移到末尾",
"Update All Profiles": "更新所有配置",
"View Runtime Config": "查看运行时配置",
"Reactivate Profiles": "重新激活配置",
"Location": "当前节点", "Location": "当前节点",
"Delay check": "延迟测试", "Delay check": "延迟测试",
@ -76,14 +80,17 @@
"Current System Proxy": "当前系统代理", "Current System Proxy": "当前系统代理",
"Theme Mode": "主题模式", "Theme Mode": "主题模式",
"Theme Blur": "背景模糊", "Theme Blur": "背景模糊",
"Miscellaneous": "杂项设置",
"Theme Setting": "主题设置", "Theme Setting": "主题设置",
"Layout Setting": "界面设置",
"Miscellaneous": "杂项设置",
"Hotkey Setting": "热键设置", "Hotkey Setting": "热键设置",
"Traffic Graph": "流量图显", "Traffic Graph": "流量图显",
"Memory Usage": "内存使用",
"Language": "语言设置", "Language": "语言设置",
"Open App Dir": "应用目录", "Open App Dir": "应用目录",
"Open Core Dir": "内核目录", "Open Core Dir": "内核目录",
"Open Logs Dir": "日志目录", "Open Logs Dir": "日志目录",
"Check for Updates": "检查更新",
"Verge Version": "应用版本", "Verge Version": "应用版本",
"theme.light": "浅色", "theme.light": "浅色",
"theme.dark": "深色", "theme.dark": "深色",
@ -91,6 +98,7 @@
"Clash Field": "Clash 字段", "Clash Field": "Clash 字段",
"Runtime Config": "运行配置", "Runtime Config": "运行配置",
"ReadOnly": "只读", "ReadOnly": "只读",
"Restart": "重启内核",
"Back": "返回", "Back": "返回",
"Save": "保存", "Save": "保存",
@ -100,6 +108,7 @@
"Download Speed": "下载速度", "Download Speed": "下载速度",
"Upload Speed": "上传速度", "Upload Speed": "上传速度",
"open_dashboard": "打开面板",
"clash_mode_rule": "规则模式", "clash_mode_rule": "规则模式",
"clash_mode_global": "全局模式", "clash_mode_global": "全局模式",
"clash_mode_direct": "直连模式", "clash_mode_direct": "直连模式",
@ -109,5 +118,18 @@
"disable_system_proxy": "关闭系统代理", "disable_system_proxy": "关闭系统代理",
"toggle_tun_mode": "切换Tun模式", "toggle_tun_mode": "切换Tun模式",
"enable_tun_mode": "开启Tun模式", "enable_tun_mode": "开启Tun模式",
"disable_tun_mode": "关闭Tun模式" "disable_tun_mode": "关闭Tun模式",
"App Log Level": "App日志等级",
"Auto Close Connections": "自动关闭连接",
"Enable Clash Fields Filter": "开启Clash字段过滤",
"Enable Builtin Enhanced": "开启内建增强功能",
"Proxy Layout Column": "代理页布局列数",
"Default Latency Test": "默认测试链接",
"Auto Log Clean": "自动清理日志",
"Never Clean": "不清理",
"Retain 7 Days": "保留7天",
"Retain 30 Days": "保留30天",
"Retain 90 Days": "保留90天"
} }

View file

@ -8,19 +8,30 @@ if (!window.ResizeObserver) {
} }
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client";
import { RecoilRoot } from "recoil"; import { RecoilRoot } from "recoil";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { BaseErrorBoundary } from "./components/base";
import Layout from "./pages/_layout"; import Layout from "./pages/_layout";
import "./services/i18n"; import "./services/i18n";
ReactDOM.render( const mainElementId = "root";
const container = document.getElementById(mainElementId);
if (!container) {
throw new Error(
`No container '${mainElementId}' found to render application`
);
}
createRoot(container).render(
<React.StrictMode> <React.StrictMode>
<RecoilRoot> <RecoilRoot>
<BrowserRouter> <BaseErrorBoundary>
<Layout /> <BrowserRouter>
</BrowserRouter> <Layout />
</BrowserRouter>
</BaseErrorBoundary>
</RecoilRoot> </RecoilRoot>
</React.StrictMode>, </React.StrictMode>
document.getElementById("root")
); );

View file

@ -3,7 +3,6 @@ import i18next from "i18next";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { SWRConfig, mutate } from "swr"; import { SWRConfig, mutate } from "swr";
import { useEffect } from "react"; import { useEffect } from "react";
import { useSetRecoilState } from "recoil";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import { alpha, List, Paper, ThemeProvider } from "@mui/material"; import { alpha, List, Paper, ThemeProvider } from "@mui/material";
@ -11,17 +10,16 @@ import { listen } from "@tauri-apps/api/event";
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from "@tauri-apps/api/window";
import { routers } from "./_routers"; import { routers } from "./_routers";
import { getAxios } from "@/services/api"; import { getAxios } from "@/services/api";
import { atomCurrentProfile } from "@/services/states";
import { getProfiles } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { ReactComponent as LogoSvg } from "@/assets/image/logo.svg"; import { ReactComponent as LogoSvg } from "@/assets/image/logo.svg";
import { BaseErrorBoundary, Notice } from "@/components/base"; import { BaseErrorBoundary, Notice } from "@/components/base";
import LayoutItem from "@/components/layout/layout-item"; import { LayoutItem } from "@/components/layout/layout-item";
import LayoutControl from "@/components/layout/layout-control"; import { LayoutControl } from "@/components/layout/layout-control";
import LayoutTraffic from "@/components/layout/layout-traffic"; import { LayoutTraffic } from "@/components/layout/layout-traffic";
import UpdateButton from "@/components/layout/update-button"; import { UpdateButton } from "@/components/layout/update-button";
import useCustomTheme from "@/components/layout/use-custom-theme"; import { useCustomTheme } from "@/components/layout/use-custom-theme";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import "dayjs/locale/ru";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@ -36,13 +34,11 @@ const Layout = () => {
const { verge } = useVerge(); const { verge } = useVerge();
const { theme_blur, language } = verge || {}; const { theme_blur, language } = verge || {};
const setCurrentProfile = useSetRecoilState(atomCurrentProfile);
useEffect(() => { useEffect(() => {
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
if (e.key === "Escape") { // macOS有cmd+w
if (OS === "macos") appWindow.hide(); if (e.key === "Escape" && OS !== "macos") {
else appWindow.close(); appWindow.close();
} }
}); });
@ -52,6 +48,7 @@ const Layout = () => {
mutate("getProxies"); mutate("getProxies");
mutate("getVersion"); mutate("getVersion");
mutate("getClashConfig"); mutate("getClashConfig");
mutate("getProviders");
}); });
// update the verge config // update the verge config
@ -71,9 +68,6 @@ const Layout = () => {
break; break;
} }
}); });
// set current profile uid
getProfiles().then((data) => setCurrentProfile(data.current ?? ""));
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -84,7 +78,7 @@ const Layout = () => {
}, [language]); }, [language]);
return ( return (
<SWRConfig value={{}}> <SWRConfig value={{ errorRetryCount: 3 }}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Paper <Paper
square square
@ -95,7 +89,17 @@ const Layout = () => {
}} }}
onContextMenu={(e) => { onContextMenu={(e) => {
// only prevent it on Windows // only prevent it on Windows
if (OS === "windows") e.preventDefault(); const validList = ["input", "textarea"];
const target = e.currentTarget;
if (
OS === "windows" &&
!(
validList.includes(target.tagName.toLowerCase()) ||
target.isContentEditable
)
) {
e.preventDefault();
}
}} }}
sx={[ sx={[
({ palette }) => ({ ({ palette }) => ({
@ -133,13 +137,19 @@ const Layout = () => {
)} )}
<div className="the-content"> <div className="the-content">
<BaseErrorBoundary> <Routes>
<Routes> {routers.map(({ label, link, ele: Ele }) => (
{routers.map(({ label, link, ele: Ele }) => ( <Route
<Route key={label} path={link} element={<Ele />} /> key={label}
))} path={link}
</Routes> element={
</BaseErrorBoundary> <BaseErrorBoundary key={label}>
<Ele />
</BaseErrorBoundary>
}
/>
))}
</Routes>
</div> </div>
</div> </div>
</Paper> </Paper>

View file

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { import {
Box, Box,
@ -17,8 +17,13 @@ import { closeAllConnections } from "@/services/api";
import { atomConnectionSetting } from "@/services/states"; import { atomConnectionSetting } from "@/services/states";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
import { BaseEmpty, BasePage } from "@/components/base"; import { BaseEmpty, BasePage } from "@/components/base";
import ConnectionItem from "@/components/connection/connection-item"; import { useWebsocket } from "@/hooks/use-websocket";
import ConnectionTable from "@/components/connection/connection-table"; import { ConnectionItem } from "@/components/connection/connection-item";
import { ConnectionTable } from "@/components/connection/connection-table";
import {
ConnectionDetail,
ConnectionDetailRef,
} from "@/components/connection/connection-detail";
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] }; const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
@ -53,23 +58,18 @@ const ConnectionsPage = () => {
return connections; return connections;
}, [connData, filterText, curOrderOpt]); }, [connData, filterText, curOrderOpt]);
useEffect(() => { const { connect, disconnect } = useWebsocket(
if (!clashInfo) return; (event) => {
// meta v1.15.0 出现data.connections为null的情况
const { server = "", secret = "" } = clashInfo;
const ws = new WebSocket(`ws://${server}/connections?token=${secret}`);
ws.addEventListener("message", (event) => {
const data = JSON.parse(event.data) as IConnections; const data = JSON.parse(event.data) as IConnections;
// 尽量与前一次connections的展示顺序保持一致 // 尽量与前一次connections的展示顺序保持一致
setConnData((old) => { setConnData((old) => {
const oldConn = old.connections; const oldConn = old.connections;
const maxLen = data.connections.length; const maxLen = data.connections?.length;
const connections: typeof oldConn = []; const connections: typeof oldConn = [];
const rest = data.connections.filter((each) => { const rest = (data.connections || []).filter((each) => {
const index = oldConn.findIndex((o) => o.id === each.id); const index = oldConn.findIndex((o) => o.id === each.id);
if (index >= 0 && index < maxLen) { if (index >= 0 && index < maxLen) {
@ -93,23 +93,34 @@ const ConnectionsPage = () => {
return { ...data, connections }; return { ...data, connections };
}); });
}); },
{ errorCount: 3, retryInterval: 1000 }
);
return () => ws?.close(); useEffect(() => {
if (!clashInfo) return;
const { server = "", secret = "" } = clashInfo;
connect(`ws://${server}/connections?token=${encodeURIComponent(secret)}`);
return () => {
disconnect();
};
}, [clashInfo]); }, [clashInfo]);
const onCloseAll = useLockFn(closeAllConnections); const onCloseAll = useLockFn(closeAllConnections);
const detailRef = useRef<ConnectionDetailRef>(null!);
return ( return (
<BasePage <BasePage
title={t("Connections")} title={t("Connections")}
contentStyle={{ height: "100%" }} contentStyle={{ height: "100%" }}
header={ header={
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}> <Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 2 }}>
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
sx={{ mr: 2 }}
onClick={() => onClick={() =>
setSetting((o) => setSetting((o) =>
o.layout === "list" o.layout === "list"
@ -181,14 +192,24 @@ const ConnectionsPage = () => {
{filterConn.length === 0 ? ( {filterConn.length === 0 ? (
<BaseEmpty text="No Connections" /> <BaseEmpty text="No Connections" />
) : isTableLayout ? ( ) : isTableLayout ? (
<ConnectionTable connections={filterConn} /> <ConnectionTable
connections={filterConn}
onShowDetail={(detail) => detailRef.current?.open(detail)}
/>
) : ( ) : (
<Virtuoso <Virtuoso
data={filterConn} data={filterConn}
itemContent={(index, item) => <ConnectionItem value={item} />} itemContent={(index, item) => (
<ConnectionItem
value={item}
onShowDetail={() => detailRef.current?.open(item)}
/>
)}
/> />
)} )}
</Box> </Box>
<ConnectionDetail ref={detailRef} />
</Paper> </Paper>
</BasePage> </BasePage>
); );

View file

@ -41,11 +41,10 @@ const LogPage = () => {
title={t("Logs")} title={t("Logs")}
contentStyle={{ height: "100%" }} contentStyle={{ height: "100%" }}
header={ header={
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}> <Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 2 }}>
<IconButton <IconButton
size="small" size="small"
color="inherit" color="inherit"
sx={{ mr: 2 }}
onClick={() => setEnableLog((e) => !e)} onClick={() => setEnableLog((e) => !e)}
> >
{enableLog ? ( {enableLog ? (

View file

@ -1,21 +1,27 @@
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useEffect, useMemo, useRef, useState } from "react";
import { useSetRecoilState } from "recoil"; import { useSetRecoilState } from "recoil";
import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material"; import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material";
import { CachedRounded } from "@mui/icons-material"; import {
ClearRounded,
ContentCopyRounded,
LocalFireDepartmentRounded,
RefreshRounded,
TextSnippetOutlined,
} from "@mui/icons-material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
getProfiles, getProfiles,
patchProfile,
importProfile, importProfile,
enhanceProfiles, enhanceProfiles,
getRuntimeLogs, getRuntimeLogs,
deleteProfile, deleteProfile,
updateProfile,
} from "@/services/cmds"; } from "@/services/cmds";
import { closeAllConnections, getProxies, updateProxy } from "@/services/api"; import { atomLoadingCache } from "@/services/states";
import { atomCurrentProfile } from "@/services/states"; import { closeAllConnections } from "@/services/api";
import { BasePage, Notice } from "@/components/base"; import { BasePage, DialogRef, Notice } from "@/components/base";
import { import {
ProfileViewer, ProfileViewer,
ProfileViewerRef, ProfileViewerRef,
@ -23,16 +29,22 @@ import {
import { ProfileItem } from "@/components/profile/profile-item"; import { ProfileItem } from "@/components/profile/profile-item";
import { ProfileMore } from "@/components/profile/profile-more"; import { ProfileMore } from "@/components/profile/profile-more";
import { useProfiles } from "@/hooks/use-profiles"; import { useProfiles } from "@/hooks/use-profiles";
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { throttle } from "lodash-es";
const ProfilePage = () => { const ProfilePage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
const [activating, setActivating] = useState("");
const setCurrentProfile = useSetRecoilState(atomCurrentProfile); const {
profiles = {},
const { profiles = {}, patchProfiles, mutateProfiles } = useProfiles(); activateSelected,
patchProfiles,
mutateProfiles,
} = useProfiles();
const { data: chainLogs = {}, mutate: mutateLogs } = useSWR( const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
"getRuntimeLogs", "getRuntimeLogs",
@ -41,6 +53,7 @@ const ProfilePage = () => {
const chain = profiles.chain || []; const chain = profiles.chain || [];
const viewerRef = useRef<ProfileViewerRef>(null); const viewerRef = useRef<ProfileViewerRef>(null);
const configRef = useRef<DialogRef>(null);
// distinguish type // distinguish type
const { regularItems, enhanceItems } = useMemo(() => { const { regularItems, enhanceItems } = useMemo(() => {
@ -50,58 +63,17 @@ const ProfilePage = () => {
const type1 = ["local", "remote"]; const type1 = ["local", "remote"];
const type2 = ["merge", "script"]; const type2 = ["merge", "script"];
const regularItems = items.filter((i) => type1.includes(i.type!)); const regularItems = items.filter((i) => i && type1.includes(i.type!));
const restItems = items.filter((i) => type2.includes(i.type!)); const restItems = items.filter((i) => i && type2.includes(i.type!));
const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i])); const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
const enhanceItems = chain const enhanceItems = chain
.map((i) => restMap[i]!) .map((i) => restMap[i]!)
.filter(Boolean)
.concat(restItems.filter((i) => !chain.includes(i.uid))); .concat(restItems.filter((i) => !chain.includes(i.uid)));
return { regularItems, enhanceItems }; return { regularItems, enhanceItems };
}, [profiles]); }, [profiles]);
// sync selected proxy
useEffect(() => {
if (profiles.current == null) return;
const current = profiles.current;
const profile = regularItems.find((p) => p.uid === current);
setCurrentProfile(current);
if (!profile) return;
setTimeout(async () => {
const proxiesData = await getProxies();
mutate("getProxies", proxiesData);
// init selected array
const { selected = [] } = profile;
const selectedMap = Object.fromEntries(
selected.map((each) => [each.name!, each.now!])
);
let hasChange = false;
const newSelected: typeof selected = [];
const { global, groups } = proxiesData;
[global, ...groups].forEach(({ type, name, now }) => {
if (!now || (type !== "Selector" && type !== "Fallback")) return;
if (selectedMap[name] != null && selectedMap[name] !== now) {
hasChange = true;
updateProxy(name, selectedMap[name]);
}
newSelected.push({ name, now });
});
// update profile selected list
patchProfile(current!, { selected: newSelected });
// update proxies cache
if (hasChange) mutate("getProxies", getProxies());
}, 100);
}, [profiles, regularItems]);
const onImport = async () => { const onImport = async () => {
if (!url) return; if (!url) return;
setUrl(""); setUrl("");
@ -119,6 +91,7 @@ const ProfilePage = () => {
const current = remoteItem.uid; const current = remoteItem.uid;
patchProfiles({ current }); patchProfiles({ current });
mutateLogs(); mutateLogs();
setTimeout(() => activateSelected(), 2000);
} }
}); });
} catch (err: any) { } catch (err: any) {
@ -130,14 +103,19 @@ const ProfilePage = () => {
const onSelect = useLockFn(async (current: string, force: boolean) => { const onSelect = useLockFn(async (current: string, force: boolean) => {
if (!force && current === profiles.current) return; if (!force && current === profiles.current) return;
// 避免大多数情况下loading态闪烁
const reset = setTimeout(() => setActivating(current), 100);
try { try {
await patchProfiles({ current }); await patchProfiles({ current });
setCurrentProfile(current);
mutateLogs(); mutateLogs();
closeAllConnections(); closeAllConnections();
setTimeout(() => activateSelected(), 2000);
Notice.success("Refresh clash config", 1000); Notice.success("Refresh clash config", 1000);
} catch (err: any) { } catch (err: any) {
Notice.error(err?.message || err.toString(), 4000); Notice.error(err?.message || err.toString(), 4000);
} finally {
clearTimeout(reset);
setActivating("");
} }
}); });
@ -190,18 +168,70 @@ const ProfilePage = () => {
mutateLogs(); mutateLogs();
}); });
// 更新所有配置
const setLoadingCache = useSetRecoilState(atomLoadingCache);
const onUpdateAll = useLockFn(async () => {
const throttleMutate = throttle(mutateProfiles, 2000, {
trailing: true,
});
const updateOne = async (uid: string) => {
try {
await updateProfile(uid);
throttleMutate();
} finally {
setLoadingCache((cache) => ({ ...cache, [uid]: false }));
}
};
return new Promise((resolve) => {
setLoadingCache((cache) => {
// 获取没有正在更新的配置
const items = regularItems.filter(
(e) => e.type === "remote" && !cache[e.uid]
);
const change = Object.fromEntries(items.map((e) => [e.uid, true]));
Promise.allSettled(items.map((e) => updateOne(e.uid))).then(resolve);
return { ...cache, ...change };
});
});
});
const onCopyLink = async () => {
const text = await navigator.clipboard.readText();
if (text) setUrl(text);
};
return ( return (
<BasePage <BasePage
title={t("Profiles")} title={t("Profiles")}
header={ header={
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}> <Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 1 }}>
<IconButton <IconButton
size="small" size="small"
color="inherit" color="inherit"
title={t("Refresh profiles")} title={t("Update All Profiles")}
onClick={onUpdateAll}
>
<RefreshRounded />
</IconButton>
<IconButton
size="small"
color="inherit"
title={t("View Runtime Config")}
onClick={() => configRef.current?.open()}
>
<TextSnippetOutlined />
</IconButton>
<IconButton
size="small"
color="primary"
title={t("Reactivate Profiles")}
onClick={onEnhance} onClick={onEnhance}
> >
<CachedRounded /> <LocalFireDepartmentRounded />
</IconButton> </IconButton>
</Box> </Box>
} }
@ -218,6 +248,28 @@ const ProfilePage = () => {
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
sx={{ input: { py: 0.65, px: 1.25 } }} sx={{ input: { py: 0.65, px: 1.25 } }}
placeholder={t("Profile URL")} placeholder={t("Profile URL")}
InputProps={{
sx: { pr: 1 },
endAdornment: !url ? (
<IconButton
size="small"
sx={{ p: 0.5 }}
title={t("Paste")}
onClick={onCopyLink}
>
<ContentCopyRounded fontSize="inherit" />
</IconButton>
) : (
<IconButton
size="small"
sx={{ p: 0.5 }}
title={t("Clear")}
onClick={() => setUrl("")}
>
<ClearRounded fontSize="inherit" />
</IconButton>
),
}}
/> />
<Button <Button
disabled={!url || disabled} disabled={!url || disabled}
@ -242,6 +294,7 @@ const ProfilePage = () => {
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}> <Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
<ProfileItem <ProfileItem
selected={profiles.current === item.uid} selected={profiles.current === item.uid}
activating={activating === item.uid}
itemData={item} itemData={item}
onSelect={(f) => onSelect(item.uid, f)} onSelect={(f) => onSelect(item.uid, f)}
onEdit={() => viewerRef.current?.edit(item)} onEdit={() => viewerRef.current?.edit(item)}
@ -273,6 +326,7 @@ const ProfilePage = () => {
)} )}
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} /> <ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
<ConfigViewer ref={configRef} />
</BasePage> </BasePage>
); );
}; };

View file

@ -2,12 +2,17 @@ import useSWR from "swr";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, ButtonGroup, Paper } from "@mui/material"; import { Box, Button, ButtonGroup, Paper } from "@mui/material";
import { getClashConfig, updateConfigs } from "@/services/api"; import {
closeAllConnections,
getClashConfig,
updateConfigs,
} from "@/services/api";
import { patchClashConfig } from "@/services/cmds"; import { patchClashConfig } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { BasePage } from "@/components/base"; import { BasePage } from "@/components/base";
import { ProxyGroups } from "@/components/proxy/proxy-groups"; import { ProxyGroups } from "@/components/proxy/proxy-groups";
import { ProviderButton } from "@/components/proxy/provider-button";
const ProxyPage = () => { const ProxyPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -26,9 +31,13 @@ const ProxyPage = () => {
return ["rule", "global", "direct", "script"]; return ["rule", "global", "direct", "script"];
}, [verge?.clash_core]); }, [verge?.clash_core]);
const curMode = clashConfig?.mode.toLowerCase(); const curMode = clashConfig?.mode?.toLowerCase();
const onChangeMode = useLockFn(async (mode: string) => { const onChangeMode = useLockFn(async (mode: string) => {
// 断开连接
if (mode !== curMode && verge?.auto_close_connection) {
closeAllConnections();
}
await updateConfigs({ mode }); await updateConfigs({ mode });
await patchClashConfig({ mode }); await patchClashConfig({ mode });
mutateClash(); mutateClash();
@ -45,18 +54,22 @@ const ProxyPage = () => {
contentStyle={{ height: "100%" }} contentStyle={{ height: "100%" }}
title={t("Proxy Groups")} title={t("Proxy Groups")}
header={ header={
<ButtonGroup size="small"> <Box display="flex" alignItems="center" gap={1}>
{modeList.map((mode) => ( <ProviderButton />
<Button
key={mode} <ButtonGroup size="small">
variant={mode === curMode ? "contained" : "outlined"} {modeList.map((mode) => (
onClick={() => onChangeMode(mode)} <Button
sx={{ textTransform: "capitalize" }} key={mode}
> variant={mode === curMode ? "contained" : "outlined"}
{t(mode)} onClick={() => onChangeMode(mode)}
</Button> sx={{ textTransform: "capitalize" }}
))} >
</ButtonGroup> {t(mode)}
</Button>
))}
</ButtonGroup>
</Box>
} }
> >
<Paper <Paper

View file

@ -1,6 +1,9 @@
import { Paper } from "@mui/material"; import { IconButton, Paper } from "@mui/material";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BasePage, Notice } from "@/components/base"; import { BasePage, Notice } from "@/components/base";
import { GitHub } from "@mui/icons-material";
import { openWebUrl } from "@/services/cmds";
import SettingVerge from "@/components/setting/setting-verge"; import SettingVerge from "@/components/setting/setting-verge";
import SettingClash from "@/components/setting/setting-clash"; import SettingClash from "@/components/setting/setting-clash";
import SettingSystem from "@/components/setting/setting-system"; import SettingSystem from "@/components/setting/setting-system";
@ -12,8 +15,24 @@ const SettingPage = () => {
Notice.error(err?.message || err.toString()); Notice.error(err?.message || err.toString());
}; };
const toGithubRepo = useLockFn(() => {
return openWebUrl("https://github.com/zzzgydi/clash-verge");
});
return ( return (
<BasePage title={t("Settings")}> <BasePage
title={t("Settings")}
header={
<IconButton
size="small"
color="inherit"
title="@zzzgydi/clash-verge"
onClick={toGithubRepo}
>
<GitHub fontSize="inherit" />
</IconButton>
}
>
<Paper sx={{ borderRadius: 1, boxShadow: 2, mb: 3 }}> <Paper sx={{ borderRadius: 1, boxShadow: 2, mb: 3 }}>
<SettingClash onError={onError} /> <SettingClash onError={onError} />
</Paper> </Paper>

View file

@ -27,6 +27,7 @@ export const getAxios = async (force: boolean = false) => {
axiosIns = axios.create({ axiosIns = axios.create({
baseURL: `http://${server}`, baseURL: `http://${server}`,
headers: secret ? { Authorization: `Bearer ${secret}` } : {}, headers: secret ? { Authorization: `Bearer ${secret}` } : {},
timeout: 15000,
}); });
axiosIns.interceptors.response.use((r) => r.data); axiosIns.interceptors.response.use((r) => r.data);
return axiosIns; return axiosIns;
@ -64,7 +65,7 @@ export const getRules = async () => {
/// Get Proxy delay /// Get Proxy delay
export const getProxyDelay = async (name: string, url?: string) => { export const getProxyDelay = async (name: string, url?: string) => {
const params = { const params = {
timeout: 5000, timeout: 10000,
url: url || "http://www.gstatic.com/generate_204", url: url || "http://www.gstatic.com/generate_204",
}; };
const instance = await getAxios(); const instance = await getAxios();
@ -168,6 +169,11 @@ export const providerHealthCheck = async (name: string) => {
); );
}; };
export const providerUpdate = async (name: string) => {
const instance = await getAxios();
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`);
};
export const getConnections = async () => { export const getConnections = async () => {
const instance = await getAxios(); const instance = await getAxios();
const result = await instance.get("/connections"); const result = await instance.get("/connections");

View file

@ -127,6 +127,10 @@ export async function restartSidecar() {
return invoke<void>("restart_sidecar"); return invoke<void>("restart_sidecar");
} }
export async function grantPermission(core: string) {
return invoke<void>("grant_permission", { core });
}
export async function openAppDir() { export async function openAppDir() {
return invoke<void>("open_app_dir").catch((err) => return invoke<void>("open_app_dir").catch((err) =>
Notice.error(err?.message || err.toString(), 1500) Notice.error(err?.message || err.toString(), 1500)
@ -149,6 +153,11 @@ export async function openWebUrl(url: string) {
return invoke<void>("open_web_url", { url }); return invoke<void>("open_web_url", { url });
} }
export async function cmdGetProxyDelay(name: string, url?: string) {
name = encodeURIComponent(name);
return invoke<{ delay: number }>("clash_api_get_proxy_delay", { name, url });
}
/// service mode /// service mode
export async function checkService() { export async function checkService() {

View file

@ -1,4 +1,4 @@
import { getProxyDelay } from "./api"; import { cmdGetProxyDelay } from "./cmds";
const hashKey = (name: string, group: string) => `${group ?? ""}::${name}`; const hashKey = (name: string, group: string) => `${group ?? ""}::${name}`;
@ -74,7 +74,7 @@ class DelayManager {
try { try {
const url = this.getUrl(group); const url = this.getUrl(group);
const result = await getProxyDelay(name, url); const result = await cmdGetProxyDelay(name, url);
delay = result.delay; delay = result.delay;
} catch { } catch {
delay = 1e6; // error delay = 1e6; // error
@ -84,7 +84,7 @@ class DelayManager {
return delay; return delay;
} }
async checkListDelay(nameList: string[], group: string, concurrency = 6) { async checkListDelay(nameList: string[], group: string, concurrency = 36) {
const names = nameList.filter(Boolean); const names = nameList.filter(Boolean);
// 设置正在延迟测试中 // 设置正在延迟测试中
names.forEach((name) => this.setDelay(name, group, -2)); names.forEach((name) => this.setDelay(name, group, -2));
@ -107,6 +107,21 @@ class DelayManager {
for (let i = 0; i < concurrency; ++i) help(); for (let i = 0; i < concurrency; ++i) help();
}); });
} }
formatDelay(delay: number) {
if (delay < 0) return "-";
if (delay > 1e5) return "Error";
if (delay >= 10000) return "Timeout"; // 10s
return `${delay}`;
}
formatDelayColor(delay: number) {
if (delay <= 0) return "text.secondary";
if (delay >= 10000) return "error.main";
if (delay > 500) return "warning.main";
if (delay > 100) return "text.secondary";
return "success.main";
}
} }
export default new DelayManager(); export default new DelayManager();

View file

@ -1,10 +1,12 @@
import i18n from "i18next"; import i18n from "i18next";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import en from "@/locales/en.json"; import en from "@/locales/en.json";
import ru from "@/locales/ru.json";
import zh from "@/locales/zh.json"; import zh from "@/locales/zh.json";
const resources = { const resources = {
en: { translation: en }, en: { translation: en },
ru: { translation: ru },
zh: { translation: zh }, zh: { translation: zh },
}; };

View file

@ -71,9 +71,3 @@ export const atomUpdateState = atom<boolean>({
key: "atomUpdateState", key: "atomUpdateState",
default: false, default: false,
}); });
// current profile uid
export const atomCurrentProfile = atom<string>({
key: "atomCurrentProfile",
default: "",
});

View file

@ -153,11 +153,13 @@ interface IProfilesConfig {
} }
interface IVergeConfig { interface IVergeConfig {
app_log_level?: "trace" | "debug" | "info" | "warn" | "error" | string;
language?: string; language?: string;
clash_core?: string; clash_core?: string;
theme_mode?: "light" | "dark" | "system"; theme_mode?: "light" | "dark" | "system";
theme_blur?: boolean; theme_blur?: boolean;
traffic_graph?: boolean; traffic_graph?: boolean;
enable_memory_usage?: boolean;
enable_tun_mode?: boolean; enable_tun_mode?: boolean;
enable_auto_launch?: boolean; enable_auto_launch?: boolean;
enable_service_mode?: boolean; enable_service_mode?: boolean;
@ -182,7 +184,9 @@ interface IVergeConfig {
}; };
auto_close_connection?: boolean; auto_close_connection?: boolean;
default_latency_test?: string; default_latency_test?: string;
enable_clash_fields?: boolean;
enable_builtin_enhanced?: boolean; enable_builtin_enhanced?: boolean;
auto_log_clean?: 0 | 1 | 2 | 3;
proxy_layout_column?: number; proxy_layout_column?: number;
} }

View file

@ -39,9 +39,14 @@ export const OTHERS_FIELDS = [
"authentication", "authentication",
"tls", // meta "tls", // meta
"sniffer", // meta "sniffer", // meta
"geox-url", // meta
"listeners", // meta "listeners", // meta
"sub-rules", // meta "sub-rules", // meta
"geodata-mode", // meta "geodata-mode", // meta
"unified-delay", // meta
"tcp-concurrent", // meta "tcp-concurrent", // meta
"enable-process", // meta "enable-process", // meta
"find-process-mode", // meta
"external-controller-tls", // meta
"global-client-fingerprint", // meta
] as const; ] as const;

View file

@ -1,6 +1,7 @@
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const parseTraffic = (num: number) => { const parseTraffic = (num?: number) => {
if (typeof num !== "number") return ["NaN", ""];
if (num < 1000) return [`${Math.round(num)}`, "B"]; if (num < 1000) return [`${Math.round(num)}`, "B"];
const exp = Math.min(Math.floor(Math.log2(num) / 10), UNITS.length - 1); const exp = Math.min(Math.floor(Math.log2(num) / 10), UNITS.length - 1);
const dat = num / Math.pow(1024, exp); const dat = num / Math.pow(1024, exp);

View file

@ -0,0 +1,6 @@
export const truncateStr = (str?: string, prefixLen = 16, maxLen = 56) => {
if (!str || str.length <= maxLen) return str;
return (
str.slice(0, prefixLen) + " ... " + str.slice(-(maxLen - prefixLen - 5))
);
};

416
yarn.lock
View file

@ -437,10 +437,15 @@
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
"@esbuild/linux-loong64@0.14.54": "@esbuild/android-arm@0.15.18":
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80"
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== integrity sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==
"@esbuild/linux-loong64@0.15.18":
version "0.15.18"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239"
integrity sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==
"@jridgewell/gen-mapping@^0.3.2": "@jridgewell/gen-mapping@^0.3.2":
version "0.3.2" version "0.3.2"
@ -787,70 +792,70 @@
"@svgr/hast-util-to-babel-ast" "^6.3.1" "@svgr/hast-util-to-babel-ast" "^6.3.1"
svg-parser "^2.0.4" svg-parser "^2.0.4"
"@tauri-apps/api@^1.2.0": "@tauri-apps/api@^1.3.0":
version "1.2.0" version "1.3.0"
resolved "https://registry.npmmirror.com/@tauri-apps/api/-/api-1.2.0.tgz#1f196b3e012971227f41b98214c846430a4eb477" resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.3.0.tgz#d0c853ab2cc7506bd826c5f7f260c67c7c15def5"
integrity sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw== integrity sha512-AH+3FonkKZNtfRtGrObY38PrzEj4d+1emCbwNGu0V2ENbXjlLHMZQlUh+Bhu/CRmjaIwZMGJ3yFvWaZZgTHoog==
"@tauri-apps/cli-darwin-arm64@1.2.2": "@tauri-apps/cli-darwin-arm64@1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.2.tgz#f18b0689b399b64dc67da41c0fd4da474be324fe" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.3.1.tgz#ef0fe290e0a6e3e53fa2cc4f1a72a0c87921427c"
integrity sha512-W+Cp2weUMlvmGkRJeUjypbz9Lpl6o98xkgKAtobZSum5SNwpsBQfawJTESakNoD+FXyVg/snIk5sRdHge+tAaA== integrity sha512-QlepYVPgOgspcwA/u4kGG4ZUijlXfdRtno00zEy+LxinN/IRXtk+6ErVtsmoLi1ZC9WbuMwzAcsRvqsD+RtNAg==
"@tauri-apps/cli-darwin-x64@1.2.2": "@tauri-apps/cli-darwin-x64@1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.2.tgz#4450bd244f586da346651f62a5a41686cee36db7" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.3.1.tgz#4c84ea0f08a5b636b067943d637a38e091a4aad3"
integrity sha512-vmVAqt+ECH2d6cbcGJ7ddcCAZgmKe5xmxlL5r4xoaphu7OqU4gnv4VFURYkVltOfwzIFQVOPVSqwYyIDToCYNQ== integrity sha512-fKcAUPVFO3jfDKXCSDGY0MhZFF/wDtx3rgFnogWYu4knk38o9RaqRkvMvqJhLYPuWaEM5h6/z1dRrr9KKCbrVg==
"@tauri-apps/cli-linux-arm-gnueabihf@1.2.2": "@tauri-apps/cli-linux-arm-gnueabihf@1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.2.tgz#a4d5b95665620605fb0f54cb77f8134239b58370" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.3.1.tgz#a4f1b237189e4f8f89cc890e1dc2eec76d4345be"
integrity sha512-yYTdQurgi4QZR8z+fANjl522jdQz/VtesFpw+C/A0+zXg7tiRjicsywBDdPsvNzCqFeGKKkmTR+Lny5qxhGaeQ== integrity sha512-+4H0dv8ltJHYu/Ma1h9ixUPUWka9EjaYa8nJfiMsdCI4LJLNE6cPveE7RmhZ59v9GW1XB108/k083JUC/OtGvA==
"@tauri-apps/cli-linux-arm64-gnu@1.2.2": "@tauri-apps/cli-linux-arm64-gnu@1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.2.tgz#c992907586b42e2085160d45b3a17316c99febea" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.3.1.tgz#e2391326b64dfe13c7442bdcc13c4988ce5e6df9"
integrity sha512-ZSOVT6Eq1ay2+27B8KfA0MnpO7KYzONU6TjenH7DNcQki6eWGG5JoNu8QQ9Mdn3dAzY0XBP9i1ZHQOFu4iPtEg== integrity sha512-Pj3odVO1JAxLjYmoXKxcrpj/tPxcA8UP8N06finhNtBtBaxAjrjjxKjO4968KB0BUH7AASIss9EL4Tr0FGnDuw==
"@tauri-apps/cli-linux-arm64-musl@1.2.2": "@tauri-apps/cli-linux-arm64-musl@1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.2.tgz#f8bd7901ce525b8433919e2cfe7ec22aace0f776" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.3.1.tgz#49354349f80f879ffc6950c0c03c0aea1395efa5"
integrity sha512-/NHSkqNQ+Pr4PshvyD1CeNFaPCaCpe1OeuAQgVi0rboSecC9fXN96G5dQbSBoxOUcCo6f8aTVE7zkZ4WchFVog== integrity sha512-tA0JdDLPFaj42UDIVcF2t8V0tSha40rppcmAR/MfQpTCxih6399iMjwihz9kZE1n4b5O4KTq9GliYo50a8zYlQ==
"@tauri-apps/cli-linux-x64-gnu@1.2.2": "@tauri-apps/cli-linux-x64-gnu@1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.2.tgz#a558862d7101b0a641c5a994c9ee74edb8300fc5" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.3.1.tgz#9a33ffe9e0d9b1b3825db57cbcfcddeb773682c6"
integrity sha512-4YTmfPuyvlHsvCkATDMwhklfuQm3HKxYXv/IOW9H0ra6pS9efVhrFYIC9Vfv6XaKN85Vnn/FYTEGMJLwCxZw2Q== integrity sha512-FDU+Mnvk6NLkqQimcNojdKpMN4Y3W51+SQl+NqG9AFCWprCcSg62yRb84751ujZuf2MGT8HQOfmd0i77F4Q3tQ==
"@tauri-apps/cli-linux-x64-musl@1.2.2": "@tauri-apps/cli-linux-x64-musl@1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.2.tgz#573e965a638864a65b606783d3dbd34114939277" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.3.1.tgz#5283731e894c17bc070c499e73145cfe2633ef21"
integrity sha512-wr46tbscwFuCcA931R+ItOiUTT0djMmgKLd1HFCmFF82V9BKE2reIjr6O9l0NCXCo2WeD4pe3jA/Pt1dxDu+JA== integrity sha512-MpO3akXFmK8lZYEbyQRDfhdxz1JkTBhonVuz5rRqxwA7gnGWHa1aF1+/2zsy7ahjB2tQ9x8DDFDMdVE20o9HrA==
"@tauri-apps/cli-win32-ia32-msvc@1.2.2": "@tauri-apps/cli-win32-ia32-msvc@1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.2.tgz#0fe38283ffeb71acac6a6a317951dc75ec2d3dad" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.3.1.tgz#f31538abfd94f27ade1f17d01f30da6be1660c6f"
integrity sha512-6VmbVJOWUZJK5/JKhb3mNFKrKGfq0KV7lJGumfN95WJgkHeyL61p8bZit+o6ZgUGUhrOabkAawhDkrRY+ZQhIw== integrity sha512-9Boeo3K5sOrSBAZBuYyGkpV2RfnGQz3ZhGJt4hE6P+HxRd62lS6+qDKAiw1GmkZ0l1drc2INWrNeT50gwOKwIQ==
"@tauri-apps/cli-win32-x64-msvc@1.2.2": "@tauri-apps/cli-win32-x64-msvc@1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.2.tgz#9b8ea7c63f2883af105e204fc4f1f11a83990a59" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.3.1.tgz#1eb09d55b99916a3cd84cb91c75ef906db67d35d"
integrity sha512-YRPJguJma+zSuRZpFoSZqls6+laggG1vqG0FPQWQTi+ywATgMpai2b2RZnffDlpHKp9mt4V/s2dtqOy6bpGZHg== integrity sha512-wMrTo91hUu5CdpbElrOmcZEoJR4aooTG+fbtcc87SMyPGQy1Ux62b+ZdwLvL1sVTxnIm//7v6QLRIWGiUjCPwA==
"@tauri-apps/cli@^1.2.2": "@tauri-apps/cli@^1.3.1":
version "1.2.2" version "1.3.1"
resolved "https://registry.npmmirror.com/@tauri-apps/cli/-/cli-1.2.2.tgz#cf7cb90d11c3c3b757efe07b7e2c48abde045ada" resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.3.1.tgz#4c5259bf1f9c97084dd016e6b34dca53de380e24"
integrity sha512-D8zib3A0vWCvPPSyYLxww/OdDlVcY7fpcDVBH6qUvheOjj2aCyU7H9AYMRBwpgCfz8zY5+vomee+laLeB0H13w== integrity sha512-o4I0JujdITsVRm3/0spfJX7FcKYrYV1DXJqzlWIn6IY25/RltjU6qbC1TPgVww3RsRX63jyVUTcWpj5wwFl+EQ==
optionalDependencies: optionalDependencies:
"@tauri-apps/cli-darwin-arm64" "1.2.2" "@tauri-apps/cli-darwin-arm64" "1.3.1"
"@tauri-apps/cli-darwin-x64" "1.2.2" "@tauri-apps/cli-darwin-x64" "1.3.1"
"@tauri-apps/cli-linux-arm-gnueabihf" "1.2.2" "@tauri-apps/cli-linux-arm-gnueabihf" "1.3.1"
"@tauri-apps/cli-linux-arm64-gnu" "1.2.2" "@tauri-apps/cli-linux-arm64-gnu" "1.3.1"
"@tauri-apps/cli-linux-arm64-musl" "1.2.2" "@tauri-apps/cli-linux-arm64-musl" "1.3.1"
"@tauri-apps/cli-linux-x64-gnu" "1.2.2" "@tauri-apps/cli-linux-x64-gnu" "1.3.1"
"@tauri-apps/cli-linux-x64-musl" "1.2.2" "@tauri-apps/cli-linux-x64-musl" "1.3.1"
"@tauri-apps/cli-win32-ia32-msvc" "1.2.2" "@tauri-apps/cli-win32-ia32-msvc" "1.3.1"
"@tauri-apps/cli-win32-x64-msvc" "1.2.2" "@tauri-apps/cli-win32-x64-msvc" "1.3.1"
"@types/fs-extra@^9.0.13": "@types/fs-extra@^9.0.13":
version "9.0.13" version "9.0.13"
@ -869,6 +874,18 @@
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.2.tgz#451eaeece64c6bdac8b2dde0caab23b085899e0d" resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.2.tgz#451eaeece64c6bdac8b2dde0caab23b085899e0d"
integrity sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA== integrity sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA==
"@types/lodash-es@^4.17.7":
version "4.17.7"
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.7.tgz#22edcae9f44aff08546e71db8925f05b33c7cc40"
integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.191"
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
"@types/lodash@^4.14.180": "@types/lodash@^4.14.180":
version "4.14.180" version "4.14.180"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
@ -899,10 +916,10 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/react-dom@^17.0.0": "@types/react-dom@^18.0.11":
version "17.0.14" version "18.0.11"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.14.tgz#c8f917156b652ddf807711f5becbd2ab018dea9f" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
integrity sha512-H03xwEP1oXmSfl3iobtmQ/2dHF5aBHr8aUMwyGZya6OW45G+xtdzmq6HkncefiBt5JU8DVyaWl/nWZbjZCnzAQ== integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
@ -920,7 +937,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@^17.0.0": "@types/react@*":
version "17.0.43" version "17.0.43"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55"
integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A== integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==
@ -1282,132 +1299,133 @@ error-ex@^1.3.1:
dependencies: dependencies:
is-arrayish "^0.2.1" is-arrayish "^0.2.1"
esbuild-android-64@0.14.54: esbuild-android-64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz#20a7ae1416c8eaade917fb2453c1259302c637a5"
integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== integrity sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==
esbuild-android-arm64@0.14.54: esbuild-android-arm64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz#9cc0ec60581d6ad267568f29cf4895ffdd9f2f04"
integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== integrity sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==
esbuild-darwin-64@0.14.54: esbuild-darwin-64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz#428e1730ea819d500808f220fbc5207aea6d4410"
integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== integrity sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==
esbuild-darwin-arm64@0.14.54: esbuild-darwin-arm64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz#b6dfc7799115a2917f35970bfbc93ae50256b337"
integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== integrity sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==
esbuild-freebsd-64@0.14.54: esbuild-freebsd-64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz#4e190d9c2d1e67164619ae30a438be87d5eedaf2"
integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== integrity sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==
esbuild-freebsd-arm64@0.14.54: esbuild-freebsd-arm64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz#18a4c0344ee23bd5a6d06d18c76e2fd6d3f91635"
integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== integrity sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==
esbuild-linux-32@0.14.54: esbuild-linux-32@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz#9a329731ee079b12262b793fb84eea762e82e0ce"
integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== integrity sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==
esbuild-linux-64@0.14.54: esbuild-linux-64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz#532738075397b994467b514e524aeb520c191b6c"
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== integrity sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==
esbuild-linux-arm64@0.14.54: esbuild-linux-arm64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz#5372e7993ac2da8f06b2ba313710d722b7a86e5d"
integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== integrity sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==
esbuild-linux-arm@0.14.54: esbuild-linux-arm@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz#e734aaf259a2e3d109d4886c9e81ec0f2fd9a9cc"
integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== integrity sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==
esbuild-linux-mips64le@0.14.54: esbuild-linux-mips64le@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz#c0487c14a9371a84eb08fab0e1d7b045a77105eb"
integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== integrity sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==
esbuild-linux-ppc64le@0.14.54: esbuild-linux-ppc64le@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz#af048ad94eed0ce32f6d5a873f7abe9115012507"
integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== integrity sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==
esbuild-linux-riscv64@0.14.54: esbuild-linux-riscv64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz#423ed4e5927bd77f842bd566972178f424d455e6"
integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== integrity sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==
esbuild-linux-s390x@0.14.54: esbuild-linux-s390x@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz#21d21eaa962a183bfb76312e5a01cc5ae48ce8eb"
integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== integrity sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==
esbuild-netbsd-64@0.14.54: esbuild-netbsd-64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz#ae75682f60d08560b1fe9482bfe0173e5110b998"
integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== integrity sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==
esbuild-openbsd-64@0.14.54: esbuild-openbsd-64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz#79591a90aa3b03e4863f93beec0d2bab2853d0a8"
integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== integrity sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==
esbuild-sunos-64@0.14.54: esbuild-sunos-64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz#fd528aa5da5374b7e1e93d36ef9b07c3dfed2971"
integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== integrity sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==
esbuild-windows-32@0.14.54: esbuild-windows-32@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz#0e92b66ecdf5435a76813c4bc5ccda0696f4efc3"
integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== integrity sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==
esbuild-windows-64@0.14.54: esbuild-windows-64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz#0fc761d785414284fc408e7914226d33f82420d0"
integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== integrity sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==
esbuild-windows-arm64@0.14.54: esbuild-windows-arm64@0.15.18:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz#5b5bdc56d341d0922ee94965c89ee120a6a86eb7"
integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== integrity sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==
esbuild@^0.14.47: esbuild@^0.15.9:
version "0.14.54" version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.18.tgz#ea894adaf3fbc036d32320a00d4d6e4978a2f36d"
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== integrity sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==
optionalDependencies: optionalDependencies:
"@esbuild/linux-loong64" "0.14.54" "@esbuild/android-arm" "0.15.18"
esbuild-android-64 "0.14.54" "@esbuild/linux-loong64" "0.15.18"
esbuild-android-arm64 "0.14.54" esbuild-android-64 "0.15.18"
esbuild-darwin-64 "0.14.54" esbuild-android-arm64 "0.15.18"
esbuild-darwin-arm64 "0.14.54" esbuild-darwin-64 "0.15.18"
esbuild-freebsd-64 "0.14.54" esbuild-darwin-arm64 "0.15.18"
esbuild-freebsd-arm64 "0.14.54" esbuild-freebsd-64 "0.15.18"
esbuild-linux-32 "0.14.54" esbuild-freebsd-arm64 "0.15.18"
esbuild-linux-64 "0.14.54" esbuild-linux-32 "0.15.18"
esbuild-linux-arm "0.14.54" esbuild-linux-64 "0.15.18"
esbuild-linux-arm64 "0.14.54" esbuild-linux-arm "0.15.18"
esbuild-linux-mips64le "0.14.54" esbuild-linux-arm64 "0.15.18"
esbuild-linux-ppc64le "0.14.54" esbuild-linux-mips64le "0.15.18"
esbuild-linux-riscv64 "0.14.54" esbuild-linux-ppc64le "0.15.18"
esbuild-linux-s390x "0.14.54" esbuild-linux-riscv64 "0.15.18"
esbuild-netbsd-64 "0.14.54" esbuild-linux-s390x "0.15.18"
esbuild-openbsd-64 "0.14.54" esbuild-netbsd-64 "0.15.18"
esbuild-sunos-64 "0.14.54" esbuild-openbsd-64 "0.15.18"
esbuild-windows-32 "0.14.54" esbuild-sunos-64 "0.15.18"
esbuild-windows-64 "0.14.54" esbuild-windows-32 "0.15.18"
esbuild-windows-arm64 "0.14.54" esbuild-windows-64 "0.15.18"
esbuild-windows-arm64 "0.15.18"
escalade@^3.1.1: escalade@^3.1.1:
version "3.1.1" version "3.1.1"
@ -1722,6 +1740,11 @@ locate-path@^5.0.0:
dependencies: dependencies:
p-locate "^4.1.0" p-locate "^4.1.0"
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash@^4.17.21: lodash@^4.17.21:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@ -1924,10 +1947,10 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
postcss@^8.4.16: postcss@^8.4.18:
version "8.4.16" version "8.4.21"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
dependencies: dependencies:
nanoid "^3.3.4" nanoid "^3.3.4"
picocolors "^1.0.0" picocolors "^1.0.0"
@ -1972,14 +1995,13 @@ pump@^3.0.0:
end-of-stream "^1.1.0" end-of-stream "^1.1.0"
once "^1.3.1" once "^1.3.1"
react-dom@^17.0.2: react-dom@^18.2.0:
version "17.0.2" version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" scheduler "^0.23.0"
scheduler "^0.20.2"
react-error-boundary@^3.1.4: react-error-boundary@^3.1.4:
version "3.1.4" version "3.1.4"
@ -2049,13 +2071,12 @@ react-virtuoso@^3.1.3:
"@virtuoso.dev/react-urx" "^0.2.12" "@virtuoso.dev/react-urx" "^0.2.12"
"@virtuoso.dev/urx" "^0.2.12" "@virtuoso.dev/urx" "^0.2.12"
react@^17.0.2: react@^18.2.0:
version "17.0.2" version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1"
readdirp@~3.6.0: readdirp@~3.6.0:
version "3.6.0" version "3.6.0"
@ -2105,10 +2126,10 @@ resolve@^1.19.0, resolve@^1.22.1:
path-parse "^1.0.7" path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0" supports-preserve-symlinks-flag "^1.0.0"
"rollup@>=2.75.6 <2.77.0 || ~2.77.0": rollup@^2.79.1:
version "2.77.3" version "2.79.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g== integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
@ -2126,13 +2147,12 @@ sass@^1.54.0:
immutable "^4.0.0" immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0" source-map-js ">=0.6.2 <2.0.0"
scheduler@^0.20.2: scheduler@^0.23.0:
version "0.20.2" version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1"
screenfull@^5.0.0: screenfull@^5.0.0:
version "5.2.0" version "5.2.0"
@ -2275,15 +2295,15 @@ vite-plugin-svgr@^2.2.1:
"@rollup/pluginutils" "^4.2.1" "@rollup/pluginutils" "^4.2.1"
"@svgr/core" "^6.3.1" "@svgr/core" "^6.3.1"
vite@^3.0.9: vite@^3.2.5:
version "3.0.9" version "3.2.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.9.tgz#45fac22c2a5290a970f23d66c1aef56a04be8a30" resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.5.tgz#dee5678172a8a0ab3e547ad4148c3d547f90e86a"
integrity sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw== integrity sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==
dependencies: dependencies:
esbuild "^0.14.47" esbuild "^0.15.9"
postcss "^8.4.16" postcss "^8.4.18"
resolve "^1.22.1" resolve "^1.22.1"
rollup ">=2.75.6 <2.77.0 || ~2.77.0" rollup "^2.79.1"
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"