Compare commits
116 commits
Author | SHA1 | Date | |
---|---|---|---|
|
9df1115380 | ||
|
f22e360cbb | ||
|
67769af6f4 | ||
|
b1a9a1d6d9 | ||
|
cf0606ecb7 | ||
|
7287edcd6f | ||
|
e0d26203dd | ||
|
7e3a85e9da | ||
|
5a0fed9c93 | ||
|
1f1e743912 | ||
|
b4301ed0d5 | ||
|
b5391560fc | ||
|
718989cbcf | ||
|
d0aee76962 | ||
|
fb08af96bd | ||
|
510a0c5e70 | ||
|
5fe2be031f | ||
|
2ba3aaba47 | ||
|
ad8903991c | ||
|
3e5624c570 | ||
|
f5ee6f3537 | ||
|
afc77e7adc | ||
|
024f42fce6 | ||
|
8a5f12b97c | ||
|
954b21cf39 | ||
|
74d095774d | ||
|
17a2722e6d | ||
|
c843bddbfe | ||
|
3f22a49755 | ||
|
7af2ffcebf | ||
|
de90c959e0 | ||
|
9987dc1eb4 | ||
|
3efd575dd2 | ||
|
f4c7b17a87 | ||
|
16d80718cb | ||
|
ad228d53b7 | ||
|
15ee1e531b | ||
|
1c8fb3392a | ||
|
8647866a32 | ||
|
23351c4f1c | ||
|
1367c304cf | ||
|
26d6bcb074 | ||
|
b0d651ece1 | ||
|
b6d50ba6a4 | ||
|
b3ab6a9166 | ||
|
f39a5ac9c2 | ||
|
38a9a9240d | ||
|
241b22a465 | ||
|
741abc0366 | ||
|
7854775de5 | ||
|
e62eaa6b4b | ||
|
b4cce23ef4 | ||
|
2bcaf90fc8 | ||
|
96ffbe2f84 | ||
|
6f5acee1c3 | ||
|
54e491d8bf | ||
|
ab6374e278 | ||
|
2fda4c9f67 | ||
|
5138a45b0f | ||
|
b224d4fa8a | ||
|
a552e44483 | ||
|
0cf3bba118 | ||
|
2c48ea3508 | ||
|
b9b6212b75 | ||
|
b978aaec21 | ||
|
af704681d9 | ||
|
1443ddfe6c | ||
|
54457a3e1b | ||
|
bf180e6a2c | ||
|
864a5820c9 | ||
|
4d3ca49c3f | ||
|
c49c3cf7f0 | ||
|
5d5ab57469 | ||
|
31978d8de0 | ||
|
e8eb68bf24 | ||
|
9ea08f4fed | ||
|
fe078a5c5b | ||
|
61933954f3 | ||
|
4c243638cb | ||
|
02ba04b5d8 | ||
|
4f158a4829 | ||
|
177a22df59 | ||
|
6b0ca2966e | ||
|
aadfaf7150 | ||
|
b307b9a66b | ||
|
6c1ab6002d | ||
|
9638eefc91 | ||
|
9e9c4ad587 | ||
|
ce231431b9 | ||
|
06e1e14e02 | ||
|
416e7884f5 | ||
|
d579222007 | ||
|
30243c84cd | ||
|
3557a77645 | ||
|
97be28638b | ||
|
aba0826c38 | ||
|
f032228d0e | ||
|
6cf174c5ed | ||
|
c2109d245f | ||
|
6a9745171e | ||
|
f9a68e8b23 | ||
|
6e391df5ee | ||
|
f5edca94d3 | ||
|
60046abec3 | ||
|
cafc2060b8 | ||
|
b1f45752cf | ||
|
ed17551170 | ||
|
ef5adab638 | ||
|
fb653ff99d | ||
|
78fc47a9c4 | ||
|
2a124cea61 | ||
|
4c7cc563dc | ||
|
6114af4f93 | ||
|
8c31629655 | ||
|
03c8a8edb2 | ||
|
3eeaee154f |
78 changed files with 2782 additions and 1374 deletions
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Information**
|
||||
- OS: [e.g. macOS]
|
||||
- Clash Verge Version: [e.g. 1.3.4]
|
||||
- Clash Core: [e.g. Clash or Clash Meta]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
39
.github/workflows/alpha.yml
vendored
39
.github/workflows/alpha.yml
vendored
|
@ -16,19 +16,15 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
os: [windows-latest, ubuntu-20.04, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
@ -36,12 +32,13 @@ jobs:
|
|||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Delete current release assets
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
@ -56,6 +53,7 @@ jobs:
|
|||
*.dmg
|
||||
*.msi
|
||||
*.sig
|
||||
*.exe
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
|
@ -63,23 +61,10 @@ jobs:
|
|||
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
|
||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check --force
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
|
@ -96,7 +81,7 @@ jobs:
|
|||
includeDebug: ${{ github.event.inputs.debug }}
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
yarn build
|
||||
yarn run portable
|
||||
|
|
51
.github/workflows/ci.yml
vendored
51
.github/workflows/ci.yml
vendored
|
@ -19,14 +19,10 @@ jobs:
|
|||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
@ -34,9 +30,10 @@ jobs:
|
|||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
|
@ -44,22 +41,9 @@ jobs:
|
|||
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 install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
|
@ -78,7 +62,7 @@ jobs:
|
|||
prerelease: true
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
# rebuild with env settings
|
||||
run: |
|
||||
yarn build
|
||||
|
@ -97,23 +81,16 @@ jobs:
|
|||
startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Yarn install
|
||||
run: yarn install
|
||||
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
|
||||
- name: Release updater file
|
||||
run: yarn run updater
|
||||
|
|
38
.github/workflows/test.yml
vendored
38
.github/workflows/test.yml
vendored
|
@ -35,14 +35,10 @@ jobs:
|
|||
echo ${{ github.event.inputs.os }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
@ -50,32 +46,20 @@ jobs:
|
|||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(github.event.inputs.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Yarn install and check
|
||||
run: |
|
||||
yarn install --network-timeout 1000000
|
||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
|
@ -84,3 +68,9 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: alpha
|
||||
releaseName: "Clash Verge Alpha"
|
||||
releaseBody: "Alpha Version (include debug)"
|
||||
releaseDraft: false
|
||||
includeUpdaterJson: false
|
||||
|
|
19
.github/workflows/updater.yml
vendored
19
.github/workflows/updater.yml
vendored
|
@ -8,23 +8,16 @@ jobs:
|
|||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Yarn install
|
||||
run: yarn install
|
||||
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
|
||||
- name: Release updater file
|
||||
run: yarn run updater
|
||||
|
|
58
README.md
58
README.md
|
@ -17,14 +17,66 @@ A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="h
|
|||
- Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core.
|
||||
- System proxy setting and guard.
|
||||
|
||||
## Promotion
|
||||
|
||||
[狗狗加速 —— 技术流机场 Doggygo VPN](https://dg1.top)
|
||||
|
||||
- High-performance overseas VPN, free trial, discounted packages, unlock streaming media, the world's first to support Hysteria protocol.
|
||||
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
||||
- 使用 Clash Verge 专属邀请链接注册送 15 天,每天 1G 流量免费试用:https://panel.dg1.top/#/register?code=sFCDayZf
|
||||
|
||||
<details>
|
||||
<summary>Promotion Detail</summary>
|
||||
|
||||
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
||||
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
||||
- 海外团队,无跑路风险,高达 50% 返佣
|
||||
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
|
||||
- 全球首家 Hysteria 协议机场,将在今年 10 月上线更快的 `tuic` 协议(Clash Verge 客户端最佳搭配)
|
||||
- 解锁流媒体及 ChatGPT
|
||||
- 官网:https://dg1.top
|
||||
|
||||
</details>
|
||||
|
||||
<br />
|
||||
|
||||
[EEVPN —— 海外运营机场 ※ 支持 ChatGPT](https://www.eejsq.net/#/register?code=yRr6qBO3)
|
||||
|
||||
- 年付低至 9.99 元,价格低,速度不减
|
||||
|
||||
<details>
|
||||
<summary>Promotion Detail</summary>
|
||||
|
||||
- 中国大陆 BGP 网络接入
|
||||
- IEPL 专线网络
|
||||
- 最高 2500Mbps 速率可用
|
||||
- 不限制在线客户端
|
||||
- 解锁流媒体及 ChatGPT
|
||||
- 海外运营 数据安全
|
||||
|
||||
</details>
|
||||
|
||||
## Install
|
||||
|
||||
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
||||
|
||||
- [Windows x64](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64_en-US.msi)
|
||||
- [macOS intel](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64.dmg)
|
||||
- [macOS arm](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_aarch64.dmg)
|
||||
- [Linux AppImage](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.AppImage)
|
||||
- [Linux deb](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.deb)
|
||||
- [Fedora Linux](https://github.com/zzzgydi/clash-verge/issues/352)
|
||||
|
||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
||||
|
||||
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
|
||||
|
||||
open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/Clash\ Verge.app`
|
||||
|
||||
## Development
|
||||
|
||||
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||
|
@ -36,6 +88,9 @@ yarn install
|
|||
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
|
||||
|
||||
```shell
|
||||
# force update to latest version
|
||||
# yarn run check --force
|
||||
|
||||
yarn run check
|
||||
```
|
||||
|
||||
|
@ -43,6 +98,9 @@ Then run
|
|||
|
||||
```shell
|
||||
yarn dev
|
||||
|
||||
# run it in another way if app instance exists
|
||||
yarn dev:diff
|
||||
```
|
||||
|
||||
Or you can build it
|
||||
|
|
115
UPDATELOG.md
115
UPDATELOG.md
|
@ -1,3 +1,118 @@
|
|||
## 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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.8",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
|
@ -24,7 +24,7 @@
|
|||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@mui/x-data-grid": "^5.17.11",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"ahooks": "^3.7.2",
|
||||
"axios": "^1.1.3",
|
||||
"dayjs": "1.11.5",
|
||||
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^5.0.3",
|
||||
"@tauri-apps/cli": "^1.2.3",
|
||||
"@tauri-apps/cli": "^1.3.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/lodash": "^4.14.180",
|
||||
|
|
|
@ -15,28 +15,29 @@ const SIDECAR_HOST = execSync("rustc -vV")
|
|||
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||
|
||||
/* ======= clash ======= */
|
||||
const CLASH_STORAGE_PREFIX = "https://release.dreamacro.workers.dev/";
|
||||
const CLASH_URL_PREFIX =
|
||||
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
||||
const CLASH_LATEST_DATE = "2023.03.18";
|
||||
const CLASH_LATEST_DATE = "latest";
|
||||
|
||||
const CLASH_MAP = {
|
||||
"win32-x64": "clash-windows-amd64",
|
||||
"darwin-x64": "clash-darwin-amd64",
|
||||
"darwin-arm64": "clash-darwin-arm64",
|
||||
"linux-x64": "clash-linux-amd64",
|
||||
"linux-arm64": "clash-linux-armv8",
|
||||
"linux-arm64": "clash-linux-arm64",
|
||||
};
|
||||
|
||||
/* ======= clash meta ======= */
|
||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/Clash.Meta/releases/download/`;
|
||||
const META_VERSION = "v1.14.3";
|
||||
const META_VERSION = "v1.16.0";
|
||||
|
||||
const META_MAP = {
|
||||
"win32-x64": "Clash.Meta-windows-amd64-compatible",
|
||||
"darwin-x64": "Clash.Meta-darwin-amd64",
|
||||
"darwin-arm64": "Clash.Meta-darwin-arm64",
|
||||
"linux-x64": "Clash.Meta-linux-amd64-compatible",
|
||||
"linux-arm64": "Clash.Meta-linux-arm64",
|
||||
"win32-x64": "clash.meta-windows-amd64-compatible",
|
||||
"darwin-x64": "clash.meta-darwin-amd64",
|
||||
"darwin-arm64": "clash.meta-darwin-arm64",
|
||||
"linux-x64": "clash.meta-linux-amd64-compatible",
|
||||
"linux-arm64": "clash.meta-linux-arm64",
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -69,6 +70,24 @@ function clash() {
|
|||
};
|
||||
}
|
||||
|
||||
function clashS3() {
|
||||
const name = CLASH_MAP[`${platform}-${arch}`];
|
||||
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${CLASH_STORAGE_PREFIX}${CLASH_LATEST_DATE}/${name}-${CLASH_LATEST_DATE}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "clash",
|
||||
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
};
|
||||
}
|
||||
|
||||
function clashMeta() {
|
||||
const name = META_MAP[`${platform}-${arch}`];
|
||||
const isWin = platform === "win32";
|
||||
|
@ -103,36 +122,61 @@ async function resolveSidecar(binInfo) {
|
|||
const tempExe = path.join(tempDir, exeFile);
|
||||
|
||||
await fs.mkdirp(tempDir);
|
||||
if (!(await fs.pathExists(tempZip))) await downloadFile(downloadURL, tempZip);
|
||||
try {
|
||||
if (!(await fs.pathExists(tempZip))) {
|
||||
await downloadFile(downloadURL, tempZip);
|
||||
}
|
||||
|
||||
if (zipFile.endsWith(".zip")) {
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
console.log(`[DEBUG]: ${name} entry name`, entry.entryName);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
await fs.rename(tempExe, sidecarPath);
|
||||
console.log(`[INFO]: ${name} unzip finished`);
|
||||
} else {
|
||||
// gz
|
||||
const readStream = fs.createReadStream(tempZip);
|
||||
const writeStream = fs.createWriteStream(sidecarPath);
|
||||
readStream
|
||||
.pipe(zlib.createGunzip())
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
console.log(`[INFO]: ${name} gunzip finished`);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
console.log(`[INFO]: ${name} chmod binary finished`);
|
||||
})
|
||||
.on("error", (error) => {
|
||||
console.error(`[ERROR]: ${name} gz failed`, error.message);
|
||||
throw error;
|
||||
if (zipFile.endsWith(".zip")) {
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
await fs.rename(tempExe, sidecarPath);
|
||||
console.log(`[INFO]: "${name}" unzip finished`);
|
||||
} else {
|
||||
// gz
|
||||
const readStream = fs.createReadStream(tempZip);
|
||||
const writeStream = fs.createWriteStream(sidecarPath);
|
||||
await new Promise((resolve, reject) => {
|
||||
const onError = (error) => {
|
||||
console.error(`[ERROR]: "${name}" gz failed:`, error.message);
|
||||
reject(error);
|
||||
};
|
||||
readStream
|
||||
.pipe(zlib.createGunzip().on("error", onError))
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
console.log(`[INFO]: "${name}" gunzip finished`);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
console.log(`[INFO]: "${name}" chmod binary finished`);
|
||||
resolve();
|
||||
})
|
||||
.on("error", onError);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// 需要删除文件
|
||||
await fs.remove(sidecarPath);
|
||||
throw err;
|
||||
} finally {
|
||||
// delete temp dir
|
||||
await fs.remove(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
// delete temp dir
|
||||
await fs.remove(tempDir);
|
||||
/**
|
||||
* prepare clash core
|
||||
* if the core version is not updated in time, use S3 storage as a backup.
|
||||
*/
|
||||
async function resolveClash() {
|
||||
try {
|
||||
return await resolveSidecar(clash());
|
||||
} catch {
|
||||
console.log(`[WARN]: clash core needs to be updated`);
|
||||
return await resolveSidecar(clashS3());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -242,21 +286,21 @@ const resolveUninstall = () =>
|
|||
const resolveMmdb = () =>
|
||||
resolveResource({
|
||||
file: "Country.mmdb",
|
||||
downloadURL: `https://github.com/Dreamacro/maxmind-geoip/releases/download/20221112/Country.mmdb`,
|
||||
downloadURL: `https://github.com/Dreamacro/maxmind-geoip/releases/download/20230812/Country.mmdb`,
|
||||
});
|
||||
const resolveGeosite = () =>
|
||||
resolveResource({
|
||||
file: "geosite.dat",
|
||||
downloadURL: `https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat`,
|
||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`,
|
||||
});
|
||||
const resolveGeoIP = () =>
|
||||
resolveResource({
|
||||
file: "geoip.dat",
|
||||
downloadURL: `https://github.com/Loyalsoldier/geoip/releases/latest/download/geoip.dat`,
|
||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`,
|
||||
});
|
||||
|
||||
const tasks = [
|
||||
{ name: "clash", func: () => resolveSidecar(clash()), retry: 5 },
|
||||
{ name: "clash", func: () => resolveSidecar(clashS3()), retry: 5 },
|
||||
{ name: "clash-meta", func: () => resolveSidecar(clashMeta()), retry: 5 },
|
||||
{ name: "wintun", func: resolveWintun, retry: 5, winOnly: true },
|
||||
{ name: "service", func: resolveService, retry: 5, winOnly: true },
|
||||
|
@ -277,7 +321,8 @@ async function runTask() {
|
|||
await task.func();
|
||||
break;
|
||||
} catch (err) {
|
||||
console.error(`[ERROR]: task::${task.name} try ${i} == `, err.message);
|
||||
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message);
|
||||
if (i === task.retry - 1) throw err;
|
||||
}
|
||||
}
|
||||
return runTask();
|
||||
|
|
|
@ -43,6 +43,7 @@ async function resolveUpdater() {
|
|||
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||
"darwin-aarch64": { signature: "", url: "" },
|
||||
"darwin-intel": { signature: "", url: "" },
|
||||
"darwin-x86_64": { signature: "", url: "" },
|
||||
"linux-x86_64": { signature: "", url: "" },
|
||||
"windows-x86_64": { signature: "", url: "" },
|
||||
"windows-i686": { signature: "", url: "" }, // no supported
|
||||
|
@ -68,12 +69,14 @@ async function resolveUpdater() {
|
|||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
||||
updateData.platforms.darwin.url = browser_download_url;
|
||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
||||
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
||||
}
|
||||
// darwin signature (intel)
|
||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.darwin.signature = sig;
|
||||
updateData.platforms["darwin-intel"].signature = sig;
|
||||
updateData.platforms["darwin-x86_64"].signature = sig;
|
||||
}
|
||||
|
||||
// darwin url (aarch)
|
||||
|
@ -117,10 +120,7 @@ async function resolveUpdater() {
|
|||
|
||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||
if (value.url) {
|
||||
updateDataNew.platforms[key].url = value.url.replace(
|
||||
"https://github.com/",
|
||||
"https://hub.fastgit.xyz/"
|
||||
);
|
||||
updateDataNew.platforms[key].url = "https://ghproxy.com/" + value.url;
|
||||
} else {
|
||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||
}
|
||||
|
|
1575
src-tauri/Cargo.lock
generated
1575
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -24,29 +24,37 @@ dunce = "1.0.2"
|
|||
log4rs = "1.0.0"
|
||||
nanoid = "0.4.0"
|
||||
chrono = "0.4.19"
|
||||
sysinfo = "0.28"
|
||||
sysinfo = "0.29"
|
||||
sysproxy = "0.3"
|
||||
rquickjs = "0.1.7"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
auto-launch = "0.4"
|
||||
auto-launch = "0.5"
|
||||
once_cell = "1.14.0"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.1"
|
||||
parking_lot = "0.12.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tauri = { version = "1.1.1", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
tauri-runtime-wry = { version = "0.12" }
|
||||
reqwest = { version = "0.11", features = ["json","rustls-tls"] }
|
||||
tauri = { version = "1.2.4", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
window-vibrancy = { version = "0.3.0" }
|
||||
window-shadows = { version = "0.2.0" }
|
||||
wry = { version = "0.24.3" }
|
||||
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "0.2.1"
|
||||
runas = "1.1.0"
|
||||
deelevate = "0.2.0"
|
||||
winreg = { version = "0.10", features = ["transactions"] }
|
||||
windows-sys = { version = "0.36", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
|
||||
winreg = { version = "0.50", features = ["transactions"] }
|
||||
windows-sys = { version = "0.48", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies.tauri]
|
||||
features = ["global-shortcut-all", "icon-png", "process-all", "shell-all", "system-tray", "updater", "window-all"]
|
||||
|
||||
[target.'cfg(linux)'.dependencies.tauri]
|
||||
features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "native-tls-vendored", "reqwest-native-tls-vendored"]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
|
|
BIN
src-tauri/icons/win-tray-icon-activated.png
Normal file
BIN
src-tauri/icons/win-tray-icon-activated.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/win-tray-icon.png
Normal file
BIN
src-tauri/icons/win-tray-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -229,6 +229,17 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
|
|||
wrap_err!(open::that(url))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clash_api_get_proxy_delay(
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
) -> CmdResult<clash_api::DelayRes> {
|
||||
match clash_api::get_proxy_delay(name, url).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(format!("{}", err.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod service {
|
||||
use super::*;
|
||||
|
|
|
@ -24,11 +24,23 @@ impl IClashTemp {
|
|||
pub fn template() -> Self {
|
||||
let mut map = Mapping::new();
|
||||
|
||||
map.insert("mixed-port".into(), 7890.into());
|
||||
map.insert(
|
||||
"mixed-port".into(),
|
||||
match cfg!(feature = "default-meta") {
|
||||
false => 7890.into(),
|
||||
true => 7898.into(),
|
||||
},
|
||||
);
|
||||
map.insert("log-level".into(), "info".into());
|
||||
map.insert("allow-lan".into(), false.into());
|
||||
map.insert("mode".into(), "rule".into());
|
||||
map.insert("external-controller".into(), "127.0.0.1:9090".into());
|
||||
map.insert(
|
||||
"external-controller".into(),
|
||||
match cfg!(feature = "default-meta") {
|
||||
false => "127.0.0.1:9090".into(),
|
||||
true => "127.0.0.1:9098".into(),
|
||||
},
|
||||
);
|
||||
map.insert("secret".into(), "".into());
|
||||
|
||||
Self(map)
|
||||
|
|
|
@ -26,7 +26,7 @@ macro_rules! draft_define {
|
|||
}
|
||||
|
||||
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
||||
MutexGuard::map(self.inner.lock(), |mut inner| {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
inner.1 = Some(inner.0.clone());
|
||||
}
|
||||
|
|
|
@ -190,7 +190,7 @@ impl PrfItem {
|
|||
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
||||
let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
|
||||
|
||||
let mut builder = reqwest::ClientBuilder::new().no_proxy();
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
// 使用软件自己的代理
|
||||
if self_proxy {
|
||||
|
|
|
@ -38,7 +38,7 @@ impl IProfiles {
|
|||
}
|
||||
// compatible with the old old old version
|
||||
profiles.items.as_mut().map(|items| {
|
||||
for mut item in items.iter_mut() {
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d"));
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ impl IProfiles {
|
|||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
valid: Some(vec!["dns".into()]),
|
||||
valid: Some(vec!["dns".into(), "sub-rules".into(), "unified-delay".into()]),
|
||||
items: Some(vec![]),
|
||||
..Self::default()
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ impl IProfiles {
|
|||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
|
||||
for mut each in items.iter_mut() {
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == Some(uid.clone()) {
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
|
@ -189,7 +189,7 @@ impl IProfiles {
|
|||
if let Some(items) = self.items.as_mut() {
|
||||
let some_uid = Some(uid.clone());
|
||||
|
||||
for mut each in items.iter_mut() {
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == some_uid {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
|
@ -8,6 +9,10 @@ pub struct IVerge {
|
|||
/// app listening port for app singleton
|
||||
pub app_singleton_port: Option<u16>,
|
||||
|
||||
/// app log level
|
||||
/// silent | error | warn | info | debug | trace
|
||||
pub app_log_level: Option<String>,
|
||||
|
||||
// i18n
|
||||
pub language: Option<String>,
|
||||
|
||||
|
@ -21,6 +26,9 @@ pub struct IVerge {
|
|||
/// enable traffic graph default is true
|
||||
pub traffic_graph: Option<bool>,
|
||||
|
||||
/// show memory info (only for Clash Meta)
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
|
||||
/// clash tun mode
|
||||
pub enable_tun_mode: Option<bool>,
|
||||
|
||||
|
@ -74,6 +82,14 @@ pub struct IVerge {
|
|||
|
||||
/// proxy 页面布局 列数
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
|
||||
/// 日志清理
|
||||
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
||||
pub auto_log_clean: Option<i32>,
|
||||
|
||||
/// window size and position
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_size_position: Option<Vec<f64>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
|
@ -116,6 +132,7 @@ impl IVerge {
|
|||
theme_mode: Some("system".into()),
|
||||
theme_blur: Some(false),
|
||||
traffic_graph: Some(true),
|
||||
enable_memory_usage: Some(true),
|
||||
enable_auto_launch: Some(false),
|
||||
enable_silent_start: Some(false),
|
||||
enable_system_proxy: Some(false),
|
||||
|
@ -124,6 +141,7 @@ impl IVerge {
|
|||
auto_close_connection: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
enable_clash_fields: Some(true),
|
||||
auto_log_clean: Some(3),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
@ -144,10 +162,12 @@ impl IVerge {
|
|||
};
|
||||
}
|
||||
|
||||
patch!(app_log_level);
|
||||
patch!(language);
|
||||
patch!(theme_mode);
|
||||
patch!(theme_blur);
|
||||
patch!(traffic_graph);
|
||||
patch!(enable_memory_usage);
|
||||
|
||||
patch!(enable_tun_mode);
|
||||
patch!(enable_service_mode);
|
||||
|
@ -168,6 +188,8 @@ impl IVerge {
|
|||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(enable_clash_fields);
|
||||
patch!(auto_log_clean);
|
||||
patch!(window_size_position);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
|
@ -182,4 +204,21 @@ impl IVerge {
|
|||
Err(_) => SERVER_PORT, // 这里就不log错误了
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取日志等级
|
||||
pub fn get_log_level(&self) -> LevelFilter {
|
||||
if let Some(level) = self.app_log_level.as_ref() {
|
||||
match level.to_lowercase().as_str() {
|
||||
"silent" => LevelFilter::Off,
|
||||
"error" => LevelFilter::Error,
|
||||
"warn" => LevelFilter::Warn,
|
||||
"info" => LevelFilter::Info,
|
||||
"debug" => LevelFilter::Debug,
|
||||
"trace" => LevelFilter::Trace,
|
||||
_ => LevelFilter::Info,
|
||||
}
|
||||
} else {
|
||||
LevelFilter::Info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::config::Config;
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -36,6 +37,32 @@ pub async fn patch_configs(config: &Mapping) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct DelayRes {
|
||||
delay: u64,
|
||||
}
|
||||
|
||||
/// GET /proxies/{name}/delay
|
||||
/// 获取代理延迟
|
||||
pub async fn get_proxy_delay(name: String, test_url: Option<String>) -> Result<DelayRes> {
|
||||
let (url, headers) = clash_client_info()?;
|
||||
let url = format!("{url}/proxies/{name}/delay");
|
||||
|
||||
let default_url = "http://www.gstatic.com/generate_204";
|
||||
let test_url = test_url
|
||||
.map(|s| if s.is_empty() { default_url.into() } else { s })
|
||||
.unwrap_or(default_url.into());
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client
|
||||
.get(&url)
|
||||
.headers(headers)
|
||||
.query(&[("timeout", "10000"), ("url", &test_url)]);
|
||||
let response = builder.send().await?;
|
||||
|
||||
Ok(response.json::<DelayRes>().await?)
|
||||
}
|
||||
|
||||
/// 根据clash info获取clash服务地址和请求头
|
||||
fn clash_client_info() -> Result<(String, HeaderMap)> {
|
||||
let client = { Config::clash().data().get_client_info() };
|
||||
|
|
|
@ -68,6 +68,10 @@ impl CoreManager {
|
|||
|
||||
if !output.status.success() {
|
||||
let error = clash_api::parse_check_output(output.stdout.clone());
|
||||
let error = match error.len() > 0 {
|
||||
true => error,
|
||||
false => output.stdout.clone(),
|
||||
};
|
||||
Logger::global().set_log(output.stdout);
|
||||
bail!("{error}");
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use once_cell::sync::OnceCell;
|
|||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::{AppHandle, GlobalShortcutManager};
|
||||
use tauri_runtime_wry::wry::application::accelerator::Accelerator;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
|
@ -76,6 +76,7 @@ impl Hotkey {
|
|||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
"open_dashboard" => || feat::open_dashboard(),
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
|
|
|
@ -28,11 +28,19 @@ pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
|||
let output = {
|
||||
let path = path.replace(' ', "\\ "); // 避免路径中有空格
|
||||
let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}");
|
||||
Command::new("sudo")
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
.arg(shell)
|
||||
.output()?
|
||||
|
||||
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() {
|
||||
|
|
|
@ -13,67 +13,81 @@ impl Tray {
|
|||
|
||||
let version = app_handle.package_info().version.to_string();
|
||||
|
||||
if zh {
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_window", "打开面板"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("rule_mode", "规则模式"))
|
||||
.add_item(CustomMenuItem::new("global_mode", "全局模式"))
|
||||
.add_item(CustomMenuItem::new("direct_mode", "直连模式"))
|
||||
.add_item(CustomMenuItem::new("script_mode", "脚本模式"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("system_proxy", "系统代理"))
|
||||
.add_item(CustomMenuItem::new("tun_mode", "TUN 模式"))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
"打开目录",
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_app_dir", "应用目录"))
|
||||
.add_item(CustomMenuItem::new("open_core_dir", "内核目录"))
|
||||
.add_item(CustomMenuItem::new("open_logs_dir", "日志目录")),
|
||||
))
|
||||
.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(
|
||||
"Open Dir",
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_app_dir", "App Dir"))
|
||||
.add_item(CustomMenuItem::new("open_core_dir", "Core Dir"))
|
||||
.add_item(CustomMenuItem::new("open_logs_dir", "Logs Dir")),
|
||||
))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
"More",
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
|
||||
.add_item(CustomMenuItem::new("restart_app", "Restart App"))
|
||||
.add_item(
|
||||
CustomMenuItem::new("app_version", format!("Version {version}"))
|
||||
.disabled(),
|
||||
),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"))
|
||||
macro_rules! t {
|
||||
($en: expr, $zh: expr) => {
|
||||
if zh {
|
||||
$zh
|
||||
} else {
|
||||
$en
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_window",
|
||||
t!("Dashboard", "打开面板"),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new(
|
||||
"rule_mode",
|
||||
t!("Rule Mode", "规则模式"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"global_mode",
|
||||
t!("Global Mode", "全局模式"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"direct_mode",
|
||||
t!("Direct Mode", "直连模式"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"script_mode",
|
||||
t!("Script Mode", "脚本模式"),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new(
|
||||
"system_proxy",
|
||||
t!("System Proxy", "系统代理"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"copy_env",
|
||||
t!("Copy Env", "复制环境变量"),
|
||||
))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
t!("Open Dir", "打开目录"),
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_app_dir",
|
||||
t!("App Dir", "应用目录"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_core_dir",
|
||||
t!("Core Dir", "内核目录"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_logs_dir",
|
||||
t!("Logs Dir", "日志目录"),
|
||||
)),
|
||||
))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
t!("More", "更多"),
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"restart_clash",
|
||||
t!("Restart Clash", "重启 Clash"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"restart_app",
|
||||
t!("Restart App", "重启应用"),
|
||||
))
|
||||
.add_item(
|
||||
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
|
||||
),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q"))
|
||||
}
|
||||
|
||||
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
|
||||
|
@ -107,6 +121,17 @@ impl Tray {
|
|||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let indication_icon = if *system_proxy {
|
||||
include_bytes!("../../icons/win-tray-icon-activated.png").to_vec()
|
||||
} else {
|
||||
include_bytes!("../../icons/win-tray-icon.png").to_vec()
|
||||
};
|
||||
|
||||
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
|
||||
}
|
||||
|
||||
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
|
||||
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
|
||||
|
||||
|
@ -124,12 +149,15 @@ impl Tray {
|
|||
"open_window" => resolve::create_window(app_handle),
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
||||
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
||||
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => api::process::restart(&app_handle.env()),
|
||||
"quit" => {
|
||||
let _ = resolve::save_window_size_position(app_handle, true);
|
||||
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
app_handle.exit(0);
|
||||
|
|
|
@ -21,7 +21,7 @@ pub const DEFAULT_FIELDS: [&str; 5] = [
|
|||
"rule-providers",
|
||||
];
|
||||
|
||||
pub const OTHERS_FIELDS: [&str; 29] = [
|
||||
pub const OTHERS_FIELDS: [&str; 30] = [
|
||||
"dns",
|
||||
"tun",
|
||||
"ebpf",
|
||||
|
@ -46,6 +46,7 @@ pub const OTHERS_FIELDS: [&str; 29] = [
|
|||
"listeners", // meta
|
||||
"sub-rules", // meta
|
||||
"geodata-mode", // meta
|
||||
"unified-delay", // meta
|
||||
"tcp-concurrent", // meta
|
||||
"enable-process", // meta
|
||||
"find-process-mode", // meta
|
||||
|
|
|
@ -7,8 +7,19 @@
|
|||
use crate::config::*;
|
||||
use crate::core::*;
|
||||
use crate::log_err;
|
||||
use crate::utils::resolve;
|
||||
use anyhow::{bail, Result};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use wry::application::clipboard::Clipboard;
|
||||
|
||||
// 打开面板
|
||||
pub fn open_dashboard() {
|
||||
let handle = handle::Handle::global();
|
||||
let app_handle = handle.app_handle.lock();
|
||||
if let Some(app_handle) = app_handle.as_ref() {
|
||||
resolve::create_window(app_handle);
|
||||
}
|
||||
}
|
||||
|
||||
// 重启clash
|
||||
pub fn restart_clash_core() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -66,6 +66,8 @@ fn main() -> std::io::Result<()> {
|
|||
cmds::service::check_service,
|
||||
cmds::service::install_service,
|
||||
cmds::service::uninstall_service,
|
||||
// clash api
|
||||
cmds::clash_api_get_proxy_delay
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
@ -109,6 +111,8 @@ fn main() -> std::io::Result<()> {
|
|||
match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||
|
||||
app_handle.get_window("main").map(|win| {
|
||||
let _ = win.hide();
|
||||
});
|
||||
|
@ -117,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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
|
|
|
@ -119,6 +119,15 @@ macro_rules! log_err {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace_err {
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// wrap the anyhow error
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
use crate::config::*;
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use chrono::{DateTime, Local};
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Config, Logger, Root};
|
||||
use log4rs::config::{Appender, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use std::fs;
|
||||
use std::fs::{self, DirEntry};
|
||||
use std::str::FromStr;
|
||||
use tauri::PackageInfo;
|
||||
|
||||
/// initialize this instance's log file
|
||||
|
@ -17,42 +18,126 @@ fn init_log() -> Result<()> {
|
|||
let _ = fs::create_dir_all(&log_dir);
|
||||
}
|
||||
|
||||
let log_level = Config::verge().data().get_log_level();
|
||||
if log_level == LevelFilter::Off {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||
let log_file = format!("{}.log", local_time);
|
||||
let log_file = log_dir.join(log_file);
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {M} {m}{n}";
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}";
|
||||
let log_pattern = match log_level {
|
||||
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
|
||||
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
|
||||
};
|
||||
|
||||
let encode = Box::new(PatternEncoder::new(time_format));
|
||||
let encode = Box::new(PatternEncoder::new(log_pattern));
|
||||
|
||||
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
|
||||
let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let level = LevelFilter::Debug;
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
let level = LevelFilter::Info;
|
||||
let mut logger_builder = Logger::builder();
|
||||
let mut root_builder = Root::builder();
|
||||
|
||||
let config = Config::builder()
|
||||
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
{
|
||||
logger_builder = logger_builder.appenders(["file", "stdout"]);
|
||||
if log_more {
|
||||
root_builder = root_builder.appenders(["file", "stdout"]);
|
||||
} else {
|
||||
root_builder = root_builder.appenders(["stdout"]);
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
{
|
||||
logger_builder = logger_builder.appenders(["file"]);
|
||||
if log_more {
|
||||
root_builder = root_builder.appenders(["file"]);
|
||||
}
|
||||
}
|
||||
|
||||
let (config, _) = log4rs::config::Config::builder()
|
||||
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||
.appender(Appender::builder().build("file", Box::new(tofile)))
|
||||
.logger(
|
||||
Logger::builder()
|
||||
.appenders(["file", "stdout"])
|
||||
.additive(false)
|
||||
.build("app", level),
|
||||
)
|
||||
.build(Root::builder().appender("stdout").build(LevelFilter::Info))?;
|
||||
.logger(logger_builder.additive(false).build("app", log_level))
|
||||
.build_lossy(root_builder.build(log_level));
|
||||
|
||||
log4rs::init_config(config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize all the files from resources
|
||||
/// 删除log文件
|
||||
pub fn delete_log() -> Result<()> {
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
if !log_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auto_log_clean = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.data();
|
||||
verge.auto_log_clean.clone().unwrap_or(0)
|
||||
};
|
||||
|
||||
let day = match auto_log_clean {
|
||||
1 => 7,
|
||||
2 => 30,
|
||||
3 => 90,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
log::debug!(target: "app", "try to delete log files, day: {day}");
|
||||
|
||||
// %Y-%m-%d to NaiveDateTime
|
||||
let parse_time_str = |s: &str| {
|
||||
let sa: Vec<&str> = s.split('-').collect();
|
||||
if sa.len() != 4 {
|
||||
return Err(anyhow::anyhow!("invalid time str"));
|
||||
}
|
||||
|
||||
let year = i32::from_str(sa[0])?;
|
||||
let month = u32::from_str(sa[1])?;
|
||||
let day = u32::from_str(sa[2])?;
|
||||
let time = chrono::NaiveDate::from_ymd_opt(year, month, day)
|
||||
.ok_or(anyhow::anyhow!("invalid time str"))?
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.ok_or(anyhow::anyhow!("invalid time str"))?;
|
||||
Ok(time)
|
||||
};
|
||||
|
||||
let process_file = |file: DirEntry| -> Result<()> {
|
||||
let file_name = file.file_name();
|
||||
let file_name = file_name.to_str().unwrap_or_default();
|
||||
|
||||
if file_name.ends_with(".log") {
|
||||
let now = Local::now();
|
||||
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
|
||||
let file_time = DateTime::<Local>::from_local(created_time, now.offset().clone());
|
||||
|
||||
let duration = now.signed_duration_since(file_time);
|
||||
if duration.num_days() > day {
|
||||
let file_path = file.path();
|
||||
let _ = fs::remove_file(file_path);
|
||||
log::info!(target: "app", "delete log file: {file_name}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
for file in fs::read_dir(&log_dir)? {
|
||||
if let Ok(file) = file {
|
||||
let _ = process_file(file);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize all the config files
|
||||
/// before tauri setup
|
||||
pub fn init_config() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
|
@ -60,6 +145,7 @@ pub fn init_config() -> Result<()> {
|
|||
}
|
||||
|
||||
let _ = init_log();
|
||||
let _ = delete_log();
|
||||
|
||||
crate::log_err!(dirs::app_home_dir().map(|app_dir| {
|
||||
if !app_dir.exists() {
|
||||
|
@ -97,7 +183,8 @@ pub fn init_config() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// initialize app
|
||||
/// initialize app resources
|
||||
/// after tauri setup
|
||||
pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let res_dir = dirs::app_resources_dir(package_info)?;
|
||||
|
@ -109,13 +196,47 @@ pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
|||
let _ = fs::create_dir_all(&res_dir);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
||||
|
||||
// copy the resource file
|
||||
for file in ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"].iter() {
|
||||
// if the source file is newer than the destination file, copy it over
|
||||
for file in file_list.iter() {
|
||||
let src_path = res_dir.join(file);
|
||||
let target_path = app_dir.join(file);
|
||||
if src_path.exists() {
|
||||
let _ = fs::copy(src_path, target_path);
|
||||
let dest_path = app_dir.join(file);
|
||||
|
||||
let handle_copy = || {
|
||||
match fs::copy(&src_path, &dest_path) {
|
||||
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "failed to copy resources '{file}', {err}")
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if src_path.exists() && !dest_path.exists() {
|
||||
handle_copy();
|
||||
continue;
|
||||
}
|
||||
|
||||
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());
|
||||
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());
|
||||
|
||||
match (src_modified, dest_modified) {
|
||||
(Ok(src_modified), Ok(dest_modified)) => {
|
||||
if src_modified > dest_modified {
|
||||
handle_copy();
|
||||
} else {
|
||||
log::debug!(target: "app", "skipping resource copy '{file}'");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::debug!(target: "app", "failed to get modified '{file}'");
|
||||
handle_copy();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::config::Config;
|
||||
use crate::log_err;
|
||||
use crate::{core::*, utils::init, utils::server};
|
||||
use crate::{config::Config, core::*, utils::init, utils::server};
|
||||
use crate::{log_err, trace_err};
|
||||
use anyhow::Result;
|
||||
use tauri::{App, AppHandle, Manager};
|
||||
|
||||
/// handle something when start app
|
||||
|
@ -13,12 +13,17 @@ pub fn resolve_setup(app: &mut App) {
|
|||
log_err!(init::init_resources(app.package_info()));
|
||||
|
||||
// 启动核心
|
||||
log::trace!("init config");
|
||||
log_err!(Config::init_config());
|
||||
|
||||
log::trace!("launch core");
|
||||
log_err!(CoreManager::global().init());
|
||||
|
||||
// setup a simple http server for singleton
|
||||
log::trace!("launch embed server");
|
||||
server::embed_server(app.app_handle());
|
||||
|
||||
log::trace!("init system tray");
|
||||
log_err!(tray::Tray::update_systray(&app.app_handle()));
|
||||
|
||||
let silent_start = { Config::verge().data().enable_silent_start.clone() };
|
||||
|
@ -43,22 +48,47 @@ pub fn resolve_reset() {
|
|||
/// create main window
|
||||
pub fn create_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
trace_err!(window.unminimize(), "set win unminimize");
|
||||
trace_err!(window.show(), "set win visible");
|
||||
trace_err!(window.set_focus(), "set win focus");
|
||||
return;
|
||||
}
|
||||
|
||||
let builder = tauri::window::WindowBuilder::new(
|
||||
let mut builder = tauri::window::WindowBuilder::new(
|
||||
app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WindowUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.center()
|
||||
.fullscreen(false)
|
||||
.min_inner_size(600.0, 520.0);
|
||||
|
||||
match Config::verge().latest().window_size_position.clone() {
|
||||
Some(size_pos) if size_pos.len() == 4 => {
|
||||
let size = (size_pos[0], size_pos[1]);
|
||||
let pos = (size_pos[2], size_pos[3]);
|
||||
let w = size.0.clamp(600.0, f64::INFINITY);
|
||||
let h = size.1.clamp(520.0, f64::INFINITY);
|
||||
builder = builder.inner_size(w, h).position(pos.0, pos.1);
|
||||
}
|
||||
_ => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 636.0).center();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 642.0).center();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 642.0).center();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::time::Duration;
|
||||
|
@ -68,43 +98,82 @@ pub fn create_window(app_handle: &AppHandle) {
|
|||
match builder
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.inner_size(800.0, 636.0)
|
||||
.visible(false)
|
||||
.build()
|
||||
{
|
||||
Ok(_) => {
|
||||
let app_handle = app_handle.clone();
|
||||
Ok(win) => {
|
||||
log::trace!("try to calculate the monitor size");
|
||||
let center = (|| -> Result<bool> {
|
||||
let mut center = false;
|
||||
let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?;
|
||||
let size = monitor.size();
|
||||
let pos = win.outer_position()?;
|
||||
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
let _ = set_shadow(&window, true);
|
||||
if pos.x < -400
|
||||
|| pos.x > (size.width - 200).try_into()?
|
||||
|| pos.y < -200
|
||||
|| pos.y > (size.height - 200).try_into()?
|
||||
{
|
||||
center = true;
|
||||
}
|
||||
Ok(center)
|
||||
})();
|
||||
|
||||
if center.unwrap_or(true) {
|
||||
trace_err!(win.center(), "set win center");
|
||||
}
|
||||
|
||||
log::trace!("try to create window");
|
||||
let app_handle = app_handle.clone();
|
||||
|
||||
// 加点延迟避免界面闪一下
|
||||
tauri::async_runtime::spawn(async move {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
sleep(Duration::from_millis(888)).await;
|
||||
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.unminimize();
|
||||
let _ = window.set_focus();
|
||||
trace_err!(set_shadow(&window, true), "set win shadow");
|
||||
trace_err!(window.show(), "set win visible");
|
||||
trace_err!(window.unminimize(), "set win unminimize");
|
||||
trace_err!(window.set_focus(), "set win focus");
|
||||
} else {
|
||||
log::error!(target: "app", "failed to create window, get_window is None")
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
Err(err) => log::error!(target: "app", "failed to create window, {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::log_err!(builder
|
||||
.decorations(true)
|
||||
.inner_size(800.0, 642.0)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.build());
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
crate::log_err!(builder
|
||||
.decorations(true)
|
||||
.transparent(false)
|
||||
.inner_size(800.0, 642.0)
|
||||
.build());
|
||||
crate::log_err!(builder.decorations(true).transparent(false).build());
|
||||
}
|
||||
|
||||
/// save window size and position
|
||||
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
|
||||
let win = app_handle
|
||||
.get_window("main")
|
||||
.ok_or(anyhow::anyhow!("failed to get window"))?;
|
||||
|
||||
let scale = win.scale_factor()?;
|
||||
let size = win.inner_size()?;
|
||||
let size = size.to_logical::<f64>(scale);
|
||||
let pos = win.outer_position()?;
|
||||
let pos = pos.to_logical::<f64>(scale);
|
||||
|
||||
let verge = Config::verge();
|
||||
let mut verge = verge.latest();
|
||||
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
|
||||
|
||||
if save_to_file {
|
||||
verge.save_file()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "1.3.1"
|
||||
"version": "1.3.8"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
|
@ -46,15 +46,15 @@
|
|||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "",
|
||||
"wix": {
|
||||
"language": ["zh-CN", "en-US"]
|
||||
"language": ["zh-CN", "en-US", "ru-RU"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json",
|
||||
"https://hub.fastgit.xyz/zzzgydi/clash-verge/releases/download/updater/update-proxy.json"
|
||||
"https://ghproxy.com/https://github.com/zzzgydi/clash-verge/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"
|
||||
|
|
|
@ -3,18 +3,24 @@ import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
|||
|
||||
function ErrorFallback({ error }: FallbackProps) {
|
||||
return (
|
||||
<div role="alert">
|
||||
<p>Something went wrong:(</p>
|
||||
<div role="alert" style={{ padding: 16 }}>
|
||||
<h4>Something went wrong:(</h4>
|
||||
|
||||
<pre>{error.message}</pre>
|
||||
|
||||
<details title="Error Stack">
|
||||
<summary>Error Stack</summary>
|
||||
<pre>{error.stack}</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface BaseErrorBoundaryProps {
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const BaseErrorBoundary: React.FC<BaseErrorBoundaryProps> = (props) => {
|
||||
export const BaseErrorBoundary = (props: Props) => {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
{props.children}
|
||||
|
|
104
src/components/connection/connection-detail.tsx
Normal file
104
src/components/connection/connection-detail.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import dayjs from "dayjs";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, Button, Snackbar } from "@mui/material";
|
||||
import { deleteConnection } from "@/services/api";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
export interface ConnectionDetailRef {
|
||||
open: (detail: IConnectionsItem) => void;
|
||||
}
|
||||
|
||||
export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
(props, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (detail: IConnectionsItem) => {
|
||||
if (open) return;
|
||||
setOpen(true);
|
||||
setDetail(detail);
|
||||
},
|
||||
}));
|
||||
|
||||
const onClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail data={detail} onClose={onClose} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface InnerProps {
|
||||
data: IConnectionsItem;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const { metadata, rulePayload } = data;
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||
const host = metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`;
|
||||
|
||||
const information = [
|
||||
{ label: "Host", value: host },
|
||||
{ label: "Download", value: parseTraffic(data.download).join(" ") },
|
||||
{ label: "Upload", value: parseTraffic(data.upload).join(" ") },
|
||||
{
|
||||
label: "DL Speed",
|
||||
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
label: "UL Speed",
|
||||
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{ label: "Chains", value: chains },
|
||||
{ label: "Rule", value: rule },
|
||||
{
|
||||
label: "Process",
|
||||
value: truncateStr(metadata.process || metadata.processPath),
|
||||
},
|
||||
{ label: "Time", value: dayjs(data.start).fromNow() },
|
||||
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
|
||||
{ label: "Destination IP", value: metadata.destinationIP },
|
||||
{ label: "Type", value: `${metadata.type}(${metadata.network})` },
|
||||
];
|
||||
|
||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||
|
||||
return (
|
||||
<Box sx={{ userSelect: "text" }}>
|
||||
{information.map((each) => (
|
||||
<div key={each.label}>
|
||||
<b>{each.label}</b>: <span>{each.value}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Box sx={{ textAlign: "right" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
title="Close Connection"
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -24,10 +24,11 @@ const Tag = styled("span")(({ theme }) => ({
|
|||
|
||||
interface Props {
|
||||
value: IConnectionsItem;
|
||||
onShowDetail?: () => void;
|
||||
}
|
||||
|
||||
const ConnectionItem = (props: Props) => {
|
||||
const { value } = props;
|
||||
export const ConnectionItem = (props: Props) => {
|
||||
const { value, onShowDetail } = props;
|
||||
|
||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
||||
|
||||
|
@ -44,8 +45,9 @@ const ConnectionItem = (props: Props) => {
|
|||
}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ userSelect: "text" }}
|
||||
sx={{ userSelect: "text", cursor: "pointer" }}
|
||||
primary={metadata.host || metadata.destinationIP}
|
||||
onClick={onShowDetail}
|
||||
secondary={
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
||||
<Tag sx={{ textTransform: "uppercase", color: "success" }}>
|
||||
|
@ -56,7 +58,7 @@ const ConnectionItem = (props: Props) => {
|
|||
|
||||
{!!metadata.process && <Tag>{metadata.process}</Tag>}
|
||||
|
||||
{chains.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>}
|
||||
{chains?.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>}
|
||||
|
||||
<Tag>{dayjs(start).fromNow()}</Tag>
|
||||
|
||||
|
@ -71,5 +73,3 @@ const ConnectionItem = (props: Props) => {
|
|||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionItem;
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
import dayjs from "dayjs";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
interface Props {
|
||||
connections: IConnectionsItem[];
|
||||
onShowDetail: (data: IConnectionsItem) => void;
|
||||
}
|
||||
|
||||
const ConnectionTable = (props: Props) => {
|
||||
const { connections } = props;
|
||||
export const ConnectionTable = (props: Props) => {
|
||||
const { connections, onShowDetail } = props;
|
||||
|
||||
const [columnVisible, setColumnVisible] = useState<
|
||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||
>({});
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "host",
|
||||
headerName: "Host",
|
||||
flex: 200,
|
||||
minWidth: 200,
|
||||
resizable: false,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "host", headerName: "Host", flex: 220, minWidth: 220 },
|
||||
{
|
||||
field: "download",
|
||||
headerName: "Download",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "upload",
|
||||
|
@ -34,18 +31,13 @@ const ConnectionTable = (props: Props) => {
|
|||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "dlSpeed",
|
||||
headerName: "DL Speed",
|
||||
align: "right",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "ulSpeed",
|
||||
|
@ -53,55 +45,26 @@ const ConnectionTable = (props: Props) => {
|
|||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "chains",
|
||||
headerName: "Chains",
|
||||
width: 360,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "rule",
|
||||
headerName: "Rule",
|
||||
width: 225,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "process",
|
||||
headerName: "Process",
|
||||
width: 120,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "chains", headerName: "Chains", flex: 360, minWidth: 360 },
|
||||
{ field: "rule", headerName: "Rule", flex: 300, minWidth: 250 },
|
||||
{ field: "process", headerName: "Process", flex: 480, minWidth: 480 },
|
||||
{
|
||||
field: "time",
|
||||
headerName: "Time",
|
||||
width: 120,
|
||||
flex: 120,
|
||||
minWidth: 100,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params) => dayjs(params.value).fromNow(),
|
||||
},
|
||||
{
|
||||
field: "source",
|
||||
headerName: "Source",
|
||||
width: 150,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "source", headerName: "Source", flex: 200, minWidth: 130 },
|
||||
{
|
||||
field: "destinationIP",
|
||||
headerName: "Destination IP",
|
||||
width: 125,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "type",
|
||||
headerName: "Type",
|
||||
width: 160,
|
||||
disableColumnMenu: true,
|
||||
flex: 200,
|
||||
minWidth: 130,
|
||||
},
|
||||
{ field: "type", headerName: "Type", flex: 160, minWidth: 100 },
|
||||
];
|
||||
|
||||
const connRows = useMemo(() => {
|
||||
|
@ -115,30 +78,33 @@ const ConnectionTable = (props: Props) => {
|
|||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
ulSpeed: each.curUpload,
|
||||
download: parseTraffic(each.download).join(" "),
|
||||
upload: parseTraffic(each.upload).join(" "),
|
||||
dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s",
|
||||
ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s",
|
||||
chains,
|
||||
rule,
|
||||
process: metadata.process || metadata.processPath,
|
||||
time: each.start,
|
||||
process: truncateStr(metadata.process || metadata.processPath),
|
||||
time: dayjs(each.start).fromNow(),
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
destinationIP: metadata.destinationIP,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
|
||||
connectionData: each,
|
||||
};
|
||||
});
|
||||
}, [connections]);
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
hideFooter
|
||||
rows={connRows}
|
||||
columns={columns}
|
||||
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
||||
density="compact"
|
||||
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
|
||||
hideFooter
|
||||
columnVisibilityModel={columnVisible}
|
||||
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionTable;
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
HorizontalRuleRounded,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const LayoutControl = () => {
|
||||
export const LayoutControl = () => {
|
||||
const minWidth = 40;
|
||||
|
||||
return (
|
||||
|
@ -37,5 +37,3 @@ const LayoutControl = () => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutControl;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { alpha, ListItem, ListItemButton, ListItemText } from "@mui/material";
|
|||
import { useMatch, useResolvedPath, useNavigate } from "react-router-dom";
|
||||
import type { LinkProps } from "react-router-dom";
|
||||
|
||||
const LayoutItem = (props: LinkProps) => {
|
||||
export const LayoutItem = (props: LinkProps) => {
|
||||
const { to, children } = props;
|
||||
|
||||
const resolved = useResolvedPath(to);
|
||||
|
@ -40,5 +40,3 @@ const LayoutItem = (props: LinkProps) => {
|
|||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutItem;
|
||||
|
|
|
@ -1,23 +1,30 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { ArrowDownward, ArrowUpward } from "@mui/icons-material";
|
||||
import {
|
||||
ArrowDownward,
|
||||
ArrowUpward,
|
||||
MemoryOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
|
||||
import { useLogSetup } from "./use-log-setup";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { useWebsocket } from "@/hooks/use-websocket";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
// setup the traffic
|
||||
const LayoutTraffic = () => {
|
||||
export const LayoutTraffic = () => {
|
||||
const { clashInfo } = useClashInfo();
|
||||
const { verge } = useVerge();
|
||||
|
||||
// whether hide traffic graph
|
||||
const { verge } = useVerge();
|
||||
const trafficGraph = verge?.traffic_graph ?? true;
|
||||
|
||||
const trafficRef = useRef<TrafficRef>(null);
|
||||
const [traffic, setTraffic] = useState({ up: 0, down: 0 });
|
||||
const [memory, setMemory] = useState({ inuse: 0 });
|
||||
const pageVisible = useVisibility();
|
||||
|
||||
// setup log ws during layout
|
||||
useLogSetup();
|
||||
|
@ -29,44 +36,48 @@ const LayoutTraffic = () => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!clashInfo) return;
|
||||
if (!clashInfo || !pageVisible) return;
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
connect(`ws://${server}/traffic?token=${secret}`);
|
||||
connect(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`);
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [clashInfo]);
|
||||
}, [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(() => {
|
||||
// 页面隐藏时去掉请求
|
||||
const handleVisibleChange = () => {
|
||||
if (!clashInfo) return;
|
||||
if (document.visibilityState === "visible") {
|
||||
// reconnect websocket
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
connect(`ws://${server}/traffic?token=${secret}`);
|
||||
} else {
|
||||
disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibleChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibleChange);
|
||||
};
|
||||
}, []);
|
||||
if (!clashInfo || !pageVisible || !displayMemory) return;
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
memoryWs.connect(
|
||||
`ws://${server}/memory?token=${encodeURIComponent(secret)}`
|
||||
);
|
||||
return () => memoryWs.disconnect();
|
||||
}, [clashInfo, pageVisible, displayMemory]);
|
||||
|
||||
const [up, upUnit] = parseTraffic(traffic.up);
|
||||
const [down, downUnit] = parseTraffic(traffic.down);
|
||||
const [inuse, inuseUnit] = parseTraffic(memory.inuse);
|
||||
|
||||
const iconStyle: any = {
|
||||
sx: { mr: "8px", fontSize: 16 },
|
||||
};
|
||||
const valStyle: any = {
|
||||
component: "span",
|
||||
color: "primary",
|
||||
textAlign: "center",
|
||||
sx: { flex: "1 1 54px", userSelect: "none" },
|
||||
sx: { flex: "1 1 56px", userSelect: "none" },
|
||||
};
|
||||
const unitStyle: any = {
|
||||
component: "span",
|
||||
|
@ -82,31 +93,44 @@ const LayoutTraffic = () => {
|
|||
position="relative"
|
||||
onClick={trafficRef.current?.toggleStyle}
|
||||
>
|
||||
{trafficGraph && (
|
||||
{trafficGraph && pageVisible && (
|
||||
<div style={{ width: "100%", height: 60, marginBottom: 6 }}>
|
||||
<TrafficGraph ref={trafficRef} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Box mb={1.5} display="flex" alignItems="center" whiteSpace="nowrap">
|
||||
<ArrowUpward
|
||||
sx={{ mr: 0.75, fontSize: 18 }}
|
||||
color={+up > 0 ? "primary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle}>{up}</Typography>
|
||||
<Typography {...unitStyle}>{upUnit}/s</Typography>
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="column" gap={0.75}>
|
||||
<Box display="flex" alignItems="center" whiteSpace="nowrap">
|
||||
<ArrowUpward
|
||||
{...iconStyle}
|
||||
color={+up > 0 ? "primary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle}>{up}</Typography>
|
||||
<Typography {...unitStyle}>{upUnit}/s</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" whiteSpace="nowrap">
|
||||
<ArrowDownward
|
||||
sx={{ mr: 0.75, fontSize: 18 }}
|
||||
color={+down > 0 ? "primary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle}>{down}</Typography>
|
||||
<Typography {...unitStyle}>{downUnit}/s</Typography>
|
||||
<Box display="flex" alignItems="center" whiteSpace="nowrap">
|
||||
<ArrowDownward
|
||||
{...iconStyle}
|
||||
color={+down > 0 ? "primary" : "disabled"}
|
||||
/>
|
||||
<Typography {...valStyle}>{down}</Typography>
|
||||
<Typography {...unitStyle}>{downUnit}/s</Typography>
|
||||
</Box>
|
||||
|
||||
{displayMemory && (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
whiteSpace="nowrap"
|
||||
title="Memory Usage"
|
||||
>
|
||||
<MemoryOutlined {...iconStyle} color="disabled" />
|
||||
<Typography {...valStyle}>{inuse}</Typography>
|
||||
<Typography {...unitStyle}>{inuseUnit}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutTraffic;
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { Button } from "@mui/material";
|
||||
import { checkUpdate } from "@tauri-apps/api/updater";
|
||||
import UpdateDialog from "./update-dialog";
|
||||
import { UpdateViewer } from "../setting/mods/update-viewer";
|
||||
import { DialogRef } from "../base";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const UpdateButton = (props: Props) => {
|
||||
export const UpdateButton = (props: Props) => {
|
||||
const { className } = props;
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const viewerRef = useRef<DialogRef>(null);
|
||||
|
||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
|
@ -22,21 +24,17 @@ const UpdateButton = (props: Props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<UpdateViewer ref={viewerRef} />
|
||||
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={className}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
onClick={() => viewerRef.current?.open()}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
|
||||
{dialogOpen && (
|
||||
<UpdateDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateButton;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useVerge } from "@/hooks/use-verge";
|
|||
/**
|
||||
* custom theme
|
||||
*/
|
||||
export default function useCustomTheme() {
|
||||
export const useCustomTheme = () => {
|
||||
const { verge } = useVerge();
|
||||
const { theme_mode, theme_setting } = verge ?? {};
|
||||
const [mode, setMode] = useRecoilState(atomThemeMode);
|
||||
|
@ -121,4 +121,4 @@ export default function useCustomTheme() {
|
|||
}, [mode, theme_setting]);
|
||||
|
||||
return { theme };
|
||||
}
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@ export const useLogSetup = () => {
|
|||
getClashLogs().then(setLogData);
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
connect(`ws://${server}/logs?token=${secret}`);
|
||||
connect(`ws://${server}/logs?token=${encodeURIComponent(secret)}`);
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { styled, Box } from "@mui/material";
|
||||
|
||||
const Item = styled(Box)(({ theme: { palette } }) => ({
|
||||
const Item = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||
padding: "8px 0",
|
||||
margin: "0 12px",
|
||||
lineHeight: 1.35,
|
||||
borderBottom: `1px solid ${palette.divider}`,
|
||||
fontSize: "0.875rem",
|
||||
fontFamily: typography.fontFamily,
|
||||
userSelect: "text",
|
||||
"& .time": {
|
||||
color: palette.text.secondary,
|
||||
|
|
|
@ -27,6 +27,7 @@ export const ProfileBox = styled(Box)(
|
|||
}[key]!;
|
||||
|
||||
return {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
display: "block",
|
||||
cursor: "pointer",
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
keyframes,
|
||||
MenuItem,
|
||||
Menu,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import { RefreshRounded } from "@mui/icons-material";
|
||||
import { atomLoadingCache } from "@/services/states";
|
||||
|
@ -28,13 +29,14 @@ const round = keyframes`
|
|||
|
||||
interface Props {
|
||||
selected: boolean;
|
||||
activating: boolean;
|
||||
itemData: IProfileItem;
|
||||
onSelect: (force: boolean) => void;
|
||||
onEdit: () => void;
|
||||
}
|
||||
|
||||
export const ProfileItem = (props: Props) => {
|
||||
const { selected, itemData, onSelect, onEdit } = props;
|
||||
const { selected, activating, itemData, onSelect, onEdit } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
|
@ -192,6 +194,25 @@ export const ProfileItem = (props: Props) => {
|
|||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{activating && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
top: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 2,
|
||||
zIndex: 10,
|
||||
backdropFilter: "blur(2px)",
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={20} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box position="relative">
|
||||
<Typography
|
||||
width="calc(100% - 36px)"
|
||||
|
|
|
@ -115,7 +115,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||
fileDataRef.current = null;
|
||||
props.onChange();
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message);
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -143,7 +143,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||
<BaseDialog
|
||||
open={open}
|
||||
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")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={handleClose}
|
||||
|
@ -188,7 +188,12 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Subscription URL")} />
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
label={t("Subscription URL")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
|
86
src/components/proxy/provider-button.tsx
Normal file
86
src/components/proxy/provider-button.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import dayjs from "dayjs";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { RefreshRounded } from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { getProviders, providerUpdate } from "@/services/api";
|
||||
import { BaseDialog } from "../base";
|
||||
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useSWR("getProviders", getProviders);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const hasProvider = Object.keys(data || {}).length > 0;
|
||||
|
||||
const handleUpdate = useLockFn(async (key: string) => {
|
||||
await providerUpdate(key);
|
||||
await mutate("getProxies");
|
||||
await mutate("getProviders");
|
||||
});
|
||||
|
||||
if (!hasProvider) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t("Provider")}
|
||||
</Button>
|
||||
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Proxy Provider")}
|
||||
contentSx={{ width: 400 }}
|
||||
disableOk
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(data || {}).map(([key, item]) => {
|
||||
const time = dayjs(item.updatedAt);
|
||||
return (
|
||||
<ListItem sx={{ p: 0 }} key={key}>
|
||||
<ListItemText
|
||||
primary={key}
|
||||
secondary={
|
||||
<>
|
||||
<span style={{ marginRight: "4em" }}>
|
||||
Type: {item.vehicleType}
|
||||
</span>
|
||||
<span title={time.format("YYYY-MM-DD HH:mm:ss")}>
|
||||
Updated: {time.fromNow()}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="Update Provider"
|
||||
onClick={() => handleUpdate(key)}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -137,20 +137,14 @@ export const ProxyItemMini = (props: Props) => {
|
|||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={
|
||||
delay > 500
|
||||
? "error.main"
|
||||
: delay < 100
|
||||
? "success.main"
|
||||
: "text.secondary"
|
||||
}
|
||||
color={delayManager.formatDelayColor(delay)}
|
||||
sx={({ palette }) =>
|
||||
!proxy.provider
|
||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{delay > 1e5 ? "Error" : delay > 3000 ? "Timeout" : `${delay}`}
|
||||
{delayManager.formatDelay(delay)}
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
|
@ -166,19 +160,21 @@ export const ProxyItemMini = (props: Props) => {
|
|||
);
|
||||
};
|
||||
|
||||
const Widget = styled(Box)(() => ({
|
||||
const Widget = styled(Box)(({ theme: { typography } }) => ({
|
||||
padding: "3px 6px",
|
||||
fontSize: 14,
|
||||
fontFamily: typography.fontFamily,
|
||||
borderRadius: "4px",
|
||||
}));
|
||||
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.text.secondary, 0.36),
|
||||
color: alpha(theme.palette.text.secondary, 0.42),
|
||||
borderColor: alpha(palette.text.secondary, 0.36),
|
||||
color: alpha(palette.text.secondary, 0.42),
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
fontFamily: typography.fontFamily,
|
||||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
|
|
|
@ -145,20 +145,14 @@ export const ProxyItem = (props: Props) => {
|
|||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={
|
||||
delay > 500
|
||||
? "error.main"
|
||||
: delay < 100
|
||||
? "success.main"
|
||||
: "text.secondary"
|
||||
}
|
||||
color={delayManager.formatDelayColor(delay)}
|
||||
sx={({ palette }) =>
|
||||
!proxy.provider
|
||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{delay > 1e5 ? "Error" : delay > 3000 ? "Timeout" : `${delay}ms`}
|
||||
{delayManager.formatDelay(delay)}
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ const ItemWrapper = styled("div")`
|
|||
`;
|
||||
|
||||
const HOTKEY_FUNC = [
|
||||
"open_dashboard",
|
||||
"clash_mode_rule",
|
||||
"clash_mode_global",
|
||||
"clash_mode_direct",
|
||||
|
|
80
src/components/setting/mods/layout-viewer.tsx
Normal file
80
src/components/setting/mods/layout-viewer.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { List, Switch } from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { SettingItem } from "./setting-comp";
|
||||
import { GuardState } from "./guard-state";
|
||||
|
||||
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onError = (err: any) => {
|
||||
Notice.error(err.message || err.toString());
|
||||
};
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Layout Setting")}
|
||||
contentSx={{ width: 450 }}
|
||||
disableOk
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List>
|
||||
<SettingItem label={t("Theme Blur")}>
|
||||
<GuardState
|
||||
value={verge?.theme_blur ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ theme_blur: e })}
|
||||
onGuard={(e) => patchVerge({ theme_blur: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Traffic Graph")}>
|
||||
<GuardState
|
||||
value={verge?.traffic_graph ?? true}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ traffic_graph: e })}
|
||||
onGuard={(e) => patchVerge({ traffic_graph: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Memory Usage")}>
|
||||
<GuardState
|
||||
value={verge?.enable_memory_usage ?? true}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ enable_memory_usage: e })}
|
||||
onGuard={(e) => patchVerge({ enable_memory_usage: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
</List>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
|
@ -19,22 +19,26 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [values, setValues] = useState({
|
||||
appLogLevel: "info",
|
||||
autoCloseConnection: false,
|
||||
enableClashFields: true,
|
||||
enableBuiltinEnhanced: true,
|
||||
proxyLayoutColumn: 6,
|
||||
defaultLatencyTest: "",
|
||||
autoLogClean: 0,
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setOpen(true);
|
||||
setValues({
|
||||
appLogLevel: verge?.app_log_level ?? "info",
|
||||
autoCloseConnection: verge?.auto_close_connection ?? false,
|
||||
enableClashFields: verge?.enable_clash_fields ?? true,
|
||||
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
||||
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
||||
defaultLatencyTest: verge?.default_latency_test || "",
|
||||
autoLogClean: verge?.auto_log_clean || 0,
|
||||
});
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
|
@ -43,11 +47,13 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
const onSave = useLockFn(async () => {
|
||||
try {
|
||||
await patchVerge({
|
||||
app_log_level: values.appLogLevel,
|
||||
auto_close_connection: values.autoCloseConnection,
|
||||
enable_clash_fields: values.enableClashFields,
|
||||
enable_builtin_enhanced: values.enableBuiltinEnhanced,
|
||||
proxy_layout_column: values.proxyLayoutColumn,
|
||||
default_latency_test: values.defaultLatencyTest,
|
||||
auto_log_clean: values.autoLogClean as any,
|
||||
});
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
|
@ -68,7 +74,28 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
>
|
||||
<List>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Auto Close Connections" />
|
||||
<ListItemText primary={t("App Log Level")} />
|
||||
<Select
|
||||
size="small"
|
||||
sx={{ width: 100, "> div": { py: "7.5px" } }}
|
||||
value={values.appLogLevel}
|
||||
onChange={(e) => {
|
||||
setValues((v) => ({
|
||||
...v,
|
||||
appLogLevel: e.target.value as string,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{["trace", "debug", "info", "warn", "error", "silent"].map((i) => (
|
||||
<MenuItem value={i} key={i}>
|
||||
{i[0].toUpperCase() + i.slice(1).toLowerCase()}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Auto Close Connections")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.autoCloseConnection}
|
||||
|
@ -79,7 +106,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Clash Fields Filter" />
|
||||
<ListItemText primary={t("Enable Clash Fields Filter")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.enableClashFields}
|
||||
|
@ -90,7 +117,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Enable Builtin Enhanced" />
|
||||
<ListItemText primary={t("Enable Builtin Enhanced")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.enableBuiltinEnhanced}
|
||||
|
@ -101,10 +128,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Proxy Layout Column" />
|
||||
<ListItemText primary={t("Proxy Layout Column")} />
|
||||
<Select
|
||||
size="small"
|
||||
sx={{ width: 100, "> div": { py: "7.5px" } }}
|
||||
sx={{ width: 135, "> div": { py: "7.5px" } }}
|
||||
value={values.proxyLayoutColumn}
|
||||
onChange={(e) => {
|
||||
setValues((v) => ({
|
||||
|
@ -125,7 +152,33 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
</ListItem>
|
||||
|
||||
<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
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
|
|
|
@ -11,10 +11,11 @@ interface ItemProps {
|
|||
label: ReactNode;
|
||||
extra?: ReactNode;
|
||||
children?: ReactNode;
|
||||
secondary?: ReactNode;
|
||||
}
|
||||
|
||||
export const SettingItem: React.FC<ItemProps> = (props) => {
|
||||
const { label, extra, children } = props;
|
||||
const { label, extra, children, secondary } = props;
|
||||
|
||||
const primary = !extra ? (
|
||||
label
|
||||
|
@ -27,7 +28,7 @@ export const SettingItem: React.FC<ItemProps> = (props) => {
|
|||
|
||||
return (
|
||||
<ListItem sx={{ pt: "5px", pb: "5px" }}>
|
||||
<ListItemText primary={primary} />
|
||||
<ListItemText primary={primary} secondary={secondary} />
|
||||
{children}
|
||||
</ListItem>
|
||||
);
|
||||
|
|
|
@ -1,43 +1,45 @@
|
|||
import useSWR from "swr";
|
||||
import snarkdown from "snarkdown";
|
||||
import { useMemo } from "react";
|
||||
import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, styled } from "@mui/material";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
import { relaunch } from "@tauri-apps/api/process";
|
||||
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { atomUpdateState } from "@/services/states";
|
||||
import { Notice } from "@/components/base";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const UpdateLog = styled(Box)(() => ({
|
||||
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
|
||||
}));
|
||||
|
||||
const UpdateDialog = (props: Props) => {
|
||||
const { open, onClose } = props;
|
||||
export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
|
||||
|
||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
});
|
||||
|
||||
const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const onUpdate = async () => {
|
||||
// markdown parser
|
||||
const parseContent = useMemo(() => {
|
||||
if (!updateInfo?.manifest?.body) {
|
||||
return "New Version is available";
|
||||
}
|
||||
return snarkdown(updateInfo?.manifest?.body);
|
||||
}, [updateInfo]);
|
||||
|
||||
const onUpdate = useLockFn(async () => {
|
||||
if (updateState) return;
|
||||
setUpdateState(true);
|
||||
|
||||
|
@ -49,39 +51,20 @@ const UpdateDialog = (props: Props) => {
|
|||
} finally {
|
||||
setUpdateState(false);
|
||||
}
|
||||
};
|
||||
|
||||
// markdown parser
|
||||
const parseContent = useMemo(() => {
|
||||
if (!updateInfo?.manifest?.body) {
|
||||
return "New Version is available";
|
||||
}
|
||||
return snarkdown(updateInfo?.manifest?.body);
|
||||
}, [updateInfo]);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>New Version v{updateInfo?.manifest?.version}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}>
|
||||
<UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={onClose}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
autoFocus
|
||||
variant="contained"
|
||||
disabled={updateState}
|
||||
onClick={onUpdate}
|
||||
>
|
||||
{t("Update")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={`New Version v${updateInfo?.manifest?.version}`}
|
||||
contentSx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}
|
||||
okBtn={t("Update")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onUpdate}
|
||||
>
|
||||
<UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateDialog;
|
||||
});
|
|
@ -60,7 +60,10 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
.trim();
|
||||
|
||||
url = url.replaceAll("%port", port || "9090");
|
||||
url = url.replaceAll("%secret", clashInfo.secret || "");
|
||||
url = url.replaceAll(
|
||||
"%secret",
|
||||
encodeURIComponent(clashInfo.secret || "")
|
||||
);
|
||||
}
|
||||
|
||||
await openWebUrl(url);
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
import { useRef } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { IconButton, MenuItem, Select, Typography } from "@mui/material";
|
||||
import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
|
||||
import { ArrowForward } from "@mui/icons-material";
|
||||
import { checkUpdate } from "@tauri-apps/api/updater";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { version } from "@root/package.json";
|
||||
import { DialogRef } from "@/components/base";
|
||||
import { DialogRef, Notice } from "@/components/base";
|
||||
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||
import { ThemeModeSwitch } from "./mods/theme-mode-switch";
|
||||
import { ConfigViewer } from "./mods/config-viewer";
|
||||
|
@ -19,34 +15,54 @@ import { HotkeyViewer } from "./mods/hotkey-viewer";
|
|||
import { MiscViewer } from "./mods/misc-viewer";
|
||||
import { ThemeViewer } from "./mods/theme-viewer";
|
||||
import { GuardState } from "./mods/guard-state";
|
||||
import { LayoutViewer } from "./mods/layout-viewer";
|
||||
import { UpdateViewer } from "./mods/update-viewer";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
interface Props {
|
||||
onError?: (err: Error) => void;
|
||||
}
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
const SettingVerge = ({ onError }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||
|
||||
const { theme_mode, theme_blur, traffic_graph, language } = verge ?? {};
|
||||
const { theme_mode, language } = verge ?? {};
|
||||
|
||||
const configRef = useRef<DialogRef>(null);
|
||||
const hotkeyRef = useRef<DialogRef>(null);
|
||||
const miscRef = useRef<DialogRef>(null);
|
||||
const themeRef = useRef<DialogRef>(null);
|
||||
const layoutRef = useRef<DialogRef>(null);
|
||||
const updateRef = useRef<DialogRef>(null);
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
};
|
||||
|
||||
const onCheckUpdate = useLockFn(async () => {
|
||||
try {
|
||||
const info = await checkUpdate();
|
||||
if (!info?.shouldUpdate) {
|
||||
Notice.success("No Updates Available");
|
||||
} else {
|
||||
updateRef.current?.open();
|
||||
}
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingList title={t("Verge Setting")}>
|
||||
<ThemeViewer ref={themeRef} />
|
||||
<ConfigViewer ref={configRef} />
|
||||
<HotkeyViewer ref={hotkeyRef} />
|
||||
<MiscViewer ref={miscRef} />
|
||||
<LayoutViewer ref={layoutRef} />
|
||||
<UpdateViewer ref={updateRef} />
|
||||
|
||||
<SettingItem label={t("Language")}>
|
||||
<GuardState
|
||||
|
@ -59,6 +75,7 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
<Select size="small" sx={{ width: 100, "> div": { py: "7.5px" } }}>
|
||||
<MenuItem value="zh">中文</MenuItem>
|
||||
<MenuItem value="en">English</MenuItem>
|
||||
<MenuItem value="ru">Русский</MenuItem>
|
||||
</Select>
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
@ -74,30 +91,26 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Theme Blur")}>
|
||||
<GuardState
|
||||
value={theme_blur ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ theme_blur: e })}
|
||||
onGuard={(e) => patchVerge({ theme_blur: e })}
|
||||
<SettingItem label={t("Theme Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => themeRef.current?.open()}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Traffic Graph")}>
|
||||
<GuardState
|
||||
value={traffic_graph ?? true}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ traffic_graph: e })}
|
||||
onGuard={(e) => patchVerge({ traffic_graph: e })}
|
||||
<SettingItem label={t("Layout Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => layoutRef.current?.open()}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Miscellaneous")}>
|
||||
|
@ -111,17 +124,6 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Theme Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => themeRef.current?.open()}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Hotkey Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
|
@ -177,6 +179,19 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
{!(OS === "windows" && WIN_PORTABLE) && (
|
||||
<SettingItem label={t("Check for Updates")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={onCheckUpdate}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem label={t("Verge Version")}>
|
||||
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
|
||||
</SettingItem>
|
||||
|
|
|
@ -32,7 +32,7 @@ export const useProfiles = () => {
|
|||
if (!profileData || !proxiesData) return;
|
||||
|
||||
const current = profileData.items?.find(
|
||||
(e) => e.uid === profileData.current
|
||||
(e) => e && e.uid === profileData.current
|
||||
);
|
||||
|
||||
if (!current) return;
|
||||
|
@ -49,15 +49,10 @@ export const useProfiles = () => {
|
|||
const { global, groups } = proxiesData;
|
||||
|
||||
[global, ...groups].forEach(({ type, name, now }) => {
|
||||
if (!now || (type !== "Selector" && type !== "Fallback")) return;
|
||||
if (!now || type !== "Selector") return;
|
||||
if (selectedMap[name] != null && selectedMap[name] !== now) {
|
||||
hasChange = true;
|
||||
updateProxy(name, selectedMap[name]);
|
||||
console.log({
|
||||
name,
|
||||
now,
|
||||
select: selectedMap[name],
|
||||
});
|
||||
}
|
||||
newSelected.push({ name, now: selectedMap[name] });
|
||||
});
|
||||
|
@ -70,7 +65,7 @@ export const useProfiles = () => {
|
|||
|
||||
return {
|
||||
profiles,
|
||||
current: profiles?.items?.find((p) => p.uid === profiles.current),
|
||||
current: profiles?.items?.find((p) => p && p.uid === profiles.current),
|
||||
activateSelected,
|
||||
patchProfiles,
|
||||
patchCurrent,
|
||||
|
|
27
src/hooks/use-visibility.ts
Normal file
27
src/hooks/use-visibility.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useVisibility = () => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
setVisible(document.visibilityState === "visible");
|
||||
};
|
||||
|
||||
const handleFocus = () => setVisible(true);
|
||||
const handleClick = () => setVisible(true);
|
||||
|
||||
handleVisibilityChange();
|
||||
document.addEventListener("focus", handleFocus);
|
||||
document.addEventListener("pointerdown", handleClick);
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("focus", handleFocus);
|
||||
document.removeEventListener("pointerdown", handleClick);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return visible;
|
||||
};
|
|
@ -2,12 +2,13 @@ import { useRef } from "react";
|
|||
|
||||
export type WsMsgFn = (event: MessageEvent<any>) => void;
|
||||
|
||||
interface Options {
|
||||
export interface WsOptions {
|
||||
errorCount?: number; // default is 5
|
||||
retryInterval?: number; // default is 2500
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
export const useWebsocket = (onMessage: WsMsgFn, options?: Options) => {
|
||||
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const timerRef = useRef<any>(null);
|
||||
|
||||
|
@ -38,6 +39,9 @@ export const useWebsocket = (onMessage: WsMsgFn, options?: Options) => {
|
|||
|
||||
if (errorCount >= 0) {
|
||||
timerRef.current = setTimeout(connectHelper, 2500);
|
||||
} else {
|
||||
disconnect();
|
||||
options?.onError?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -55,6 +55,8 @@
|
|||
"Descriptions": "Descriptions",
|
||||
"Subscription URL": "Subscription URL",
|
||||
"Update Interval": "Update Interval",
|
||||
"Use System Proxy": "Use System Proxy",
|
||||
"Use Clash Proxy": "Use Clash Proxy",
|
||||
|
||||
"Settings": "Settings",
|
||||
"Clash Setting": "Clash Setting",
|
||||
|
@ -64,6 +66,7 @@
|
|||
"IPv6": "IPv6",
|
||||
"Log Level": "Log Level",
|
||||
"Mixed Port": "Mixed Port",
|
||||
"External": "External",
|
||||
"Clash Core": "Clash Core",
|
||||
"Tun Mode": "Tun Mode",
|
||||
"Service Mode": "Service Mode",
|
||||
|
@ -78,12 +81,16 @@
|
|||
"Theme Mode": "Theme Mode",
|
||||
"Theme Blur": "Theme Blur",
|
||||
"Theme Setting": "Theme Setting",
|
||||
"Layout Setting": "Layout Setting",
|
||||
"Miscellaneous": "Miscellaneous",
|
||||
"Hotkey Setting": "Hotkey Setting",
|
||||
"Traffic Graph": "Traffic Graph",
|
||||
"Memory Usage": "Memory Usage",
|
||||
"Language": "Language",
|
||||
"Open App Dir": "Open App Dir",
|
||||
"Open Core Dir": "Open Core Dir",
|
||||
"Open Logs Dir": "Open Logs Dir",
|
||||
"Check for Updates": "Check for Updates",
|
||||
"Verge Version": "Verge Version",
|
||||
"theme.light": "Light",
|
||||
"theme.dark": "Dark",
|
||||
|
@ -97,6 +104,11 @@
|
|||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
|
||||
"Default": "Default",
|
||||
"Download Speed": "Download Speed",
|
||||
"Upload Speed": "Upload Speed",
|
||||
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"clash_mode_rule": "Rule Mode",
|
||||
"clash_mode_global": "Global Mode",
|
||||
"clash_mode_direct": "Direct Mode",
|
||||
|
@ -106,5 +118,18 @@
|
|||
"disable_system_proxy": "Disable System Proxy",
|
||||
"toggle_tun_mode": "Toggle Tun Mode",
|
||||
"enable_tun_mode": "Enable Tun Mode",
|
||||
"disable_tun_mode": "Disable Tun Mode"
|
||||
"disable_tun_mode": "Disable Tun Mode",
|
||||
|
||||
"App Log Level": "App Log Level",
|
||||
"Auto Close Connections": "Auto Close Connections",
|
||||
"Enable Clash Fields Filter": "Enable Clash Fields Filter",
|
||||
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
|
||||
"Proxy Layout Column": "Proxy Layout Column",
|
||||
"Default Latency Test": "Default Latency Test",
|
||||
|
||||
"Auto Log Clean": "Auto Log Clean",
|
||||
"Never Clean": "Never Clean",
|
||||
"Retain 7 Days": "Retain 7 Days",
|
||||
"Retain 30 Days": "Retain 30 Days",
|
||||
"Retain 90 Days": "Retain 90 Days"
|
||||
}
|
||||
|
|
111
src/locales/ru.json
Normal file
111
src/locales/ru.json
Normal file
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"Label-Proxies": "Прокси",
|
||||
"Label-Profiles": "Профили",
|
||||
"Label-Connections": "Соединения",
|
||||
"Label-Logs": "Логи",
|
||||
"Label-Rules": "Правила",
|
||||
"Label-Settings": "Настройки",
|
||||
|
||||
"Connections": "Соединения",
|
||||
"Logs": "Логи",
|
||||
"Clear": "Очистить",
|
||||
"Proxies": "Прокси",
|
||||
"Proxy Groups": "Группы прокси",
|
||||
"rule": "правила",
|
||||
"global": "глобальный",
|
||||
"direct": "прямой",
|
||||
"script": "скриптовый",
|
||||
|
||||
"Profiles": "Профили",
|
||||
"Profile URL": "URL профиля",
|
||||
"Import": "Импорт",
|
||||
"New": "Новый",
|
||||
"Create Profile": "Создать профиль",
|
||||
"Choose File": "Выбрать файл",
|
||||
"Close All": "Закрыть всё",
|
||||
"Select": "Выбрать",
|
||||
"Edit Info": "Изменить информацию",
|
||||
"Edit File": "Изменить файл",
|
||||
"Open File": "Открыть файл",
|
||||
"Update": "Обновить",
|
||||
"Update(Proxy)": "Обновить (прокси)",
|
||||
"Delete": "Удалить",
|
||||
"Enable": "Включить",
|
||||
"Disable": "Отключить",
|
||||
"Refresh": "Обновить",
|
||||
"To Top": "Наверх",
|
||||
"To End": "Вниз",
|
||||
"Update All Profiles": "Обновить все профили",
|
||||
"View Runtime Config": "Просмотреть используемый конфиг",
|
||||
"Reactivate Profiles": "Реактивировать профили",
|
||||
|
||||
"Location": "Местоположение",
|
||||
"Delay check": "Проверка задержки",
|
||||
"Sort by default": "Сортировать по умолчанию",
|
||||
"Sort by delay": "Сортировать по задержке",
|
||||
"Sort by name": "Сортировать по названию",
|
||||
"Delay check URL": "URL проверки задержки",
|
||||
"Proxy detail": "Подробности о прокси",
|
||||
"Filter": "Фильтр",
|
||||
"Filter conditions": "Условия фильтрации",
|
||||
"Refresh profiles": "Обновить профили",
|
||||
|
||||
"Type": "Тип",
|
||||
"Name": "Название",
|
||||
"Descriptions": "Описания",
|
||||
"Subscription URL": "URL подписки",
|
||||
"Update Interval": "Интервал обновления",
|
||||
|
||||
"Settings": "Настройки",
|
||||
"Clash Setting": "Настройки Clash",
|
||||
"System Setting": "Настройки системы",
|
||||
"Verge Setting": "Настройки Verge",
|
||||
"Allow Lan": "Разрешить локальную сеть",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "Уровень логов",
|
||||
"Mixed Port": "Смешанный порт",
|
||||
"Clash Core": "Ядро Clash",
|
||||
"Tun Mode": "Режим туннеля",
|
||||
"Service Mode": "Режим сервиса",
|
||||
"Auto Launch": "Автозапуск",
|
||||
"Silent Start": "Тихий запуск",
|
||||
"System Proxy": "Системный прокси",
|
||||
"System Proxy Setting": "Настройка системного прокси",
|
||||
"Proxy Guard": "Защита прокси",
|
||||
"Guard Duration": "Период защиты",
|
||||
"Proxy Bypass": "Игнорирование прокси",
|
||||
"Current System Proxy": "Текущий системный прокси",
|
||||
"Theme Mode": "Режим темы",
|
||||
"Theme Blur": "Размытие темы",
|
||||
"Theme Setting": "Настройка темы",
|
||||
"Hotkey Setting": "Настройка клавиатурных сокращений",
|
||||
"Traffic Graph": "График трафика",
|
||||
"Language": "Язык",
|
||||
"Open App Dir": "Открыть папку приложения",
|
||||
"Open Core Dir": "Открыть папку ядра",
|
||||
"Open Logs Dir": "Открыть папку логов",
|
||||
"Verge Version": "Версия Verge",
|
||||
"theme.light": "Светлая",
|
||||
"theme.dark": "Тёмная",
|
||||
"theme.system": "Системная",
|
||||
"Clash Field": "Используемые настройки Clash",
|
||||
"Runtime Config": "Используемый конфиг",
|
||||
"ReadOnly": "Только для чтения",
|
||||
"Restart": "Перезапуск",
|
||||
|
||||
"Back": "Назад",
|
||||
"Save": "Сохранить",
|
||||
"Cancel": "Отмена",
|
||||
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"clash_mode_rule": "Режим правил",
|
||||
"clash_mode_global": "Глобальный режим",
|
||||
"clash_mode_direct": "Прямой режим",
|
||||
"clash_mode_script": "Скриптовый режим",
|
||||
"toggle_system_proxy": "Переключить режим системного прокси",
|
||||
"enable_system_proxy": "Включить системный прокси",
|
||||
"disable_system_proxy": "Отключить системный прокси",
|
||||
"toggle_tun_mode": "Переключить режим туннеля",
|
||||
"enable_tun_mode": "Включить режим туннеля",
|
||||
"disable_tun_mode": "Отключить режим туннеля"
|
||||
}
|
|
@ -80,14 +80,17 @@
|
|||
"Current System Proxy": "当前系统代理",
|
||||
"Theme Mode": "主题模式",
|
||||
"Theme Blur": "背景模糊",
|
||||
"Miscellaneous": "杂项设置",
|
||||
"Theme Setting": "主题设置",
|
||||
"Layout Setting": "界面设置",
|
||||
"Miscellaneous": "杂项设置",
|
||||
"Hotkey Setting": "热键设置",
|
||||
"Traffic Graph": "流量图显",
|
||||
"Memory Usage": "内存使用",
|
||||
"Language": "语言设置",
|
||||
"Open App Dir": "应用目录",
|
||||
"Open Core Dir": "内核目录",
|
||||
"Open Logs Dir": "日志目录",
|
||||
"Check for Updates": "检查更新",
|
||||
"Verge Version": "应用版本",
|
||||
"theme.light": "浅色",
|
||||
"theme.dark": "深色",
|
||||
|
@ -105,6 +108,7 @@
|
|||
"Download Speed": "下载速度",
|
||||
"Upload Speed": "上传速度",
|
||||
|
||||
"open_dashboard": "打开面板",
|
||||
"clash_mode_rule": "规则模式",
|
||||
"clash_mode_global": "全局模式",
|
||||
"clash_mode_direct": "直连模式",
|
||||
|
@ -114,5 +118,18 @@
|
|||
"disable_system_proxy": "关闭系统代理",
|
||||
"toggle_tun_mode": "切换Tun模式",
|
||||
"enable_tun_mode": "开启Tun模式",
|
||||
"disable_tun_mode": "关闭Tun模式"
|
||||
"disable_tun_mode": "关闭Tun模式",
|
||||
|
||||
"App Log Level": "App日志等级",
|
||||
"Auto Close Connections": "自动关闭连接",
|
||||
"Enable Clash Fields Filter": "开启Clash字段过滤",
|
||||
"Enable Builtin Enhanced": "开启内建增强功能",
|
||||
"Proxy Layout Column": "代理页布局列数",
|
||||
"Default Latency Test": "默认测试链接",
|
||||
|
||||
"Auto Log Clean": "自动清理日志",
|
||||
"Never Clean": "不清理",
|
||||
"Retain 7 Days": "保留7天",
|
||||
"Retain 30 Days": "保留30天",
|
||||
"Retain 90 Days": "保留90天"
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import React from "react";
|
|||
import { createRoot } from "react-dom/client";
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { BaseErrorBoundary } from "./components/base";
|
||||
import Layout from "./pages/_layout";
|
||||
import "./services/i18n";
|
||||
|
||||
|
@ -26,9 +27,11 @@ if (!container) {
|
|||
createRoot(container).render(
|
||||
<React.StrictMode>
|
||||
<RecoilRoot>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
<BaseErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</BaseErrorBoundary>
|
||||
</RecoilRoot>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
@ -13,12 +13,13 @@ import { getAxios } from "@/services/api";
|
|||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { ReactComponent as LogoSvg } from "@/assets/image/logo.svg";
|
||||
import { BaseErrorBoundary, Notice } from "@/components/base";
|
||||
import LayoutItem from "@/components/layout/layout-item";
|
||||
import LayoutControl from "@/components/layout/layout-control";
|
||||
import LayoutTraffic from "@/components/layout/layout-traffic";
|
||||
import UpdateButton from "@/components/layout/update-button";
|
||||
import useCustomTheme from "@/components/layout/use-custom-theme";
|
||||
import { LayoutItem } from "@/components/layout/layout-item";
|
||||
import { LayoutControl } from "@/components/layout/layout-control";
|
||||
import { LayoutTraffic } from "@/components/layout/layout-traffic";
|
||||
import { UpdateButton } from "@/components/layout/update-button";
|
||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/zh-cn";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
@ -47,6 +48,7 @@ const Layout = () => {
|
|||
mutate("getProxies");
|
||||
mutate("getVersion");
|
||||
mutate("getClashConfig");
|
||||
mutate("getProviders");
|
||||
});
|
||||
|
||||
// update the verge config
|
||||
|
@ -87,7 +89,17 @@ const Layout = () => {
|
|||
}}
|
||||
onContextMenu={(e) => {
|
||||
// only prevent it on Windows
|
||||
if (OS === "windows") e.preventDefault();
|
||||
const validList = ["input", "textarea"];
|
||||
const target = e.currentTarget;
|
||||
if (
|
||||
OS === "windows" &&
|
||||
!(
|
||||
validList.includes(target.tagName.toLowerCase()) ||
|
||||
target.isContentEditable
|
||||
)
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
sx={[
|
||||
({ palette }) => ({
|
||||
|
@ -125,13 +137,19 @@ const Layout = () => {
|
|||
)}
|
||||
|
||||
<div className="the-content">
|
||||
<BaseErrorBoundary>
|
||||
<Routes>
|
||||
{routers.map(({ label, link, ele: Ele }) => (
|
||||
<Route key={label} path={link} element={<Ele />} />
|
||||
))}
|
||||
</Routes>
|
||||
</BaseErrorBoundary>
|
||||
<Routes>
|
||||
{routers.map(({ label, link, ele: Ele }) => (
|
||||
<Route
|
||||
key={label}
|
||||
path={link}
|
||||
element={
|
||||
<BaseErrorBoundary key={label}>
|
||||
<Ele />
|
||||
</BaseErrorBoundary>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
Box,
|
||||
|
@ -18,8 +18,12 @@ import { atomConnectionSetting } from "@/services/states";
|
|||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { BaseEmpty, BasePage } from "@/components/base";
|
||||
import { useWebsocket } from "@/hooks/use-websocket";
|
||||
import ConnectionItem from "@/components/connection/connection-item";
|
||||
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: [] };
|
||||
|
||||
|
@ -56,15 +60,16 @@ const ConnectionsPage = () => {
|
|||
|
||||
const { connect, disconnect } = useWebsocket(
|
||||
(event) => {
|
||||
// meta v1.15.0 出现data.connections为null的情况
|
||||
const data = JSON.parse(event.data) as IConnections;
|
||||
// 尽量与前一次connections的展示顺序保持一致
|
||||
setConnData((old) => {
|
||||
const oldConn = old.connections;
|
||||
const maxLen = data.connections.length;
|
||||
const maxLen = data.connections?.length;
|
||||
|
||||
const connections: typeof oldConn = [];
|
||||
|
||||
const rest = data.connections.filter((each) => {
|
||||
const rest = (data.connections || []).filter((each) => {
|
||||
const index = oldConn.findIndex((o) => o.id === each.id);
|
||||
|
||||
if (index >= 0 && index < maxLen) {
|
||||
|
@ -96,7 +101,7 @@ const ConnectionsPage = () => {
|
|||
if (!clashInfo) return;
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
connect(`ws://${server}/connections?token=${secret}`);
|
||||
connect(`ws://${server}/connections?token=${encodeURIComponent(secret)}`);
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
|
@ -105,6 +110,8 @@ const ConnectionsPage = () => {
|
|||
|
||||
const onCloseAll = useLockFn(closeAllConnections);
|
||||
|
||||
const detailRef = useRef<ConnectionDetailRef>(null!);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Connections")}
|
||||
|
@ -185,14 +192,24 @@ const ConnectionsPage = () => {
|
|||
{filterConn.length === 0 ? (
|
||||
<BaseEmpty text="No Connections" />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable connections={filterConn} />
|
||||
<ConnectionTable
|
||||
connections={filterConn}
|
||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||
/>
|
||||
) : (
|
||||
<Virtuoso
|
||||
data={filterConn}
|
||||
itemContent={(index, item) => <ConnectionItem value={item} />}
|
||||
itemContent={(index, item) => (
|
||||
<ConnectionItem
|
||||
value={item}
|
||||
onShowDetail={() => detailRef.current?.open(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ConnectionDetail ref={detailRef} />
|
||||
</Paper>
|
||||
</BasePage>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,8 @@ import { useLockFn } from "ahooks";
|
|||
import { useSetRecoilState } from "recoil";
|
||||
import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material";
|
||||
import {
|
||||
ClearRounded,
|
||||
ContentCopyRounded,
|
||||
LocalFireDepartmentRounded,
|
||||
RefreshRounded,
|
||||
TextSnippetOutlined,
|
||||
|
@ -35,6 +37,7 @@ const ProfilePage = () => {
|
|||
|
||||
const [url, setUrl] = useState("");
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [activating, setActivating] = useState("");
|
||||
|
||||
const {
|
||||
profiles = {},
|
||||
|
@ -60,11 +63,12 @@ const ProfilePage = () => {
|
|||
const type1 = ["local", "remote"];
|
||||
const type2 = ["merge", "script"];
|
||||
|
||||
const regularItems = items.filter((i) => type1.includes(i.type!));
|
||||
const restItems = items.filter((i) => type2.includes(i.type!));
|
||||
const regularItems = items.filter((i) => i && type1.includes(i.type!));
|
||||
const restItems = items.filter((i) => i && type2.includes(i.type!));
|
||||
const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
|
||||
const enhanceItems = chain
|
||||
.map((i) => restMap[i]!)
|
||||
.filter(Boolean)
|
||||
.concat(restItems.filter((i) => !chain.includes(i.uid)));
|
||||
|
||||
return { regularItems, enhanceItems };
|
||||
|
@ -99,6 +103,8 @@ const ProfilePage = () => {
|
|||
|
||||
const onSelect = useLockFn(async (current: string, force: boolean) => {
|
||||
if (!force && current === profiles.current) return;
|
||||
// 避免大多数情况下loading态闪烁
|
||||
const reset = setTimeout(() => setActivating(current), 100);
|
||||
try {
|
||||
await patchProfiles({ current });
|
||||
mutateLogs();
|
||||
|
@ -107,6 +113,9 @@ const ProfilePage = () => {
|
|||
Notice.success("Refresh clash config", 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString(), 4000);
|
||||
} finally {
|
||||
clearTimeout(reset);
|
||||
setActivating("");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -188,6 +197,11 @@ const ProfilePage = () => {
|
|||
});
|
||||
});
|
||||
|
||||
const onCopyLink = async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) setUrl(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Profiles")}
|
||||
|
@ -234,6 +248,28 @@ const ProfilePage = () => {
|
|||
onChange={(e) => setUrl(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
placeholder={t("Profile URL")}
|
||||
InputProps={{
|
||||
sx: { pr: 1 },
|
||||
endAdornment: !url ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ p: 0.5 }}
|
||||
title={t("Paste")}
|
||||
onClick={onCopyLink}
|
||||
>
|
||||
<ContentCopyRounded fontSize="inherit" />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ p: 0.5 }}
|
||||
title={t("Clear")}
|
||||
onClick={() => setUrl("")}
|
||||
>
|
||||
<ClearRounded fontSize="inherit" />
|
||||
</IconButton>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={!url || disabled}
|
||||
|
@ -258,6 +294,7 @@ const ProfilePage = () => {
|
|||
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
|
||||
<ProfileItem
|
||||
selected={profiles.current === item.uid}
|
||||
activating={activating === item.uid}
|
||||
itemData={item}
|
||||
onSelect={(f) => onSelect(item.uid, f)}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
|
|
|
@ -2,12 +2,17 @@ import useSWR from "swr";
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, ButtonGroup, Paper } from "@mui/material";
|
||||
import { getClashConfig, updateConfigs } from "@/services/api";
|
||||
import { Box, Button, ButtonGroup, Paper } from "@mui/material";
|
||||
import {
|
||||
closeAllConnections,
|
||||
getClashConfig,
|
||||
updateConfigs,
|
||||
} from "@/services/api";
|
||||
import { patchClashConfig } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BasePage } from "@/components/base";
|
||||
import { ProxyGroups } from "@/components/proxy/proxy-groups";
|
||||
import { ProviderButton } from "@/components/proxy/provider-button";
|
||||
|
||||
const ProxyPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -26,9 +31,13 @@ const ProxyPage = () => {
|
|||
return ["rule", "global", "direct", "script"];
|
||||
}, [verge?.clash_core]);
|
||||
|
||||
const curMode = clashConfig?.mode.toLowerCase();
|
||||
const curMode = clashConfig?.mode?.toLowerCase();
|
||||
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
// 断开连接
|
||||
if (mode !== curMode && verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
}
|
||||
await updateConfigs({ mode });
|
||||
await patchClashConfig({ mode });
|
||||
mutateClash();
|
||||
|
@ -45,18 +54,22 @@ const ProxyPage = () => {
|
|||
contentStyle={{ height: "100%" }}
|
||||
title={t("Proxy Groups")}
|
||||
header={
|
||||
<ButtonGroup size="small">
|
||||
{modeList.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={mode === curMode ? "contained" : "outlined"}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{t(mode)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ProviderButton />
|
||||
|
||||
<ButtonGroup size="small">
|
||||
{modeList.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={mode === curMode ? "contained" : "outlined"}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{t(mode)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Paper } from "@mui/material";
|
||||
import { IconButton, Paper } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BasePage, Notice } from "@/components/base";
|
||||
import { GitHub } from "@mui/icons-material";
|
||||
import { openWebUrl } from "@/services/cmds";
|
||||
import SettingVerge from "@/components/setting/setting-verge";
|
||||
import SettingClash from "@/components/setting/setting-clash";
|
||||
import SettingSystem from "@/components/setting/setting-system";
|
||||
|
@ -12,8 +15,24 @@ const SettingPage = () => {
|
|||
Notice.error(err?.message || err.toString());
|
||||
};
|
||||
|
||||
const toGithubRepo = useLockFn(() => {
|
||||
return openWebUrl("https://github.com/zzzgydi/clash-verge");
|
||||
});
|
||||
|
||||
return (
|
||||
<BasePage title={t("Settings")}>
|
||||
<BasePage
|
||||
title={t("Settings")}
|
||||
header={
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="@zzzgydi/clash-verge"
|
||||
onClick={toGithubRepo}
|
||||
>
|
||||
<GitHub fontSize="inherit" />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Paper sx={{ borderRadius: 1, boxShadow: 2, mb: 3 }}>
|
||||
<SettingClash onError={onError} />
|
||||
</Paper>
|
||||
|
|
|
@ -65,7 +65,7 @@ export const getRules = async () => {
|
|||
/// Get Proxy delay
|
||||
export const getProxyDelay = async (name: string, url?: string) => {
|
||||
const params = {
|
||||
timeout: 5000,
|
||||
timeout: 10000,
|
||||
url: url || "http://www.gstatic.com/generate_204",
|
||||
};
|
||||
const instance = await getAxios();
|
||||
|
@ -169,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 () => {
|
||||
const instance = await getAxios();
|
||||
const result = await instance.get("/connections");
|
||||
|
|
|
@ -153,6 +153,11 @@ export async function openWebUrl(url: string) {
|
|||
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
|
||||
|
||||
export async function checkService() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getProxyDelay } from "./api";
|
||||
import { cmdGetProxyDelay } from "./cmds";
|
||||
|
||||
const hashKey = (name: string, group: string) => `${group ?? ""}::${name}`;
|
||||
|
||||
|
@ -74,7 +74,7 @@ class DelayManager {
|
|||
|
||||
try {
|
||||
const url = this.getUrl(group);
|
||||
const result = await getProxyDelay(name, url);
|
||||
const result = await cmdGetProxyDelay(name, url);
|
||||
delay = result.delay;
|
||||
} catch {
|
||||
delay = 1e6; // error
|
||||
|
@ -84,7 +84,7 @@ class DelayManager {
|
|||
return delay;
|
||||
}
|
||||
|
||||
async checkListDelay(nameList: string[], group: string, concurrency = 6) {
|
||||
async checkListDelay(nameList: string[], group: string, concurrency = 36) {
|
||||
const names = nameList.filter(Boolean);
|
||||
// 设置正在延迟测试中
|
||||
names.forEach((name) => this.setDelay(name, group, -2));
|
||||
|
@ -107,6 +107,21 @@ class DelayManager {
|
|||
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();
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import en from "@/locales/en.json";
|
||||
import ru from "@/locales/ru.json";
|
||||
import zh from "@/locales/zh.json";
|
||||
|
||||
const resources = {
|
||||
en: { translation: en },
|
||||
ru: { translation: ru },
|
||||
zh: { translation: zh },
|
||||
};
|
||||
|
||||
|
|
3
src/services/types.d.ts
vendored
3
src/services/types.d.ts
vendored
|
@ -153,11 +153,13 @@ interface IProfilesConfig {
|
|||
}
|
||||
|
||||
interface IVergeConfig {
|
||||
app_log_level?: "trace" | "debug" | "info" | "warn" | "error" | string;
|
||||
language?: string;
|
||||
clash_core?: string;
|
||||
theme_mode?: "light" | "dark" | "system";
|
||||
theme_blur?: boolean;
|
||||
traffic_graph?: boolean;
|
||||
enable_memory_usage?: boolean;
|
||||
enable_tun_mode?: boolean;
|
||||
enable_auto_launch?: boolean;
|
||||
enable_service_mode?: boolean;
|
||||
|
@ -184,6 +186,7 @@ interface IVergeConfig {
|
|||
default_latency_test?: string;
|
||||
enable_clash_fields?: boolean;
|
||||
enable_builtin_enhanced?: boolean;
|
||||
auto_log_clean?: 0 | 1 | 2 | 3;
|
||||
proxy_layout_column?: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ export const OTHERS_FIELDS = [
|
|||
"listeners", // meta
|
||||
"sub-rules", // meta
|
||||
"geodata-mode", // meta
|
||||
"unified-delay", // meta
|
||||
"tcp-concurrent", // meta
|
||||
"enable-process", // meta
|
||||
"find-process-mode", // meta
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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"];
|
||||
const exp = Math.min(Math.floor(Math.log2(num) / 10), UNITS.length - 1);
|
||||
const dat = num / Math.pow(1024, exp);
|
||||
|
|
6
src/utils/truncate-str.ts
Normal file
6
src/utils/truncate-str.ts
Normal 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))
|
||||
);
|
||||
};
|
106
yarn.lock
106
yarn.lock
|
@ -792,70 +792,70 @@
|
|||
"@svgr/hast-util-to-babel-ast" "^6.3.1"
|
||||
svg-parser "^2.0.4"
|
||||
|
||||
"@tauri-apps/api@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/api/-/api-1.2.0.tgz#1f196b3e012971227f41b98214c846430a4eb477"
|
||||
integrity sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==
|
||||
"@tauri-apps/api@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.3.0.tgz#d0c853ab2cc7506bd826c5f7f260c67c7c15def5"
|
||||
integrity sha512-AH+3FonkKZNtfRtGrObY38PrzEj4d+1emCbwNGu0V2ENbXjlLHMZQlUh+Bhu/CRmjaIwZMGJ3yFvWaZZgTHoog==
|
||||
|
||||
"@tauri-apps/cli-darwin-arm64@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.3.tgz#dae9142e683c00199f4d7e088f22b564b08b9cac"
|
||||
integrity sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==
|
||||
"@tauri-apps/cli-darwin-arm64@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.3.1.tgz#ef0fe290e0a6e3e53fa2cc4f1a72a0c87921427c"
|
||||
integrity sha512-QlepYVPgOgspcwA/u4kGG4ZUijlXfdRtno00zEy+LxinN/IRXtk+6ErVtsmoLi1ZC9WbuMwzAcsRvqsD+RtNAg==
|
||||
|
||||
"@tauri-apps/cli-darwin-x64@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.3.tgz#c6f84a11a1a7800e3e8e22c8fa5b95d0b3d1f802"
|
||||
integrity sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==
|
||||
"@tauri-apps/cli-darwin-x64@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.3.1.tgz#4c84ea0f08a5b636b067943d637a38e091a4aad3"
|
||||
integrity sha512-fKcAUPVFO3jfDKXCSDGY0MhZFF/wDtx3rgFnogWYu4knk38o9RaqRkvMvqJhLYPuWaEM5h6/z1dRrr9KKCbrVg==
|
||||
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.3.tgz#ecccec4c255ab32903fb36e1c746ed7b4eff0d1d"
|
||||
integrity sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.3.1.tgz#a4f1b237189e4f8f89cc890e1dc2eec76d4345be"
|
||||
integrity sha512-+4H0dv8ltJHYu/Ma1h9ixUPUWka9EjaYa8nJfiMsdCI4LJLNE6cPveE7RmhZ59v9GW1XB108/k083JUC/OtGvA==
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-gnu@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.3.tgz#c3915de83a8fbe6f406eaa0b524a17c091a9a2cd"
|
||||
integrity sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==
|
||||
"@tauri-apps/cli-linux-arm64-gnu@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.3.1.tgz#e2391326b64dfe13c7442bdcc13c4988ce5e6df9"
|
||||
integrity sha512-Pj3odVO1JAxLjYmoXKxcrpj/tPxcA8UP8N06finhNtBtBaxAjrjjxKjO4968KB0BUH7AASIss9EL4Tr0FGnDuw==
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-musl@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.3.tgz#40f9f7cf0b4088964661fd412eff7310cb4ac605"
|
||||
integrity sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==
|
||||
"@tauri-apps/cli-linux-arm64-musl@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.3.1.tgz#49354349f80f879ffc6950c0c03c0aea1395efa5"
|
||||
integrity sha512-tA0JdDLPFaj42UDIVcF2t8V0tSha40rppcmAR/MfQpTCxih6399iMjwihz9kZE1n4b5O4KTq9GliYo50a8zYlQ==
|
||||
|
||||
"@tauri-apps/cli-linux-x64-gnu@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.3.tgz#0b3e4c1fda6205dbe872f4b69506669476f60591"
|
||||
integrity sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==
|
||||
"@tauri-apps/cli-linux-x64-gnu@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.3.1.tgz#9a33ffe9e0d9b1b3825db57cbcfcddeb773682c6"
|
||||
integrity sha512-FDU+Mnvk6NLkqQimcNojdKpMN4Y3W51+SQl+NqG9AFCWprCcSg62yRb84751ujZuf2MGT8HQOfmd0i77F4Q3tQ==
|
||||
|
||||
"@tauri-apps/cli-linux-x64-musl@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.3.tgz#edcf8f53da50337a2e763d4fda750ef56124036c"
|
||||
integrity sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==
|
||||
"@tauri-apps/cli-linux-x64-musl@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.3.1.tgz#5283731e894c17bc070c499e73145cfe2633ef21"
|
||||
integrity sha512-MpO3akXFmK8lZYEbyQRDfhdxz1JkTBhonVuz5rRqxwA7gnGWHa1aF1+/2zsy7ahjB2tQ9x8DDFDMdVE20o9HrA==
|
||||
|
||||
"@tauri-apps/cli-win32-ia32-msvc@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.3.tgz#0592d3e4eee4685674579ba897eef1469c6f1cfe"
|
||||
integrity sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==
|
||||
"@tauri-apps/cli-win32-ia32-msvc@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.3.1.tgz#f31538abfd94f27ade1f17d01f30da6be1660c6f"
|
||||
integrity sha512-9Boeo3K5sOrSBAZBuYyGkpV2RfnGQz3ZhGJt4hE6P+HxRd62lS6+qDKAiw1GmkZ0l1drc2INWrNeT50gwOKwIQ==
|
||||
|
||||
"@tauri-apps/cli-win32-x64-msvc@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.3.tgz#89f0cc36e11e56564161602cd6add155cc7b0dfb"
|
||||
integrity sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==
|
||||
"@tauri-apps/cli-win32-x64-msvc@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.3.1.tgz#1eb09d55b99916a3cd84cb91c75ef906db67d35d"
|
||||
integrity sha512-wMrTo91hUu5CdpbElrOmcZEoJR4aooTG+fbtcc87SMyPGQy1Ux62b+ZdwLvL1sVTxnIm//7v6QLRIWGiUjCPwA==
|
||||
|
||||
"@tauri-apps/cli@^1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.2.3.tgz#957f8a3a370f306e9e1ea5a891cb30aed91af64e"
|
||||
integrity sha512-erxtXuPhMEGJPBtnhPILD4AjuT81GZsraqpFvXAmEJZ2p8P6t7MVBifCL8LznRknznM3jn90D3M8RNBP3wcXTw==
|
||||
"@tauri-apps/cli@^1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.3.1.tgz#4c5259bf1f9c97084dd016e6b34dca53de380e24"
|
||||
integrity sha512-o4I0JujdITsVRm3/0spfJX7FcKYrYV1DXJqzlWIn6IY25/RltjU6qbC1TPgVww3RsRX63jyVUTcWpj5wwFl+EQ==
|
||||
optionalDependencies:
|
||||
"@tauri-apps/cli-darwin-arm64" "1.2.3"
|
||||
"@tauri-apps/cli-darwin-x64" "1.2.3"
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf" "1.2.3"
|
||||
"@tauri-apps/cli-linux-arm64-gnu" "1.2.3"
|
||||
"@tauri-apps/cli-linux-arm64-musl" "1.2.3"
|
||||
"@tauri-apps/cli-linux-x64-gnu" "1.2.3"
|
||||
"@tauri-apps/cli-linux-x64-musl" "1.2.3"
|
||||
"@tauri-apps/cli-win32-ia32-msvc" "1.2.3"
|
||||
"@tauri-apps/cli-win32-x64-msvc" "1.2.3"
|
||||
"@tauri-apps/cli-darwin-arm64" "1.3.1"
|
||||
"@tauri-apps/cli-darwin-x64" "1.3.1"
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf" "1.3.1"
|
||||
"@tauri-apps/cli-linux-arm64-gnu" "1.3.1"
|
||||
"@tauri-apps/cli-linux-arm64-musl" "1.3.1"
|
||||
"@tauri-apps/cli-linux-x64-gnu" "1.3.1"
|
||||
"@tauri-apps/cli-linux-x64-musl" "1.3.1"
|
||||
"@tauri-apps/cli-win32-ia32-msvc" "1.3.1"
|
||||
"@tauri-apps/cli-win32-x64-msvc" "1.3.1"
|
||||
|
||||
"@types/fs-extra@^9.0.13":
|
||||
version "9.0.13"
|
||||
|
|
Loading…
Add table
Reference in a new issue